From 3065d8e27aebf86c09456b436066fa056b1642e9 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 9 May 2026 09:03:12 +0200 Subject: [PATCH 001/153] Refactored service bus lifetiemes to singleton --- .../LiteCharms.Abstractions.csproj | 2 +- LiteCharms.Entities/LiteCharms.Entities.csproj | 2 +- LiteCharms.Extensions/LiteCharms.Extensions.csproj | 3 ++- LiteCharms.Extensions/Quartz.cs | 2 +- LiteCharms.Extensions/ServiceBus.cs | 11 ++++++----- LiteCharms.Features/LiteCharms.Features.csproj | 2 +- .../LiteCharms.Infrastructure.csproj | 2 +- LiteCharms.Models/LiteCharms.Models.csproj | 2 +- 8 files changed, 14 insertions(+), 12 deletions(-) diff --git a/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj b/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj index 7614de0..4074ad4 100644 --- a/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj +++ b/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj @@ -11,7 +11,7 @@ LiteCharms.Abstractions - 1.0.19 + 1.0.20 Khwezi Mngoma Lite Charms (PTY) Ltd Shared abstractions for Lite Charms applications. diff --git a/LiteCharms.Entities/LiteCharms.Entities.csproj b/LiteCharms.Entities/LiteCharms.Entities.csproj index 6def4d4..b32a09d 100644 --- a/LiteCharms.Entities/LiteCharms.Entities.csproj +++ b/LiteCharms.Entities/LiteCharms.Entities.csproj @@ -11,7 +11,7 @@ LiteCharms.Entities - 1.0.19 + 1.0.20 Khwezi Mngoma Lite Charms (PTY) Ltd Shared entities for Lite Charms applications. diff --git a/LiteCharms.Extensions/LiteCharms.Extensions.csproj b/LiteCharms.Extensions/LiteCharms.Extensions.csproj index 1e19321..3f531b7 100644 --- a/LiteCharms.Extensions/LiteCharms.Extensions.csproj +++ b/LiteCharms.Extensions/LiteCharms.Extensions.csproj @@ -22,7 +22,7 @@ LiteCharms.Extensions - 1.0.19 + 1.0.20 Khwezi Mngoma Lite Charms (PTY) Ltd Extension components for Lite Charms applications. @@ -49,6 +49,7 @@ + diff --git a/LiteCharms.Extensions/Quartz.cs b/LiteCharms.Extensions/Quartz.cs index a8ee941..ea71879 100644 --- a/LiteCharms.Extensions/Quartz.cs +++ b/LiteCharms.Extensions/Quartz.cs @@ -17,7 +17,7 @@ public static class Quartz { config.SchedulerName = schedulerName; config.SchedulerId = schedulerId; - + config.UseSimpleTypeLoader(); config.UseDefaultThreadPool(options => options.MaxConcurrency = 0); config.UseTimeZoneConverter(); diff --git a/LiteCharms.Extensions/ServiceBus.cs b/LiteCharms.Extensions/ServiceBus.cs index e71c074..ab8d7c0 100644 --- a/LiteCharms.Extensions/ServiceBus.cs +++ b/LiteCharms.Extensions/ServiceBus.cs @@ -1,26 +1,27 @@ using LiteCharms.Abstractions; using LiteCharms.Infrastructure.ServiceBus; using LiteCharms.Infrastructure.ServiceBus.Exchanges; +using LiteCharms.Infrastructure.ServiceBus.Queues; namespace LiteCharms.Extensions; public static class ServiceBus { public static IServiceCollection AddGeneralServiceBus(this IServiceCollection services) => services - .AddTransient() + .AddSingleton() .AddHostedService() - .AddKeyedTransient(Constants.GeneralServiceBus) + .AddKeyedSingleton(Constants.GeneralServiceBus) .AddMemoryCache(); public static IServiceCollection AddEmailServiceBus(this IServiceCollection services) => services - .AddTransient() + .AddSingleton() .AddHostedService() .AddKeyedTransient(Constants.EmailServiceBus) .AddMemoryCache(); public static IServiceCollection AddSalesServiceBus(this IServiceCollection services) => services - .AddTransient() + .AddSingleton() .AddHostedService() - .AddKeyedTransient(Constants.SalesServiceBus) + .AddKeyedSingleton(Constants.SalesServiceBus) .AddMemoryCache(); } diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index a0f6325..9a2a0ac 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -11,7 +11,7 @@ LiteCharms.Features - 1.0.19 + 1.0.20 Khwezi Mngoma Lite Charms (PTY) Ltd Feature components for Lite Charms applications. diff --git a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj b/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj index b18410a..9647c3a 100644 --- a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj +++ b/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj @@ -12,7 +12,7 @@ LiteCharms.Infrastructure - 1.0.19 + 1.0.20 Khwezi Mngoma Lite Charms (PTY) Ltd Infrastructure components for Lite Charms applications. diff --git a/LiteCharms.Models/LiteCharms.Models.csproj b/LiteCharms.Models/LiteCharms.Models.csproj index c3cd50b..95e00d1 100644 --- a/LiteCharms.Models/LiteCharms.Models.csproj +++ b/LiteCharms.Models/LiteCharms.Models.csproj @@ -11,7 +11,7 @@ LiteCharms.Models - 1.0.19 + 1.0.20 Khwezi Mngoma Lite Charms (PTY) Ltd Shared models for Lite Charms applications. From c95efd1fafc837103f1b5dab07696343b996ba44 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 9 May 2026 11:11:29 +0200 Subject: [PATCH 002/153] Added .drone.yml pipeline --- .drone.yml | 46 ++++++++ .editorconfig | 248 ++++++++++++++++++++++++++++++++++++++++++ LiteCharmsShared.slnx | 10 ++ 3 files changed, 304 insertions(+) create mode 100644 .drone.yml create mode 100644 .editorconfig diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..4b28bf7 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,46 @@ +kind: pipeline +type: docker +name: litecharms-nuget-libraries + +steps: + - name: build + image: mcr.microsoft.com/dotnet/sdk:10.0 + commands: + - dotnet restore + - dotnet build -c Release --no-restore + + - name: pack-and-publish + image: mcr.microsoft.com/dotnet/sdk:10.0 + environment: + NEXUS_KEY: + from_secret: nexus_api_key + NEXUS_URL: https://nexus.khongisa.co.za/repository/nuget-hosted/ + VERSION: 1.0.${DRONE_BUILD_NUMBER} + commands: + # Abstractions + - dotnet pack LiteCharms.Abstractions/LiteCharms.Abstractions.csproj -c Release -p:PackageVersion=$VERSION -o dist/ + - dotnet nuget push dist/LiteCharms.Abstractions.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + + # Models + - dotnet pack LiteCharms.Models/LiteCharms.Models.csproj -c Release -p:PackageVersion=$VERSION -o dist/ + - dotnet nuget push dist/LiteCharms.Models.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + + # Infrastructure + - dotnet pack LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj -c Release -p:PackageVersion=$VERSION -o dist/ + - dotnet nuget push dist/LiteCharms.Infrastructure.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + + # Features + - dotnet pack LiteCharms.Features/LiteCharms.Features.csproj -c Release -p:PackageVersion=$VERSION -o dist/ + - dotnet nuget push dist/LiteCharms.Features.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + + # Extensions + - dotnet pack LiteCharms.Extensions/LiteCharms.Extensions.csproj -c Release -p:PackageVersion=$VERSION -o dist/ + - dotnet nuget push dist/LiteCharms.Extensions.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + + # Entities + - dotnet pack LiteCharms.Entities/LiteCharms.Entities.csproj -c Release -p:PackageVersion=$VERSION -o dist/ + - dotnet nuget push dist/LiteCharms.Entities.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + +trigger: + branch: + - main \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a5bdbf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,248 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Code Actions #### + +# Type members +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties + +# Symbol search +dotnet_search_reference_assemblies = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_prefer_system_hash_code = true +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_non_hidden_explicit_cast_in_source = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = true +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_prefer_system_threading_lock = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_simple_property_accessors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_implicitly_typed_lambda_expression = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_unbound_generic_type_in_nameof = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index a08c005..f43a12e 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -1,4 +1,14 @@ + + + + + + + + + + From 8cb1943ac8e098d807d1fd580bdb618bd86f02a5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 9 May 2026 11:25:08 +0200 Subject: [PATCH 003/153] Added pipeline separator --- .drone.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.drone.yml b/.drone.yml index 4b28bf7..bdf7e1b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,3 +1,4 @@ +--- kind: pipeline type: docker name: litecharms-nuget-libraries From df1e31cc86221948ce08cfbf35d32346c472f0e3 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 9 May 2026 11:37:11 +0200 Subject: [PATCH 004/153] Changed target branch from nain to master --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index bdf7e1b..dd9227b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -44,4 +44,4 @@ steps: trigger: branch: - - main \ No newline at end of file + - master \ No newline at end of file From 60980292a2a9dad54d19fa42912c3592240a9fab Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 9 May 2026 11:55:43 +0200 Subject: [PATCH 005/153] Updated pipeline to use major versions only --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index dd9227b..3fcf813 100644 --- a/.drone.yml +++ b/.drone.yml @@ -16,7 +16,7 @@ steps: NEXUS_KEY: from_secret: nexus_api_key NEXUS_URL: https://nexus.khongisa.co.za/repository/nuget-hosted/ - VERSION: 1.0.${DRONE_BUILD_NUMBER} + VERSION: 1.${DRONE_BUILD_NUMBER} commands: # Abstractions - dotnet pack LiteCharms.Abstractions/LiteCharms.Abstractions.csproj -c Release -p:PackageVersion=$VERSION -o dist/ From 37e2116aaf6ed194dcb43f98be43626b27603059 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 9 May 2026 12:01:52 +0200 Subject: [PATCH 006/153] Updated pipeline to use major version with minor always 0 --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 3fcf813..2a7ba50 100644 --- a/.drone.yml +++ b/.drone.yml @@ -16,7 +16,7 @@ steps: NEXUS_KEY: from_secret: nexus_api_key NEXUS_URL: https://nexus.khongisa.co.za/repository/nuget-hosted/ - VERSION: 1.${DRONE_BUILD_NUMBER} + VERSION: 1.${DRONE_BUILD_NUMBER}.0 commands: # Abstractions - dotnet pack LiteCharms.Abstractions/LiteCharms.Abstractions.csproj -c Release -p:PackageVersion=$VERSION -o dist/ From bd852b30c38d3a8f3e7332bce7a318a76676f37f Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 9 May 2026 13:27:54 +0200 Subject: [PATCH 007/153] Fixed quartz table name prefix --- LiteCharms.Extensions/Quartz.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LiteCharms.Extensions/Quartz.cs b/LiteCharms.Extensions/Quartz.cs index ea71879..ec622e1 100644 --- a/LiteCharms.Extensions/Quartz.cs +++ b/LiteCharms.Extensions/Quartz.cs @@ -28,7 +28,7 @@ public static class Quartz storage.UseSystemTextJsonSerializer(); storage.SetProperty("quartz.jobStore.clustered", "true"); - storage.SetProperty("quartz.jobStore.tablePrefix", "quartz_"); + storage.SetProperty("quartz.jobStore.tablePrefix", "qrtz_"); storage.UsePostgres(connectionString!); storage.UseClustering(cluster => From 61118c622342a21f63809dc7084951e8b5df2ac1 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 9 May 2026 13:29:35 +0200 Subject: [PATCH 008/153] Fixed quartz host config table prefix --- LiteCharms.Extensions/Quartz.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LiteCharms.Extensions/Quartz.cs b/LiteCharms.Extensions/Quartz.cs index ec622e1..5f57b6c 100644 --- a/LiteCharms.Extensions/Quartz.cs +++ b/LiteCharms.Extensions/Quartz.cs @@ -66,7 +66,7 @@ public static class Quartz storage.UseSystemTextJsonSerializer(); storage.SetProperty("quartz.jobStore.clustered", "true"); - storage.SetProperty("quartz.jobStore.tablePrefix", "quartz_"); + storage.SetProperty("quartz.jobStore.tablePrefix", "qrtz_"); storage.UsePostgres(connectionString!); storage.UseClustering(cluster => From 6db6228bc9aedf422fee1335fcccb587e37010bb Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 9 May 2026 15:27:07 +0200 Subject: [PATCH 009/153] Added scheduler constants --- LiteCharms.Abstractions/Constants.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/LiteCharms.Abstractions/Constants.cs b/LiteCharms.Abstractions/Constants.cs index 6f533d2..9ad6cf5 100644 --- a/LiteCharms.Abstractions/Constants.cs +++ b/LiteCharms.Abstractions/Constants.cs @@ -4,6 +4,9 @@ public static class Constants { public const int QueueBounds = 100000; + public const string LeadGeneratorSchedulerName = "leadgenerator"; + public const string LeadGeneratorSchedulerInstanceId = "golden-dawn"; + public const string EmailServiceBus = nameof(EmailServiceBus); public const string GeneralServiceBus = nameof(GeneralServiceBus); public const string SalesServiceBus = nameof(SalesServiceBus); From ff34326a535598a6a7907e863b28c6e44847e7e5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 9 May 2026 16:58:34 +0200 Subject: [PATCH 010/153] Refactored database references --- LiteCharms.Abstractions/Constants.cs | 4 ++-- LiteCharms.Extensions/Postgres.cs | 2 +- .../Handlers/CreateCustomerCommandHandler.cs | 2 +- .../Handlers/UpdateCustomerCommandHandler.cs | 2 +- .../Handlers/GetCustomerQueryHandler.cs | 2 +- .../Handlers/GetCustomersQueryHandler.cs | 2 +- .../Handlers/CreateLeadCommandHandler.cs | 2 +- .../Handlers/UpdateLeadCommandHandler.cs | 2 +- .../Handlers/GetCustomerLeadsQueryHandler.cs | 2 +- .../Queries/Handlers/GetLeadsQueryHandler.cs | 2 +- .../CreateNotificationCommandHandler.cs | 2 +- .../UpdateNotificationCommandHandler.cs | 2 +- .../Handlers/GetNotificationQueryHandler.cs | 2 +- .../Handlers/GetNotificationsQueryHandler.cs | 2 +- .../Handlers/CreateOrderCommandHandler.cs | 2 +- .../UpdateOrderStatusCommandHandler.cs | 2 +- .../Handlers/GetCustomerOrdersQueryHandler.cs | 2 +- .../Handlers/GetOrderRefundQueryHandler.cs | 2 +- .../Queries/Handlers/GetOrdersQueryHandler.cs | 2 +- .../Handlers/GetProductPriceQueryHandler.cs | 2 +- .../Handlers/GetProductPricesQueryHandler.cs | 2 +- .../Handlers/GetProductQueryHandler.cs | 2 +- .../Handlers/GetProductsQueryHandler.cs | 2 +- .../AssignQuoteToOrderCommandHandler.cs | 2 +- ...AssignQuoteToShoppingCartCommandHandler.cs | 2 +- .../UpdateQuoteStatusCommandHandler.cs | 2 +- .../Handlers/GetCustomerQuotesQueryHandler.cs | 2 +- .../Queries/Handlers/GetQuoteQueryHandler.cs | 2 +- .../Queries/Handlers/GetQuotesHandler.cs | 2 +- .../Handlers/RefundCustomerCommandHandler.cs | 2 +- .../UpdateOrderRefundCommandHandler.cs | 2 +- .../GetCustomerRefundsQueryHandler.cs | 2 +- .../Queries/Handlers/GetRefundQueryHandler.cs | 2 +- .../AddItemToShoppingCartCommandHandler.cs | 2 +- .../CreateShoppingCartCommandHandler.cs | 2 +- .../EmptyShoppingCartCommandHandler.cs | 2 +- .../RemoveShoppingCartItemCommandHandler.cs | 2 +- .../UpdateShoppingCartItemCommandHandler.cs | 2 +- .../GetCustomerShoppingCartsQueryHandler.cs | 2 +- .../GetShoppingCartItemsQueryHandler.cs | 2 +- .../Handlers/GetShoppingCartQueryHandler.cs | 2 +- .../Database/LeadGeneratorDbContextFactory.cs | 19 ------------------- .../20260502231708_Init.Designer.cs | 2 +- ...123_DefinedEntityRelationships.Designer.cs | 2 +- ...03003624_RemovedLeadIdFromLead.Designer.cs | 2 +- ...260503012708_AddedStatusToLead.Designer.cs | 2 +- ...tedAttributionHashColumnOnLead.Designer.cs | 2 +- ...20260505120450_GeneralisedLead.Designer.cs | 2 +- ...60505123745_AddedNotifications.Designer.cs | 2 +- ...ProcessedColumnToNotifications.Designer.cs | 2 +- ...oppingCartalteredOrderCustomer.Designer.cs | 2 +- .../LeadGeneratorDbContextModelSnapshot.cs | 2 +- ...GeneratorDbContext.cs => ShopDbContext.cs} | 2 +- .../Database/ShopDbContextFactory.cs | 19 +++++++++++++++++++ .../HealthChecks/PostgresHealthCheck.cs | 2 +- 55 files changed, 73 insertions(+), 73 deletions(-) delete mode 100644 LiteCharms.Infrastructure/Database/LeadGeneratorDbContextFactory.cs rename LiteCharms.Infrastructure/Database/{LeadGeneratorDbContext.cs => ShopDbContext.cs} (85%) create mode 100644 LiteCharms.Infrastructure/Database/ShopDbContextFactory.cs diff --git a/LiteCharms.Abstractions/Constants.cs b/LiteCharms.Abstractions/Constants.cs index 9ad6cf5..80b40a6 100644 --- a/LiteCharms.Abstractions/Constants.cs +++ b/LiteCharms.Abstractions/Constants.cs @@ -4,8 +4,8 @@ public static class Constants { public const int QueueBounds = 100000; - public const string LeadGeneratorSchedulerName = "leadgenerator"; - public const string LeadGeneratorSchedulerInstanceId = "golden-dawn"; + public const string ShopSchedulerName = "shop"; + public const string ShopSchedulerInstanceId = "golden-dawn"; public const string EmailServiceBus = nameof(EmailServiceBus); public const string GeneralServiceBus = nameof(GeneralServiceBus); diff --git a/LiteCharms.Extensions/Postgres.cs b/LiteCharms.Extensions/Postgres.cs index d46bdaa..237c601 100644 --- a/LiteCharms.Extensions/Postgres.cs +++ b/LiteCharms.Extensions/Postgres.cs @@ -6,7 +6,7 @@ public static class Postgres { public static IServiceCollection AddLeadGeneratorDatabase(this IServiceCollection services, IConfiguration configuration) { - services.AddPooledDbContextFactory(options => + services.AddPooledDbContextFactory(options => options.UseNpgsql(configuration.GetConnectionString("PostgresLeadGenerator"))); return services; diff --git a/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs b/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs index d49e7c1..82f836b 100644 --- a/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs +++ b/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Customers.Commands.Handlers; -public class CreateCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class CreateCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(CreateCustomerCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs b/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs index e16469a..72b9109 100644 --- a/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs +++ b/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Customers.Commands.Handlers; -public class UpdateCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class UpdateCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(UpdateCustomerCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs b/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs index 09f1f9b..0f921db 100644 --- a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs +++ b/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Customers.Queries.Handlers; -public class GetCustomerQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetCustomerQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetCustomerQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs b/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs index e3e3d73..960ef3a 100644 --- a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs +++ b/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Customers.Queries.Handlers; -public class GetCustomersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetCustomersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetCustomersQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs b/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs index bd99665..bc8295b 100644 --- a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs +++ b/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs @@ -3,7 +3,7 @@ using LiteCharms.Infrastructure.Database; namespace LiteCharms.Features.Leads.Commands.Handlers; -public class CreateLeadCommandHandler(IDbContextFactory contextFactory, ISender mediator) : IRequestHandler> +public class CreateLeadCommandHandler(IDbContextFactory contextFactory, ISender mediator) : IRequestHandler> { public async ValueTask> Handle(CreateLeadCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs b/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs index 7187005..afaa15c 100644 --- a/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs +++ b/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Leads.Commands.Handlers; -public class UpdateLeadCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class UpdateLeadCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(UpdateLeadCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs b/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs index 630b9c5..ed90212 100644 --- a/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs +++ b/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Leads.Queries.Handlers; -public class GetCustomerLeadsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetCustomerLeadsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetCustomerLeadsQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs b/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs index 2496116..0ebaafb 100644 --- a/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs +++ b/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Leads.Queries.Handlers; -public class GetLeadsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetLeadsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetLeadsQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs b/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs index e57569b..1c5e251 100644 --- a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs +++ b/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Notifications.Commands.Handlers; -public class CreateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class CreateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(CreateNotificationCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs b/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs index 2adb588..066638a 100644 --- a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs +++ b/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Notifications.Commands.Handlers; -public class UpdateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class UpdateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(UpdateNotificationCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs index 133e3bf..106dd88 100644 --- a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs +++ b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Notifications.Queries.Handlers; -public class GetNotificationQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetNotificationQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetNotificationQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs index f6ac872..70e448f 100644 --- a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs +++ b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Notifications.Queries.Handlers; -public class GetNotificationsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetNotificationsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetNotificationsQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs b/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs index 1200afb..f06e53b 100644 --- a/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs +++ b/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Orders.Commands.Handlers; -public class CreateOrderCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class CreateOrderCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(CreateOrderCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs b/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs index 2524b79..0cfe348 100644 --- a/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs +++ b/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Orders.Commands.Handlers; -public class UpdateOrderStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class UpdateOrderStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(UpdateOrderStatusCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs b/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs index 6d683ce..eb9e79f 100644 --- a/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs +++ b/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Orders.Queries.Handlers; -public class GetCustomerOrdersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetCustomerOrdersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetCustomerOrdersQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs b/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs index 26bc1f2..47dd481 100644 --- a/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs +++ b/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Orders.Queries.Handlers; -public class GetOrderRefundQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetOrderRefundQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetOrderRefundQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs b/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs index 29b25a3..8276bb4 100644 --- a/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs +++ b/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Orders.Queries.Handlers; -public class GetOrdersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetOrdersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetOrdersQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs index 12bcc9f..f73bced 100644 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs +++ b/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Products.Queries.Handlers; -public class GetProductPriceQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetProductPriceQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetProductPriceQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs index 0f1f5af..c9eac0a 100644 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs +++ b/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Products.Queries.Handlers; -public class GetProductPricesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetProductPricesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetProductPricesQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs index cca604f..6cf09c6 100644 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs +++ b/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Products.Queries.Handlers; -public class GetProductQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetProductQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetProductQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs index 1b99b16..3fc0366 100644 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs +++ b/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Products.Queries.Handlers; -public class GetProductsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetProductsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetProductsQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs b/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs index 4178e6b..3145cd4 100644 --- a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs +++ b/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Quotes.Commands.Handlers; -public class AssignQuoteToOrderCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class AssignQuoteToOrderCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(AssignQuoteToOrderCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs b/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs index 8638079..443c907 100644 --- a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs +++ b/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Quotes.Commands.Handlers; -public class AssignQuoteToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class AssignQuoteToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(AssignQuoteToShoppingCartCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs b/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs index 70c5191..a279370 100644 --- a/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs +++ b/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Quotes.Commands.Handlers; -public class UpdateQuoteStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class UpdateQuoteStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(UpdateQuoteStatusCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs b/LiteCharms.Features/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs index 3243924..11c2169 100644 --- a/LiteCharms.Features/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs +++ b/LiteCharms.Features/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs @@ -5,7 +5,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Quotes.Queries.Handlers; -public class GetCustomerQuotesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetCustomerQuotesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetCustomerQuotesQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs b/LiteCharms.Features/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs index d8343cd..8986161 100644 --- a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs +++ b/LiteCharms.Features/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Quotes.Queries.Handlers; -public class GetQuoteQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetQuoteQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetQuoteQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuotesHandler.cs b/LiteCharms.Features/Quotes/Queries/Handlers/GetQuotesHandler.cs index adfae5e..3d548b0 100644 --- a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuotesHandler.cs +++ b/LiteCharms.Features/Quotes/Queries/Handlers/GetQuotesHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Quotes.Queries.Handlers; -public class GetQuotesHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetQuotesHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetQuotesQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs b/LiteCharms.Features/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs index 0b52018..c4021f7 100644 --- a/LiteCharms.Features/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs +++ b/LiteCharms.Features/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Refunds.Commands.Handlers; -public class RefundCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class RefundCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(RefundCustomerCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs b/LiteCharms.Features/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs index de1bf04..9f6fb00 100644 --- a/LiteCharms.Features/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs +++ b/LiteCharms.Features/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Refunds.Commands.Handlers; -public class UpdateOrderRefundCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class UpdateOrderRefundCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(UpdateOrderRefundCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs b/LiteCharms.Features/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs index 34b9f21..c198b76 100644 --- a/LiteCharms.Features/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs +++ b/LiteCharms.Features/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs @@ -5,7 +5,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Refunds.Queries.Handlers; -public class GetCustomerRefundsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetCustomerRefundsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetCustomerRefundsQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/Refunds/Queries/Handlers/GetRefundQueryHandler.cs b/LiteCharms.Features/Refunds/Queries/Handlers/GetRefundQueryHandler.cs index 29bd3e5..2561031 100644 --- a/LiteCharms.Features/Refunds/Queries/Handlers/GetRefundQueryHandler.cs +++ b/LiteCharms.Features/Refunds/Queries/Handlers/GetRefundQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.Refunds.Queries.Handlers; -public class GetRefundQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetRefundQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetRefundQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs index 3490517..dc4a50d 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs +++ b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; -public class AddItemToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class AddItemToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(AddItemToShoppingCartCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs index c6d17b0..241af60 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs +++ b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; -public class CreateShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class CreateShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(CreateShoppingCartCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs index 2787a8c..3f3ce2f 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs +++ b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; -public class EmptyShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class EmptyShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(EmptyShoppingCartCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs index 1d7944d..73255c5 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs +++ b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; -public class RemoveShoppingCartItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class RemoveShoppingCartItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(RemoveShoppingCartItemCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs index b1880ec..42b362f 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs +++ b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; -public class UpdateShoppingCartItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +public class UpdateShoppingCartItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { public async ValueTask Handle(UpdateShoppingCartItemCommand request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs index d4a6885..d6bde0f 100644 --- a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs +++ b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs @@ -5,7 +5,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; -public class GetCustomerShoppingCartsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetCustomerShoppingCartsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetCustomerShoppingCartsQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs index e523c25..0e7104c 100644 --- a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs +++ b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; -public class GetShoppingCartItemsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetShoppingCartItemsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetShoppingCartItemsQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs index d58ffc3..6793e22 100644 --- a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs +++ b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs @@ -4,7 +4,7 @@ using LiteCharms.Models; namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; -public class GetShoppingCartQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class GetShoppingCartQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { public async ValueTask> Handle(GetShoppingCartQuery request, CancellationToken cancellationToken) { diff --git a/LiteCharms.Infrastructure/Database/LeadGeneratorDbContextFactory.cs b/LiteCharms.Infrastructure/Database/LeadGeneratorDbContextFactory.cs deleted file mode 100644 index 0a18c5b..0000000 --- a/LiteCharms.Infrastructure/Database/LeadGeneratorDbContextFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace LiteCharms.Infrastructure.Database; - -public class LeadGeneratorDbContextFactory : IDesignTimeDbContextFactory -{ - public LeadGeneratorDbContext CreateDbContext(string[] args) - { - var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddUserSecrets(typeof(LeadGeneratorDbContext).Assembly) - .AddJsonFile("appsettings.json") - .AddEnvironmentVariables() - .Build(); - - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgresLeadGenerator")); - - return new LeadGeneratorDbContext(optionsBuilder.Options); - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.Designer.cs index 129f9fd..5168e4c 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.Designer.cs @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] + [DbContext(typeof(ShopDbContext))] [Migration("20260502231708_Init")] partial class Init { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.Designer.cs index 4d9b3f9..03d73e0 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.Designer.cs @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] + [DbContext(typeof(ShopDbContext))] [Migration("20260503002123_DefinedEntityRelationships")] partial class DefinedEntityRelationships { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.Designer.cs index 0fac239..f0f5e49 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.Designer.cs @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] + [DbContext(typeof(ShopDbContext))] [Migration("20260503003624_RemovedLeadIdFromLead")] partial class RemovedLeadIdFromLead { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.Designer.cs index 036f3c2..17dfee8 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.Designer.cs @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] + [DbContext(typeof(ShopDbContext))] [Migration("20260503012708_AddedStatusToLead")] partial class AddedStatusToLead { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.Designer.cs index 0a1ad72..6e40369 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.Designer.cs @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] + [DbContext(typeof(ShopDbContext))] [Migration("20260503133855_CorrectedAttributionHashColumnOnLead")] partial class CorrectedAttributionHashColumnOnLead { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs index 6f22981..d9c0beb 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] + [DbContext(typeof(ShopDbContext))] [Migration("20260505120450_GeneralisedLead")] partial class GeneralisedLead { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.Designer.cs index afd8292..9f54643 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.Designer.cs @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] + [DbContext(typeof(ShopDbContext))] [Migration("20260505123745_AddedNotifications")] partial class AddedNotifications { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.Designer.cs index ae0228c..8ec4ca5 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.Designer.cs @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] + [DbContext(typeof(ShopDbContext))] [Migration("20260505124135_AddedProcessedColumnToNotifications")] partial class AddedProcessedColumnToNotifications { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.Designer.cs index 3707216..90c8e8f 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.Designer.cs @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] + [DbContext(typeof(ShopDbContext))] [Migration("20260505202859_AddedQuoteShoppingCartalteredOrderCustomer")] partial class AddedQuoteShoppingCartalteredOrderCustomer { diff --git a/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs b/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs index a057ff3..09719f3 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs @@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] + [DbContext(typeof(ShopDbContext))] partial class LeadGeneratorDbContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) diff --git a/LiteCharms.Infrastructure/Database/LeadGeneratorDbContext.cs b/LiteCharms.Infrastructure/Database/ShopDbContext.cs similarity index 85% rename from LiteCharms.Infrastructure/Database/LeadGeneratorDbContext.cs rename to LiteCharms.Infrastructure/Database/ShopDbContext.cs index b3e7195..91de689 100644 --- a/LiteCharms.Infrastructure/Database/LeadGeneratorDbContext.cs +++ b/LiteCharms.Infrastructure/Database/ShopDbContext.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Infrastructure.Database; -public class LeadGeneratorDbContext(DbContextOptions options) : DbContext(options) +public class ShopDbContext(DbContextOptions options) : DbContext(options) { public DbSet Customers { get; set; } diff --git a/LiteCharms.Infrastructure/Database/ShopDbContextFactory.cs b/LiteCharms.Infrastructure/Database/ShopDbContextFactory.cs new file mode 100644 index 0000000..9de3cda --- /dev/null +++ b/LiteCharms.Infrastructure/Database/ShopDbContextFactory.cs @@ -0,0 +1,19 @@ +namespace LiteCharms.Infrastructure.Database; + +public class ShopDbContextFactory : IDesignTimeDbContextFactory +{ + public ShopDbContext CreateDbContext(string[] args) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddUserSecrets(typeof(ShopDbContext).Assembly) + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables() + .Build(); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgresShop")); + + return new ShopDbContext(optionsBuilder.Options); + } +} diff --git a/LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs b/LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs index 6680c17..9cb5f03 100644 --- a/LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs +++ b/LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs @@ -2,7 +2,7 @@ public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck { - private readonly string connectionString = configuration.GetConnectionString("PostgresLeadGenerator")!; + private readonly string connectionString = configuration.GetConnectionString("PostgresShop")!; public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { From fd6b8cd965bae52651a65f01a8577622c3a58a13 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 9 May 2026 17:44:16 +0200 Subject: [PATCH 011/153] Refactored postgres extension --- LiteCharms.Extensions/Postgres.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LiteCharms.Extensions/Postgres.cs b/LiteCharms.Extensions/Postgres.cs index 237c601..50c420c 100644 --- a/LiteCharms.Extensions/Postgres.cs +++ b/LiteCharms.Extensions/Postgres.cs @@ -4,10 +4,10 @@ namespace LiteCharms.Extensions; public static class Postgres { - public static IServiceCollection AddLeadGeneratorDatabase(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddShopDatabase(this IServiceCollection services, IConfiguration configuration) { services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString("PostgresLeadGenerator"))); + options.UseNpgsql(configuration.GetConnectionString("PostgresShop"))); return services; } From 9d64b5e76d9def69a60a7cdc6d42e60494621873 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 08:20:18 +0200 Subject: [PATCH 012/153] Refactored Quartz instance id to AUTO, removed constant --- LiteCharms.Abstractions/Constants.cs | 1 - LiteCharms.Extensions/Quartz.cs | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/LiteCharms.Abstractions/Constants.cs b/LiteCharms.Abstractions/Constants.cs index 80b40a6..31be72a 100644 --- a/LiteCharms.Abstractions/Constants.cs +++ b/LiteCharms.Abstractions/Constants.cs @@ -5,7 +5,6 @@ public static class Constants public const int QueueBounds = 100000; public const string ShopSchedulerName = "shop"; - public const string ShopSchedulerInstanceId = "golden-dawn"; public const string EmailServiceBus = nameof(EmailServiceBus); public const string GeneralServiceBus = nameof(GeneralServiceBus); diff --git a/LiteCharms.Extensions/Quartz.cs b/LiteCharms.Extensions/Quartz.cs index 5f57b6c..a92d462 100644 --- a/LiteCharms.Extensions/Quartz.cs +++ b/LiteCharms.Extensions/Quartz.cs @@ -7,7 +7,7 @@ public static class Quartz { private const string databaseConfigName = "PostgresScheduler"; - public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, string schedulerId, IConfiguration configuration) + public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration) { var connectionString = configuration.GetConnectionString(databaseConfigName); @@ -16,7 +16,7 @@ public static class Quartz services.AddQuartz(config => { config.SchedulerName = schedulerName; - config.SchedulerId = schedulerId; + config.SchedulerId = "AUTO"; config.UseSimpleTypeLoader(); config.UseDefaultThreadPool(options => options.MaxConcurrency = 0); @@ -42,7 +42,7 @@ public static class Quartz return services; } - public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, string schedulerId, IConfiguration configuration) + public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration) { var connectionString = configuration.GetConnectionString(databaseConfigName); @@ -51,7 +51,7 @@ public static class Quartz services.AddQuartz(config => { config.SchedulerName = schedulerName; - config.SchedulerId = schedulerId; + config.SchedulerId = "AUTO"; config.InterruptJobsOnShutdown = true; config.InterruptJobsOnShutdownWithWait = true; config.MaxBatchSize = 5; From a015801423c80a60d50e5c9344e230f29baa5ddc Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 09:18:54 +0200 Subject: [PATCH 013/153] Added tag and release step after publish --- .drone.yml | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/.drone.yml b/.drone.yml index 2a7ba50..20cb3f8 100644 --- a/.drone.yml +++ b/.drone.yml @@ -13,8 +13,7 @@ steps: - name: pack-and-publish image: mcr.microsoft.com/dotnet/sdk:10.0 environment: - NEXUS_KEY: - from_secret: nexus_api_key + NEXUS_KEY: { from_secret: nexus_api_key } NEXUS_URL: https://nexus.khongisa.co.za/repository/nuget-hosted/ VERSION: 1.${DRONE_BUILD_NUMBER}.0 commands: @@ -42,6 +41,31 @@ steps: - dotnet pack LiteCharms.Entities/LiteCharms.Entities.csproj -c Release -p:PackageVersion=$VERSION -o dist/ - dotnet nuget push dist/LiteCharms.Entities.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + - name: gitea-tag-release + image: alpine/git + environment: + GITEA_TOKEN: { from_secret: git_token } + GITEA_USER: { from_secret: git_username } + GITEA_PASS: { from_secret: git_password } + VERSION: 1.${DRONE_BUILD_NUMBER}.0 + commands: + - apk add --no-cache curl + - git remote set-url origin https://$${GITEA_USER}:$${GITEA_PASS}@gitea.khongisa.co.za/litecharms/libraries.git + - git tag $VERSION + - git push origin $VERSION + - | + curl -X POST "https://gitea.khongisa.co.za/api/v1/repos/litecharms/libraries/releases" \ + -H "Authorization: token $${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"tag_name\": \"$VERSION\", + \"target_commitish\": \"${DRONE_COMMIT_SHA}\", + \"name\": \"Library Suite $VERSION\", + \"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Abstractions\n* LiteCharms.Models\n* LiteCharms.Infrastructure\n* LiteCharms.Features\n* LiteCharms.Extensions\n* LiteCharms.Entities\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\", + \"draft\": false, + \"prerelease\": false + }" + trigger: - branch: - - master \ No newline at end of file + event: + - pull_request \ No newline at end of file From 193496bbf96e55ab64e16bcf7fad8fcbd2df7ecf Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 09:22:09 +0200 Subject: [PATCH 014/153] fixed git repo naming --- .drone.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 20cb3f8..0c10662 100644 --- a/.drone.yml +++ b/.drone.yml @@ -50,11 +50,11 @@ steps: VERSION: 1.${DRONE_BUILD_NUMBER}.0 commands: - apk add --no-cache curl - - git remote set-url origin https://$${GITEA_USER}:$${GITEA_PASS}@gitea.khongisa.co.za/litecharms/libraries.git + - git remote set-url origin https://$${GITEA_USER}:$${GITEA_PASS}@gitea.khongisa.co.za/litecharms/components.git - git tag $VERSION - git push origin $VERSION - | - curl -X POST "https://gitea.khongisa.co.za/api/v1/repos/litecharms/libraries/releases" \ + curl -X POST "https://gitea.khongisa.co.za/api/v1/repos/litecharms/components/releases" \ -H "Authorization: token $${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ -d "{ From 275befc258484ec20df241d0afd42c82510ebd3e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 09:48:12 +0200 Subject: [PATCH 015/153] Populated README --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f355470..aa43ab3 100644 --- a/README.md +++ b/README.md @@ -1 +1,23 @@ -# LiteCharmsShared \ No newline at end of file +# LiteCharms.Components + +A suite of shared libraries providing core logic, models, and infrastructure for the LiteCharms ecosystem. + +## 📦 Published Packages +All packages are versioned and pushed to [Nexus](https://nexus.khongisa.co.za) automatically via CI/CD. + +* **Abstractions:** Interfaces and base contracts. +* **Models:** Shared DTOs and Request/Response objects. +* **Entities:** Database schema definitions. +* **Infrastructure:** Persistence and external service clients. +* **Features:** Core business logic modules. +* **Extensions:** Shared helper methods and DI configurations. + +## 🚀 CI/CD Workflow +1. Create a **Pull Request** to `master`. +2. Drone CI builds, tests, and packs the suite as `1.${DRONE_BUILD_NUMBER}.0`. +3. Packages are pushed to the Nexus `nuget-hosted` repository. +4. A formal **Gitea Release** is created with the versioned "Bill of Materials." + +## 🛠 Usage +Add the Nexus NuGet source to your local environment and reference the version listed in the latest Gitea Release: +`dotnet add package LiteCharms.Models -v 1.XX.0` \ No newline at end of file From 32d1019eb5a96665d31572405c362e83a1fec02d Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 11:16:52 +0200 Subject: [PATCH 016/153] Migrated database changes --- .../NotificationConfiguration.cs | 12 +- .../Configuration/OrderConfiguration.cs | 3 + .../Configuration/PackageConfirguration.cs | 16 + .../Configuration/PackageItemConfiguration.cs | 20 + .../Configuration/QuoteConfiguration.cs | 2 +- .../ShoppingCartConfiguration.cs | 2 +- .../ShoppingCartItemConfiguration.cs | 2 +- .../ShoppingCartPackageConfiguration.cs | 24 + LiteCharms.Entities/Package.cs | 9 + LiteCharms.Entities/PackageItem.cs | 9 + LiteCharms.Entities/ShoppingCartPackage.cs | 11 + LiteCharms.Extensions/EntityModeMappers.cs | 8 +- .../Commands/CreateNotificationCommand.cs | 9 +- .../CreateNotificationCommandHandler.cs | 8 +- .../20260502231708_Init.Designer.cs | 272 ------------ .../Migrations/20260502231708_Init.cs | 154 ------- ...123_DefinedEntityRelationships.Designer.cs | 357 --------------- ...260503002123_DefinedEntityRelationships.cs | 175 -------- ...03003624_RemovedLeadIdFromLead.Designer.cs | 354 --------------- .../20260503003624_RemovedLeadIdFromLead.cs | 29 -- ...260503012708_AddedStatusToLead.Designer.cs | 357 --------------- .../20260503012708_AddedStatusToLead.cs | 29 -- ...tedAttributionHashColumnOnLead.Designer.cs | 357 --------------- ...55_CorrectedAttributionHashColumnOnLead.cs | 28 -- ...20260505120450_GeneralisedLead.Designer.cs | 360 --------------- .../20260505120450_GeneralisedLead.cs | 38 -- ...60505123745_AddedNotifications.Designer.cs | 409 ------------------ .../20260505123745_AddedNotifications.cs | 43 -- ...135_AddedProcessedColumnToNotifications.cs | 47 -- ...edQuoteShoppingCartalteredOrderCustomer.cs | 227 ---------- ...ner.cs => 20260510090446_Init.Designer.cs} | 65 ++- .../Migrations/20260510090446_Init.cs | 382 ++++++++++++++++ ... 20260510091540_AddedPackages.Designer.cs} | 384 +++++++++++++++- .../20260510091540_AddedPackages.cs | 114 +++++ ...pshot.cs => ShopDbContextModelSnapshot.cs} | 190 +++++++- .../Database/ShopDbContext.cs | 6 + .../LiteCharms.Infrastructure.csproj | 4 + LiteCharms.Models/Enums.cs | 16 + LiteCharms.Models/Notification.cs | 20 +- LiteCharms.Models/Order.cs | 8 +- LiteCharms.Models/Package.cs | 16 + LiteCharms.Models/PackageItem.cs | 14 + LiteCharms.Models/ShoppingCartPackage.cs | 12 + LiteCharmsShared.slnx | 7 - 44 files changed, 1279 insertions(+), 3330 deletions(-) create mode 100644 LiteCharms.Entities/Configuration/PackageConfirguration.cs create mode 100644 LiteCharms.Entities/Configuration/PackageItemConfiguration.cs create mode 100644 LiteCharms.Entities/Configuration/ShoppingCartPackageConfiguration.cs create mode 100644 LiteCharms.Entities/Package.cs create mode 100644 LiteCharms.Entities/PackageItem.cs create mode 100644 LiteCharms.Entities/ShoppingCartPackage.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.Designer.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.Designer.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.Designer.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.Designer.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.Designer.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.Designer.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.cs rename LiteCharms.Infrastructure/Database/Migrations/{20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.Designer.cs => 20260510090446_Init.Designer.cs} (93%) create mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.cs rename LiteCharms.Infrastructure/Database/Migrations/{20260505124135_AddedProcessedColumnToNotifications.Designer.cs => 20260510091540_AddedPackages.Designer.cs} (53%) create mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs rename LiteCharms.Infrastructure/Database/Migrations/{LeadGeneratorDbContextModelSnapshot.cs => ShopDbContextModelSnapshot.cs} (77%) create mode 100644 LiteCharms.Models/Package.cs create mode 100644 LiteCharms.Models/PackageItem.cs create mode 100644 LiteCharms.Models/ShoppingCartPackage.cs diff --git a/LiteCharms.Entities/Configuration/NotificationConfiguration.cs b/LiteCharms.Entities/Configuration/NotificationConfiguration.cs index c5879f9..bd103f7 100644 --- a/LiteCharms.Entities/Configuration/NotificationConfiguration.cs +++ b/LiteCharms.Entities/Configuration/NotificationConfiguration.cs @@ -8,14 +8,18 @@ public class NotificationConfiguration : IEntityTypeConfiguration builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); + builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); builder.Property(f => f.Direction).IsRequired(); - builder.Property(f => f.Author).IsRequired(); - builder.Property(f => f.Title).IsRequired(); - builder.Property(f => f.Description).IsRequired(); builder.Property(f => f.Platform).IsRequired(); - builder.Property(f => f.PlatformAddress).IsRequired(); + builder.Property(f => f.Priority).IsRequired(); + builder.Property(f => f.Sender).IsRequired(); + builder.Property(f => f.Subject).IsRequired(); + builder.Property(f => f.Message).IsRequired(); + builder.Property(f => f.Recipient).IsRequired(); + builder.Property(f => f.RecipientAddress).IsRequired(); builder.Property(f => f.CorrelationId).IsRequired(); builder.Property(f => f.CorrelationIdType).IsRequired(); + builder.Property(f => f.IsHtml).HasDefaultValue(false); builder.Property(f => f.IsInternal).HasDefaultValue(true); builder.Property(f => f.Processed).HasDefaultValue(false); } diff --git a/LiteCharms.Entities/Configuration/OrderConfiguration.cs b/LiteCharms.Entities/Configuration/OrderConfiguration.cs index 5db15cd..138e136 100644 --- a/LiteCharms.Entities/Configuration/OrderConfiguration.cs +++ b/LiteCharms.Entities/Configuration/OrderConfiguration.cs @@ -14,7 +14,10 @@ public class OrderConfiguration : IEntityTypeConfiguration builder.Property(f => f.RefundId).IsRequired(false); builder.Property(f => f.ShoppingCartId).IsRequired(); builder.Property(f => f.Status).HasConversion().IsRequired(); + builder.Property(f => f.Requirements).HasColumnType("jsonb").IsRequired(false); builder.Property(f => f.Notes).HasColumnType("jsonb").IsRequired(false); + builder.Property(f => f.Terms).HasColumnType("jsonb").IsRequired(false); + builder.Property(f => f.DepositRequired); builder.HasOne(f => f.Quote) .WithOne(f => f.Order) diff --git a/LiteCharms.Entities/Configuration/PackageConfirguration.cs b/LiteCharms.Entities/Configuration/PackageConfirguration.cs new file mode 100644 index 0000000..71aa4fc --- /dev/null +++ b/LiteCharms.Entities/Configuration/PackageConfirguration.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Entities.Configuration; + +public class PackageConfirguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Package)); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); + builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); + builder.Property(f => f.Name).IsRequired(); + builder.Property(f => f.Description).IsRequired(); + builder.Property(f => f.Active); + } +} diff --git a/LiteCharms.Entities/Configuration/PackageItemConfiguration.cs b/LiteCharms.Entities/Configuration/PackageItemConfiguration.cs new file mode 100644 index 0000000..bf31659 --- /dev/null +++ b/LiteCharms.Entities/Configuration/PackageItemConfiguration.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Entities.Configuration; + +public class PackageItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(PackageItem)); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); + builder.Property(f => f.PackageId).IsRequired(); + builder.Property(f => f.ProductPriceId).IsRequired(); + builder.Property(f => f.Active); + + builder.HasOne(f => f.Package) + .WithMany() + .HasForeignKey(f => f.PackageId) + .OnDelete(DeleteBehavior.NoAction); + } +} diff --git a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs b/LiteCharms.Entities/Configuration/QuoteConfiguration.cs index 37125ae..de5240a 100644 --- a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs +++ b/LiteCharms.Entities/Configuration/QuoteConfiguration.cs @@ -8,7 +8,7 @@ public class QuoteConfiguration : IEntityTypeConfiguration builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired().ValueGeneratedOnAddOrUpdate(); + builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); builder.Property(f => f.ExpiredAt).IsRequired(false); builder.Property(f => f.CustomerId).IsRequired(); builder.Property(f => f.Status).IsRequired(); diff --git a/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs b/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs index 3a1926b..109fd99 100644 --- a/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs +++ b/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs @@ -8,7 +8,7 @@ public class ShoppingCartConfiguration : IEntityTypeConfiguration builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired().ValueGeneratedOnAddOrUpdate(); + builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); builder.Property(f => f.CustomerId).IsRequired(false); builder.Property(f => f.OrderId).IsRequired(false); builder.Property(f => f.QuoteId).IsRequired(false); diff --git a/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs b/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs index 00fc2ab..948f988 100644 --- a/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs +++ b/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs @@ -8,7 +8,7 @@ public class ShoppingCartItemConfiguration : IEntityTypeConfiguration f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired().ValueGeneratedOnAddOrUpdate(); + builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); builder.Property(f => f.Quantity).IsRequired().HasDefaultValue(1); builder.Property(f => f.ProductPriceId).IsRequired(); diff --git a/LiteCharms.Entities/Configuration/ShoppingCartPackageConfiguration.cs b/LiteCharms.Entities/Configuration/ShoppingCartPackageConfiguration.cs new file mode 100644 index 0000000..7d2f296 --- /dev/null +++ b/LiteCharms.Entities/Configuration/ShoppingCartPackageConfiguration.cs @@ -0,0 +1,24 @@ +namespace LiteCharms.Entities.Configuration; + +public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(ShoppingCartPackage)); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); + builder.Property(f => f.ShoppingCartId).IsRequired(); + builder.Property(f => f.PackageId).IsRequired(); + + builder.HasOne(f => f.Package) + .WithMany() + .HasForeignKey(f => f.PackageId) + .OnDelete(DeleteBehavior.NoAction); + + builder.HasOne(f => f.ShoppingCart) + .WithMany() + .HasForeignKey(f => f.ShoppingCartId) + .OnDelete(DeleteBehavior.NoAction); + } +} diff --git a/LiteCharms.Entities/Package.cs b/LiteCharms.Entities/Package.cs new file mode 100644 index 0000000..ce8e799 --- /dev/null +++ b/LiteCharms.Entities/Package.cs @@ -0,0 +1,9 @@ +using LiteCharms.Entities.Configuration; + +namespace LiteCharms.Entities; + +[EntityTypeConfiguration] +public class Package : Models.Package +{ + public virtual ICollection? PackageItems { get; set; } +} diff --git a/LiteCharms.Entities/PackageItem.cs b/LiteCharms.Entities/PackageItem.cs new file mode 100644 index 0000000..986fc75 --- /dev/null +++ b/LiteCharms.Entities/PackageItem.cs @@ -0,0 +1,9 @@ +using LiteCharms.Entities.Configuration; + +namespace LiteCharms.Entities; + +[EntityTypeConfiguration] +public class PackageItem : Models.PackageItem +{ + public virtual Package? Package { get; set; } +} diff --git a/LiteCharms.Entities/ShoppingCartPackage.cs b/LiteCharms.Entities/ShoppingCartPackage.cs new file mode 100644 index 0000000..45a3831 --- /dev/null +++ b/LiteCharms.Entities/ShoppingCartPackage.cs @@ -0,0 +1,11 @@ +using LiteCharms.Entities.Configuration; + +namespace LiteCharms.Entities; + +[EntityTypeConfiguration] +public class ShoppingCartPackage : Models.ShoppingCartPackage +{ + public virtual ShoppingCart? ShoppingCart { get; set; } + + public virtual Package? Package { get; set; } +} diff --git a/LiteCharms.Extensions/EntityModeMappers.cs b/LiteCharms.Extensions/EntityModeMappers.cs index 8d41155..b3b8e64 100644 --- a/LiteCharms.Extensions/EntityModeMappers.cs +++ b/LiteCharms.Extensions/EntityModeMappers.cs @@ -44,15 +44,15 @@ public static class EntityModeMappers { Id = entity.Id, CreatedAt = entity.CreatedAt, - Description = entity.Description, + Message = entity.Message, Direction = entity.Direction, CorrelationId = entity.CorrelationId, CorrelationIdType = entity.CorrelationIdType, IsInternal = entity.IsInternal, - Author = entity.Author, + Sender = entity.Sender, Platform = entity.Platform, - PlatformAddress = entity.PlatformAddress, - Title = entity.Title, + Recipient = entity.Recipient, + Subject = entity.Subject, Processed = entity.Processed }; diff --git a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs index cfa40b1..be81438 100644 --- a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs +++ b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs @@ -12,7 +12,7 @@ public class CreateNotificationCommand : IRequest> public string? Description { get; set; } - public string? Platform { get; set; } + public NotificationPlatforms Platform { get; set; } public string? PlatformAddress { get; set; } @@ -22,7 +22,7 @@ public class CreateNotificationCommand : IRequest> public bool IsInternal { get; set; } - private CreateNotificationCommand(NotificationDirection direction, string author, string title, string description, string platform, string platformAddress, string correlationId, string correlationIdType, bool isInternal) + private CreateNotificationCommand(NotificationDirection direction, string author, string title, string description, NotificationPlatforms platform, string platformAddress, string correlationId, string correlationIdType, bool isInternal) { Direction = direction; Author = author; @@ -35,7 +35,7 @@ public class CreateNotificationCommand : IRequest> IsInternal = isInternal; } - public static CreateNotificationCommand Create(NotificationDirection direction, string author, string title, string description, string platform, string platformAddress, string correlationId, string correlationIdType, bool isInternal) + public static CreateNotificationCommand Create(NotificationDirection direction, string author, string title, string description, NotificationPlatforms platform, string platformAddress, string correlationId, string correlationIdType, bool isInternal) { if (string.IsNullOrWhiteSpace(author)) throw new ArgumentException("Author cannot be null or whitespace.", nameof(author)); @@ -46,9 +46,6 @@ public class CreateNotificationCommand : IRequest> if (string.IsNullOrWhiteSpace(description)) throw new ArgumentException("Description cannot be null or whitespace.", nameof(description)); - if (string.IsNullOrWhiteSpace(platform)) - throw new ArgumentException("Platform cannot be null or whitespace.", nameof(platform)); - if (string.IsNullOrWhiteSpace(platformAddress)) throw new ArgumentException("PlatformAddress cannot be null or whitespace.", nameof(platformAddress)); diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs b/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs index 1c5e251..d33fad6 100644 --- a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs +++ b/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs @@ -13,11 +13,11 @@ public class CreateNotificationCommandHandler(IDbContextFactory c var newNotification = context.Notifications.Add(new Entities.Notification { Direction = request.Direction, - Author = request.Author, - Title = request.Title, - Description = request.Description, + Sender = request.Author, + Subject = request.Title, + Message = request.Description, Platform = request.Platform, - PlatformAddress = request.PlatformAddress, + Recipient = request.PlatformAddress, CorrelationId = request.CorrelationId, CorrelationIdType = request.CorrelationIdType, IsInternal = request.IsInternal, diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.Designer.cs deleted file mode 100644 index 5168e4c..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.Designer.cs +++ /dev/null @@ -1,272 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(ShopDbContext))] - [Migration("20260502231708_Init")] - partial class Init - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttribusionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("GoogleClickId") - .HasColumnType("text"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("LeadId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("ProductPrice", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.cs b/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.cs deleted file mode 100644 index 60c879e..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class Init : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Customer", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - Company = table.Column(type: "text", nullable: true), - Name = table.Column(type: "text", nullable: false), - LastName = table.Column(type: "text", nullable: false), - Tax = table.Column(type: "text", nullable: true), - Email = table.Column(type: "text", nullable: false), - Discord = table.Column(type: "text", nullable: true), - Slack = table.Column(type: "text", nullable: true), - LinkedIn = table.Column(type: "text", nullable: true), - Whatsapp = table.Column(type: "text", nullable: true), - Website = table.Column(type: "text", nullable: true), - Phone = table.Column(type: "text", nullable: true), - Address = table.Column(type: "text", nullable: true), - City = table.Column(type: "text", nullable: true), - Region = table.Column(type: "text", nullable: true), - Country = table.Column(type: "text", nullable: true), - PostalCode = table.Column(type: "text", nullable: true), - Active = table.Column(type: "boolean", nullable: false, defaultValue: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Customer", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Lead", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - GoogleClickId = table.Column(type: "text", nullable: true), - WebClickId = table.Column(type: "text", nullable: true), - AppClickId = table.Column(type: "text", nullable: true), - CampaignId = table.Column(type: "bigint", nullable: true), - AdGroupId = table.Column(type: "bigint", nullable: true), - AdName = table.Column(type: "bigint", nullable: true), - TargetId = table.Column(type: "bigint", nullable: true), - FeedItemId = table.Column(type: "bigint", nullable: true), - ClickLocation = table.Column(type: "text", nullable: true), - AttribusionHash = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Lead", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Order", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - LeadId = table.Column(type: "uuid", nullable: false), - ProductId = table.Column(type: "uuid", nullable: false), - ProductPriceId = table.Column(type: "uuid", nullable: false), - Status = table.Column(type: "integer", nullable: false), - Notes = table.Column(type: "jsonb", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Order", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "OrderRefund", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - OrderId = table.Column(type: "uuid", nullable: false), - Reason = table.Column(type: "text", nullable: false), - Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_OrderRefund", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Product", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), - Active = table.Column(type: "boolean", nullable: false, defaultValue: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Product", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ProductPrice", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - ProductId = table.Column(type: "uuid", nullable: false), - Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - Discount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - Active = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ProductPrice", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Customer"); - - migrationBuilder.DropTable( - name: "Lead"); - - migrationBuilder.DropTable( - name: "Order"); - - migrationBuilder.DropTable( - name: "OrderRefund"); - - migrationBuilder.DropTable( - name: "Product"); - - migrationBuilder.DropTable( - name: "ProductPrice"); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.Designer.cs deleted file mode 100644 index 03d73e0..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.Designer.cs +++ /dev/null @@ -1,357 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(ShopDbContext))] - [Migration("20260503002123_DefinedEntityRelationships")] - partial class DefinedEntityRelationships - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttribusionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("GoogleClickId") - .HasColumnType("text"); - - b.Property("LeadId") - .HasColumnType("uuid"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LeadGenerator.Database.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.HasOne("LeadGenerator.Database.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LeadGenerator.Database.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.HasOne("LeadGenerator.Database.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.cs deleted file mode 100644 index 059f789..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class DefinedEntityRelationships : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LeadId", - table: "Order"); - - migrationBuilder.RenameColumn( - name: "ProductId", - table: "Order", - newName: "CustomerId"); - - migrationBuilder.AddColumn( - name: "RefundId", - table: "Order", - type: "uuid", - nullable: true); - - migrationBuilder.AddColumn( - name: "CustomerId", - table: "Lead", - type: "uuid", - nullable: true); - - migrationBuilder.AddColumn( - name: "LeadId", - table: "Lead", - type: "uuid", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_ProductPrice_ProductId", - table: "ProductPrice", - column: "ProductId"); - - migrationBuilder.CreateIndex( - name: "IX_OrderRefund_OrderId", - table: "OrderRefund", - column: "OrderId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Order_CustomerId", - table: "Order", - column: "CustomerId"); - - migrationBuilder.CreateIndex( - name: "IX_Order_ProductPriceId", - table: "Order", - column: "ProductPriceId"); - - migrationBuilder.CreateIndex( - name: "IX_Lead_CustomerId", - table: "Lead", - column: "CustomerId"); - - migrationBuilder.AddForeignKey( - name: "FK_Lead_Customer_CustomerId", - table: "Lead", - column: "CustomerId", - principalTable: "Customer", - principalColumn: "Id"); - - migrationBuilder.AddForeignKey( - name: "FK_Order_Customer_CustomerId", - table: "Order", - column: "CustomerId", - principalTable: "Customer", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - - migrationBuilder.AddForeignKey( - name: "FK_Order_ProductPrice_ProductPriceId", - table: "Order", - column: "ProductPriceId", - principalTable: "ProductPrice", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - - migrationBuilder.AddForeignKey( - name: "FK_OrderRefund_Order_OrderId", - table: "OrderRefund", - column: "OrderId", - principalTable: "Order", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_ProductPrice_Product_ProductId", - table: "ProductPrice", - column: "ProductId", - principalTable: "Product", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Lead_Customer_CustomerId", - table: "Lead"); - - migrationBuilder.DropForeignKey( - name: "FK_Order_Customer_CustomerId", - table: "Order"); - - migrationBuilder.DropForeignKey( - name: "FK_Order_ProductPrice_ProductPriceId", - table: "Order"); - - migrationBuilder.DropForeignKey( - name: "FK_OrderRefund_Order_OrderId", - table: "OrderRefund"); - - migrationBuilder.DropForeignKey( - name: "FK_ProductPrice_Product_ProductId", - table: "ProductPrice"); - - migrationBuilder.DropIndex( - name: "IX_ProductPrice_ProductId", - table: "ProductPrice"); - - migrationBuilder.DropIndex( - name: "IX_OrderRefund_OrderId", - table: "OrderRefund"); - - migrationBuilder.DropIndex( - name: "IX_Order_CustomerId", - table: "Order"); - - migrationBuilder.DropIndex( - name: "IX_Order_ProductPriceId", - table: "Order"); - - migrationBuilder.DropIndex( - name: "IX_Lead_CustomerId", - table: "Lead"); - - migrationBuilder.DropColumn( - name: "RefundId", - table: "Order"); - - migrationBuilder.DropColumn( - name: "CustomerId", - table: "Lead"); - - migrationBuilder.DropColumn( - name: "LeadId", - table: "Lead"); - - migrationBuilder.RenameColumn( - name: "CustomerId", - table: "Order", - newName: "ProductId"); - - migrationBuilder.AddColumn( - name: "LeadId", - table: "Order", - type: "uuid", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.Designer.cs deleted file mode 100644 index f0f5e49..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.Designer.cs +++ /dev/null @@ -1,354 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(ShopDbContext))] - [Migration("20260503003624_RemovedLeadIdFromLead")] - partial class RemovedLeadIdFromLead - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttribusionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("GoogleClickId") - .HasColumnType("text"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LeadGenerator.Database.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.HasOne("LeadGenerator.Database.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LeadGenerator.Database.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.HasOne("LeadGenerator.Database.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.cs deleted file mode 100644 index d469ce3..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class RemovedLeadIdFromLead : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LeadId", - table: "Lead"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LeadId", - table: "Lead", - type: "uuid", - nullable: true); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.Designer.cs deleted file mode 100644 index 17dfee8..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.Designer.cs +++ /dev/null @@ -1,357 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(ShopDbContext))] - [Migration("20260503012708_AddedStatusToLead")] - partial class AddedStatusToLead - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttribusionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("GoogleClickId") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LeadGenerator.Database.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.HasOne("LeadGenerator.Database.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LeadGenerator.Database.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.HasOne("LeadGenerator.Database.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.cs deleted file mode 100644 index 71b82d1..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class AddedStatusToLead : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Status", - table: "Lead", - type: "integer", - nullable: false, - defaultValue: 0); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Status", - table: "Lead"); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.Designer.cs deleted file mode 100644 index 6e40369..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.Designer.cs +++ /dev/null @@ -1,357 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(ShopDbContext))] - [Migration("20260503133855_CorrectedAttributionHashColumnOnLead")] - partial class CorrectedAttributionHashColumnOnLead - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttributionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("GoogleClickId") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.HasOne("LiteCharms.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.cs deleted file mode 100644 index 9d1fd30..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class CorrectedAttributionHashColumnOnLead : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "AttribusionHash", - table: "Lead", - newName: "AttributionHash"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "AttributionHash", - table: "Lead", - newName: "AttribusionHash"); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs deleted file mode 100644 index d9c0beb..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs +++ /dev/null @@ -1,360 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(ShopDbContext))] - [Migration("20260505120450_GeneralisedLead")] - partial class GeneralisedLead - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttributionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickId") - .HasColumnType("text"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("Source") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.HasOne("LiteCharms.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.cs deleted file mode 100644 index 6176058..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class GeneralisedLead : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "GoogleClickId", - table: "Lead", - newName: "Source"); - - migrationBuilder.AddColumn( - name: "ClickId", - table: "Lead", - type: "text", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ClickId", - table: "Lead"); - - migrationBuilder.RenameColumn( - name: "Source", - table: "Lead", - newName: "GoogleClickId"); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.Designer.cs deleted file mode 100644 index 9f54643..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.Designer.cs +++ /dev/null @@ -1,409 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(ShopDbContext))] - [Migration("20260505123745_AddedNotifications")] - partial class AddedNotifications - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttributionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickId") - .HasColumnType("text"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("Source") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Notification", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Author") - .IsRequired() - .HasColumnType("text"); - - b.Property("CorrelationId") - .IsRequired() - .HasColumnType("text"); - - b.Property("CorrelationIdType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Direction") - .HasColumnType("integer"); - - b.Property("IsInternal") - .HasColumnType("boolean"); - - b.Property("Platform") - .IsRequired() - .HasColumnType("text"); - - b.Property("PlatformAddress") - .IsRequired() - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Notification", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.HasOne("LiteCharms.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.cs deleted file mode 100644 index c39cf39..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class AddedNotifications : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Notification", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - Direction = table.Column(type: "integer", nullable: false), - Author = table.Column(type: "text", nullable: false), - Title = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), - Platform = table.Column(type: "text", nullable: false), - PlatformAddress = table.Column(type: "text", nullable: false), - CorrelationId = table.Column(type: "text", nullable: false), - CorrelationIdType = table.Column(type: "text", nullable: false), - IsInternal = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Notification", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Notification"); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.cs deleted file mode 100644 index 3439cab..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class AddedProcessedColumnToNotifications : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "IsInternal", - table: "Notification", - type: "boolean", - nullable: false, - defaultValue: true, - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AddColumn( - name: "Processed", - table: "Notification", - type: "boolean", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Processed", - table: "Notification"); - - migrationBuilder.AlterColumn( - name: "IsInternal", - table: "Notification", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldDefaultValue: true); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.cs deleted file mode 100644 index 8442535..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class AddedQuoteShoppingCartalteredOrderCustomer : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Order_ProductPrice_ProductPriceId", - table: "Order"); - - migrationBuilder.DropIndex( - name: "IX_Order_ProductPriceId", - table: "Order"); - - migrationBuilder.RenameColumn( - name: "ProductPriceId", - table: "Order", - newName: "ShoppingCartId"); - - migrationBuilder.AddColumn( - name: "QuoteId", - table: "Order", - type: "uuid", - nullable: true); - - migrationBuilder.CreateTable( - name: "ShoppingCart", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - CustomerId = table.Column(type: "uuid", nullable: true), - OrderId = table.Column(type: "uuid", nullable: true), - QuoteId = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ShoppingCart", x => x.Id); - table.ForeignKey( - name: "FK_ShoppingCart_Customer_CustomerId", - column: x => x.CustomerId, - principalTable: "Customer", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "Quote", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CustomerId1 = table.Column(type: "uuid", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true), - CustomerId = table.Column(type: "uuid", nullable: false), - ShoppingCartId = table.Column(type: "uuid", nullable: false), - Status = table.Column(type: "integer", nullable: false), - Reason = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Quote", x => x.Id); - table.ForeignKey( - name: "FK_Quote_Customer_CustomerId", - column: x => x.CustomerId, - principalTable: "Customer", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Quote_Customer_CustomerId1", - column: x => x.CustomerId1, - principalTable: "Customer", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_Quote_ShoppingCart_ShoppingCartId", - column: x => x.ShoppingCartId, - principalTable: "ShoppingCart", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "ShoppingCartItems", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ShoppingCartId = table.Column(type: "uuid", nullable: false), - ProductPriceId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - Quantity = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ShoppingCartItems", x => x.Id); - table.ForeignKey( - name: "FK_ShoppingCartItems_ProductPrice_ProductPriceId", - column: x => x.ProductPriceId, - principalTable: "ProductPrice", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ShoppingCartItems_ShoppingCart_ShoppingCartId", - column: x => x.ShoppingCartId, - principalTable: "ShoppingCart", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Order_QuoteId", - table: "Order", - column: "QuoteId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Order_ShoppingCartId", - table: "Order", - column: "ShoppingCartId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Quote_CustomerId", - table: "Quote", - column: "CustomerId"); - - migrationBuilder.CreateIndex( - name: "IX_Quote_CustomerId1", - table: "Quote", - column: "CustomerId1"); - - migrationBuilder.CreateIndex( - name: "IX_Quote_ShoppingCartId", - table: "Quote", - column: "ShoppingCartId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingCart_CustomerId", - table: "ShoppingCart", - column: "CustomerId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingCartItems_ProductPriceId", - table: "ShoppingCartItems", - column: "ProductPriceId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingCartItems_ShoppingCartId", - table: "ShoppingCartItems", - column: "ShoppingCartId"); - - migrationBuilder.AddForeignKey( - name: "FK_Order_Quote_QuoteId", - table: "Order", - column: "QuoteId", - principalTable: "Quote", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - - migrationBuilder.AddForeignKey( - name: "FK_Order_ShoppingCart_ShoppingCartId", - table: "Order", - column: "ShoppingCartId", - principalTable: "ShoppingCart", - principalColumn: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Order_Quote_QuoteId", - table: "Order"); - - migrationBuilder.DropForeignKey( - name: "FK_Order_ShoppingCart_ShoppingCartId", - table: "Order"); - - migrationBuilder.DropTable( - name: "Quote"); - - migrationBuilder.DropTable( - name: "ShoppingCartItems"); - - migrationBuilder.DropTable( - name: "ShoppingCart"); - - migrationBuilder.DropIndex( - name: "IX_Order_QuoteId", - table: "Order"); - - migrationBuilder.DropIndex( - name: "IX_Order_ShoppingCartId", - table: "Order"); - - migrationBuilder.DropColumn( - name: "QuoteId", - table: "Order"); - - migrationBuilder.RenameColumn( - name: "ShoppingCartId", - table: "Order", - newName: "ProductPriceId"); - - migrationBuilder.CreateIndex( - name: "IX_Order_ProductPriceId", - table: "Order", - column: "ProductPriceId"); - - migrationBuilder.AddForeignKey( - name: "FK_Order_ProductPrice_ProductPriceId", - table: "Order", - column: "ProductPriceId", - principalTable: "ProductPrice", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.Designer.cs similarity index 93% rename from LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.Designer.cs rename to LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.Designer.cs index 90c8e8f..6ae5382 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { [DbContext(typeof(ShopDbContext))] - [Migration("20260505202859_AddedQuoteShoppingCartalteredOrderCustomer")] - partial class AddedQuoteShoppingCartalteredOrderCustomer + [Migration("20260510090446_Init")] + partial class Init { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -167,10 +167,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("Author") - .IsRequired() - .HasColumnType("text"); - b.Property("CorrelationId") .IsRequired() .HasColumnType("text"); @@ -183,35 +179,57 @@ namespace LiteCharms.Infrastructure.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone"); - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - b.Property("Direction") .HasColumnType("integer"); + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("IsInternal") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(true); - b.Property("Platform") + b.Property("Message") .IsRequired() .HasColumnType("text"); - b.Property("PlatformAddress") - .IsRequired() - .HasColumnType("text"); + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); b.Property("Processed") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(false); - b.Property("Title") + b.Property("Recipient") .IsRequired() .HasColumnType("text"); + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("Sender") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() + .HasColumnType("timestamp with time zone"); + b.HasKey("Id"); b.ToTable("Notification", (string)null); @@ -230,6 +248,9 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CustomerId") .HasColumnType("uuid"); + b.Property("DepositRequired") + .HasColumnType("boolean"); + b.PrimitiveCollection("Notes") .HasColumnType("jsonb"); @@ -239,12 +260,18 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("RefundId") .HasColumnType("uuid"); + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); + b.Property("ShoppingCartId") .HasColumnType("uuid"); b.Property("Status") .HasColumnType("integer"); + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + b.Property("UpdatedAt") .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); @@ -378,8 +405,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("Status") .HasColumnType("integer"); - b.Property("UpdatedAt") - .ValueGeneratedOnAddOrUpdate() + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -413,8 +440,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("QuoteId") .HasColumnType("uuid"); - b.Property("UpdatedAt") - .ValueGeneratedOnAddOrUpdate() + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.cs new file mode 100644 index 0000000..3bc9554 --- /dev/null +++ b/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.cs @@ -0,0 +1,382 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Infrastructure.Database.Migrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Customer", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Company = table.Column(type: "text", nullable: true), + Name = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + Tax = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: false), + Discord = table.Column(type: "text", nullable: true), + Slack = table.Column(type: "text", nullable: true), + LinkedIn = table.Column(type: "text", nullable: true), + Whatsapp = table.Column(type: "text", nullable: true), + Website = table.Column(type: "text", nullable: true), + Phone = table.Column(type: "text", nullable: true), + Address = table.Column(type: "text", nullable: true), + City = table.Column(type: "text", nullable: true), + Region = table.Column(type: "text", nullable: true), + Country = table.Column(type: "text", nullable: true), + PostalCode = table.Column(type: "text", nullable: true), + Active = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Customer", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Notification", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Direction = table.Column(type: "integer", nullable: false), + Platform = table.Column(type: "integer", nullable: false), + Priority = table.Column(type: "integer", nullable: false), + Sender = table.Column(type: "text", nullable: false), + SenderName = table.Column(type: "text", nullable: true), + Subject = table.Column(type: "text", nullable: false), + Message = table.Column(type: "text", nullable: false), + Recipient = table.Column(type: "text", nullable: false), + RecipientAddress = table.Column(type: "text", nullable: false), + CorrelationId = table.Column(type: "text", nullable: false), + CorrelationIdType = table.Column(type: "text", nullable: false), + IsHtml = table.Column(type: "boolean", nullable: false, defaultValue: false), + IsInternal = table.Column(type: "boolean", nullable: false, defaultValue: true), + Processed = table.Column(type: "boolean", nullable: false, defaultValue: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Notification", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Product", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + Active = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Product", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Lead", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: true), + Source = table.Column(type: "text", nullable: true), + ClickId = table.Column(type: "text", nullable: true), + WebClickId = table.Column(type: "text", nullable: true), + AppClickId = table.Column(type: "text", nullable: true), + CampaignId = table.Column(type: "bigint", nullable: true), + AdGroupId = table.Column(type: "bigint", nullable: true), + AdName = table.Column(type: "bigint", nullable: true), + TargetId = table.Column(type: "bigint", nullable: true), + FeedItemId = table.Column(type: "bigint", nullable: true), + ClickLocation = table.Column(type: "text", nullable: true), + AttributionHash = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Lead", x => x.Id); + table.ForeignKey( + name: "FK_Lead_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCart", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: true), + OrderId = table.Column(type: "uuid", nullable: true), + QuoteId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCart", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCart_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ProductPrice", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + ProductId = table.Column(type: "uuid", nullable: false), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Discount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductPrice", x => x.Id); + table.ForeignKey( + name: "FK_ProductPrice_Product_ProductId", + column: x => x.ProductId, + principalTable: "Product", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Quote", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId1 = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: false), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Reason = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Quote", x => x.Id); + table.ForeignKey( + name: "FK_Quote_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Quote_Customer_CustomerId1", + column: x => x.CustomerId1, + principalTable: "Customer", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Quote_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCartItems", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + ProductPriceId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Quantity = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCartItems", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCartItems_ProductPrice_ProductPriceId", + column: x => x.ProductPriceId, + principalTable: "ProductPrice", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingCartItems_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Order", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: false), + QuoteId = table.Column(type: "uuid", nullable: true), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + RefundId = table.Column(type: "uuid", nullable: true), + Status = table.Column(type: "integer", nullable: false), + Requirements = table.Column(type: "jsonb", nullable: true), + Notes = table.Column(type: "jsonb", nullable: true), + Terms = table.Column(type: "jsonb", nullable: true), + DepositRequired = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Order", x => x.Id); + table.ForeignKey( + name: "FK_Order_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Order_Quote_QuoteId", + column: x => x.QuoteId, + principalTable: "Quote", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Order_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "OrderRefund", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + Reason = table.Column(type: "text", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderRefund", x => x.Id); + table.ForeignKey( + name: "FK_OrderRefund_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Lead_CustomerId", + table: "Lead", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Order_CustomerId", + table: "Order", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Order_QuoteId", + table: "Order", + column: "QuoteId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Order_ShoppingCartId", + table: "Order", + column: "ShoppingCartId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OrderRefund_OrderId", + table: "OrderRefund", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ProductPrice_ProductId", + table: "ProductPrice", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Quote_CustomerId", + table: "Quote", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Quote_CustomerId1", + table: "Quote", + column: "CustomerId1"); + + migrationBuilder.CreateIndex( + name: "IX_Quote_ShoppingCartId", + table: "Quote", + column: "ShoppingCartId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCart_CustomerId", + table: "ShoppingCart", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ProductPriceId", + table: "ShoppingCartItems", + column: "ProductPriceId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ShoppingCartId", + table: "ShoppingCartItems", + column: "ShoppingCartId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Lead"); + + migrationBuilder.DropTable( + name: "Notification"); + + migrationBuilder.DropTable( + name: "OrderRefund"); + + migrationBuilder.DropTable( + name: "ShoppingCartItems"); + + migrationBuilder.DropTable( + name: "Order"); + + migrationBuilder.DropTable( + name: "ProductPrice"); + + migrationBuilder.DropTable( + name: "Quote"); + + migrationBuilder.DropTable( + name: "Product"); + + migrationBuilder.DropTable( + name: "ShoppingCart"); + + migrationBuilder.DropTable( + name: "Customer"); + } + } +} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.Designer.cs similarity index 53% rename from LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.Designer.cs rename to LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.Designer.cs index 8ec4ca5..f83fabb 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { [DbContext(typeof(ShopDbContext))] - [Migration("20260505124135_AddedProcessedColumnToNotifications")] - partial class AddedProcessedColumnToNotifications + [Migration("20260510091540_AddedPackages")] + partial class AddedPackages { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -167,10 +167,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("Author") - .IsRequired() - .HasColumnType("text"); - b.Property("CorrelationId") .IsRequired() .HasColumnType("text"); @@ -183,35 +179,57 @@ namespace LiteCharms.Infrastructure.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone"); - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - b.Property("Direction") .HasColumnType("integer"); + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("IsInternal") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(true); - b.Property("Platform") + b.Property("Message") .IsRequired() .HasColumnType("text"); - b.Property("PlatformAddress") - .IsRequired() - .HasColumnType("text"); + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); b.Property("Processed") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(false); - b.Property("Title") + b.Property("Recipient") .IsRequired() .HasColumnType("text"); + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("Sender") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() + .HasColumnType("timestamp with time zone"); + b.HasKey("Id"); b.ToTable("Notification", (string)null); @@ -230,18 +248,30 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CustomerId") .HasColumnType("uuid"); + b.Property("DepositRequired") + .HasColumnType("boolean"); + b.PrimitiveCollection("Notes") .HasColumnType("jsonb"); - b.Property("ProductPriceId") + b.Property("QuoteId") .HasColumnType("uuid"); b.Property("RefundId") .HasColumnType("uuid"); + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + b.Property("Status") .HasColumnType("integer"); + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + b.Property("UpdatedAt") .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); @@ -250,7 +280,11 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.HasIndex("CustomerId"); - b.HasIndex("ProductPriceId"); + b.HasIndex("QuoteId") + .IsUnique(); + + b.HasIndex("ShoppingCartId") + .IsUnique(); b.ToTable("Order", (string)null); }); @@ -284,6 +318,67 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.ToTable("OrderRefund", (string)null); }); + modelBuilder.Entity("LiteCharms.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("PackageId1") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("PackageId1"); + + b.ToTable("PackageItem", (string)null); + }); + modelBuilder.Entity("LiteCharms.Entities.Product", b => { b.Property("Id") @@ -343,6 +438,135 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.ToTable("ProductPrice", (string)null); }); + modelBuilder.Entity("LiteCharms.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("CustomerId1") + .HasColumnType("uuid"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("CustomerId1"); + + b.HasIndex("ShoppingCartId") + .IsUnique(); + + b.ToTable("Quote", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("QuoteId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("ShoppingCart", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductPriceId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartItems"); + }); + + modelBuilder.Entity("LiteCharms.Entities.ShoppingCartPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartPackage", (string)null); + }); + modelBuilder.Entity("LiteCharms.Entities.Lead", b => { b.HasOne("LiteCharms.Entities.Customer", "Customer") @@ -361,15 +585,22 @@ namespace LiteCharms.Infrastructure.Database.Migrations .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) + b.HasOne("LiteCharms.Entities.Quote", "Quote") + .WithOne("Order") + .HasForeignKey("LiteCharms.Entities.Order", "QuoteId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") + .WithOne("Order") + .HasForeignKey("LiteCharms.Entities.Order", "ShoppingCartId") + .OnDelete(DeleteBehavior.NoAction) .IsRequired(); b.Navigation("Customer"); - b.Navigation("ProductPrice"); + b.Navigation("Quote"); + + b.Navigation("ShoppingCart"); }); modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => @@ -383,6 +614,21 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => + { + b.HasOne("LiteCharms.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.Package", null) + .WithMany("PackageItems") + .HasForeignKey("PackageId1"); + + b.Navigation("Package"); + }); + modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => { b.HasOne("LiteCharms.Entities.Product", "Product") @@ -394,11 +640,86 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Navigation("Product"); }); + modelBuilder.Entity("LiteCharms.Entities.Quote", b => + { + b.HasOne("LiteCharms.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.Customer", null) + .WithMany("Quotes") + .HasForeignKey("CustomerId1"); + + b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Entities.Quote", "ShoppingCartId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => + { + b.HasOne("LiteCharms.Entities.Customer", "Customer") + .WithMany("ShoppingCarts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => + { + b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartItems") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductPrice"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Entities.ShoppingCartPackage", b => + { + b.HasOne("LiteCharms.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") + .WithMany() + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ShoppingCart"); + }); + modelBuilder.Entity("LiteCharms.Entities.Customer", b => { b.Navigation("Leads"); b.Navigation("Orders"); + + b.Navigation("Quotes"); + + b.Navigation("ShoppingCarts"); }); modelBuilder.Entity("LiteCharms.Entities.Order", b => @@ -406,10 +727,29 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Navigation("Refund"); }); + modelBuilder.Entity("LiteCharms.Entities.Package", b => + { + b.Navigation("PackageItems"); + }); + modelBuilder.Entity("LiteCharms.Entities.Product", b => { b.Navigation("ProductPrices"); }); + + modelBuilder.Entity("LiteCharms.Entities.Quote", b => + { + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => + { + b.Navigation("Order"); + + b.Navigation("Quote"); + + b.Navigation("ShoppingCartItems"); + }); #pragma warning restore 612, 618 } } diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs new file mode 100644 index 0000000..351ac6b --- /dev/null +++ b/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs @@ -0,0 +1,114 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Infrastructure.Database.Migrations +{ + /// + public partial class AddedPackages : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Package", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Package", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PackageItem", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PackageId1 = table.Column(type: "uuid", nullable: true), + PackageId = table.Column(type: "uuid", nullable: false), + ProductPriceId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PackageItem", x => x.Id); + table.ForeignKey( + name: "FK_PackageItem_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_PackageItem_Package_PackageId1", + column: x => x.PackageId1, + principalTable: "Package", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCartPackage", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + PackageId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCartPackage", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCartPackage_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_ShoppingCartPackage_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_PackageItem_PackageId", + table: "PackageItem", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_PackageItem_PackageId1", + table: "PackageItem", + column: "PackageId1"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_PackageId", + table: "ShoppingCartPackage", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_ShoppingCartId", + table: "ShoppingCartPackage", + column: "ShoppingCartId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PackageItem"); + + migrationBuilder.DropTable( + name: "ShoppingCartPackage"); + + migrationBuilder.DropTable( + name: "Package"); + } + } +} diff --git a/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs b/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs similarity index 77% rename from LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs rename to LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs index 09719f3..8a32836 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs @@ -8,10 +8,10 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace LiteCharms.Infrastructure.Migrations +namespace LiteCharms.Infrastructure.Database.Migrations { [DbContext(typeof(ShopDbContext))] - partial class LeadGeneratorDbContextModelSnapshot : ModelSnapshot + partial class ShopDbContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { @@ -164,10 +164,6 @@ namespace LiteCharms.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("Author") - .IsRequired() - .HasColumnType("text"); - b.Property("CorrelationId") .IsRequired() .HasColumnType("text"); @@ -180,35 +176,57 @@ namespace LiteCharms.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone"); - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - b.Property("Direction") .HasColumnType("integer"); + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("IsInternal") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(true); - b.Property("Platform") + b.Property("Message") .IsRequired() .HasColumnType("text"); - b.Property("PlatformAddress") - .IsRequired() - .HasColumnType("text"); + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); b.Property("Processed") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(false); - b.Property("Title") + b.Property("Recipient") .IsRequired() .HasColumnType("text"); + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("Sender") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() + .HasColumnType("timestamp with time zone"); + b.HasKey("Id"); b.ToTable("Notification", (string)null); @@ -227,6 +245,9 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("CustomerId") .HasColumnType("uuid"); + b.Property("DepositRequired") + .HasColumnType("boolean"); + b.PrimitiveCollection("Notes") .HasColumnType("jsonb"); @@ -236,12 +257,18 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("RefundId") .HasColumnType("uuid"); + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); + b.Property("ShoppingCartId") .HasColumnType("uuid"); b.Property("Status") .HasColumnType("integer"); + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + b.Property("UpdatedAt") .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); @@ -288,6 +315,67 @@ namespace LiteCharms.Infrastructure.Migrations b.ToTable("OrderRefund", (string)null); }); + modelBuilder.Entity("LiteCharms.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("PackageId1") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("PackageId1"); + + b.ToTable("PackageItem", (string)null); + }); + modelBuilder.Entity("LiteCharms.Entities.Product", b => { b.Property("Id") @@ -375,8 +463,8 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("Status") .HasColumnType("integer"); - b.Property("UpdatedAt") - .ValueGeneratedOnAddOrUpdate() + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -410,8 +498,8 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("QuoteId") .HasColumnType("uuid"); - b.Property("UpdatedAt") - .ValueGeneratedOnAddOrUpdate() + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -451,6 +539,31 @@ namespace LiteCharms.Infrastructure.Migrations b.ToTable("ShoppingCartItems"); }); + modelBuilder.Entity("LiteCharms.Entities.ShoppingCartPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartPackage", (string)null); + }); + modelBuilder.Entity("LiteCharms.Entities.Lead", b => { b.HasOne("LiteCharms.Entities.Customer", "Customer") @@ -498,6 +611,21 @@ namespace LiteCharms.Infrastructure.Migrations b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => + { + b.HasOne("LiteCharms.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.Package", null) + .WithMany("PackageItems") + .HasForeignKey("PackageId1"); + + b.Navigation("Package"); + }); + modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => { b.HasOne("LiteCharms.Entities.Product", "Product") @@ -561,6 +689,25 @@ namespace LiteCharms.Infrastructure.Migrations b.Navigation("ShoppingCart"); }); + modelBuilder.Entity("LiteCharms.Entities.ShoppingCartPackage", b => + { + b.HasOne("LiteCharms.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") + .WithMany() + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ShoppingCart"); + }); + modelBuilder.Entity("LiteCharms.Entities.Customer", b => { b.Navigation("Leads"); @@ -577,6 +724,11 @@ namespace LiteCharms.Infrastructure.Migrations b.Navigation("Refund"); }); + modelBuilder.Entity("LiteCharms.Entities.Package", b => + { + b.Navigation("PackageItems"); + }); + modelBuilder.Entity("LiteCharms.Entities.Product", b => { b.Navigation("ProductPrices"); diff --git a/LiteCharms.Infrastructure/Database/ShopDbContext.cs b/LiteCharms.Infrastructure/Database/ShopDbContext.cs index 91de689..c3d0679 100644 --- a/LiteCharms.Infrastructure/Database/ShopDbContext.cs +++ b/LiteCharms.Infrastructure/Database/ShopDbContext.cs @@ -23,4 +23,10 @@ public class ShopDbContext(DbContextOptions options) : DbContext( public DbSet ShoppingCarts { get; set; } public DbSet ShoppingCartItems { get; set; } + + public DbSet Packages { get; set; } + + public DbSet PackageItems { get; set; } + + public DbSet ShoppingCartPackages { get; set; } } diff --git a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj b/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj index 9647c3a..765140f 100644 --- a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj +++ b/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj @@ -100,5 +100,9 @@ PreserveNewest + + + + diff --git a/LiteCharms.Models/Enums.cs b/LiteCharms.Models/Enums.cs index 96bda99..385a5b5 100644 --- a/LiteCharms.Models/Enums.cs +++ b/LiteCharms.Models/Enums.cs @@ -1,5 +1,21 @@ namespace LiteCharms.Models; +public enum Priorities : int +{ + Low = 0, + Medium = 1, + High = 2, +} + +public enum NotificationPlatforms : int +{ + Email = 1, + Discord = 2, + Slack = 3, + WhatsApp = 4, + System = 5 +} + public enum QuoteStatus : int { Draft = 0, diff --git a/LiteCharms.Models/Notification.cs b/LiteCharms.Models/Notification.cs index 88c4896..90ac390 100644 --- a/LiteCharms.Models/Notification.cs +++ b/LiteCharms.Models/Notification.cs @@ -6,22 +6,32 @@ public class Notification public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset? UpdatedAt { get; set; } + public NotificationDirection Direction { get; set; } - public string? Author { get; set; } + public NotificationPlatforms Platform { get; set; } - public string? Title { get; set; } + public Priorities Priority { get; set; } - public string? Description { get; set; } + public string? Sender { get; set; } - public string? Platform { get; set; } + public string? SenderName { get; set; } - public string? PlatformAddress { get; set; } + public string? Subject { get; set; } + + public string? Message { get; set; } + + public string? Recipient { get; set; } + + public string? RecipientAddress { get; set; } public string? CorrelationId { get; set; } public string? CorrelationIdType { get; set; } + public bool IsHtml { get; set; } + public bool IsInternal { get; set; } public bool Processed { get; set; } diff --git a/LiteCharms.Models/Order.cs b/LiteCharms.Models/Order.cs index 70ec1f3..3c55c81 100644 --- a/LiteCharms.Models/Order.cs +++ b/LiteCharms.Models/Order.cs @@ -12,11 +12,17 @@ public class Order public Guid? QuoteId { get; set; } - public Guid ShoppingCartId { get; set; } + public Guid ShoppingCartId { get; set; } public Guid? RefundId { get; set; } public OrderStatus Status { get; set; } + public string[]? Requirements { get; set; } + public string[]? Notes { get; set; } + + public string[]? Terms { get; set; } + + public bool DepositRequired { get; set; } } diff --git a/LiteCharms.Models/Package.cs b/LiteCharms.Models/Package.cs new file mode 100644 index 0000000..cfc0feb --- /dev/null +++ b/LiteCharms.Models/Package.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Models; + +public class Package +{ + public Guid Id { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public DateTimeOffset? UpdatedAt { get; set; } + + public string? Name { get; set; } + + public string? Description { get; set; } + + public bool Active { get; set; } +} diff --git a/LiteCharms.Models/PackageItem.cs b/LiteCharms.Models/PackageItem.cs new file mode 100644 index 0000000..2c27ad3 --- /dev/null +++ b/LiteCharms.Models/PackageItem.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Models; + +public class PackageItem +{ + public Guid Id { get; set; } + + public Guid PackageId { get; set; } + + public Guid ProductPriceId { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public bool Active { get; set; } +} diff --git a/LiteCharms.Models/ShoppingCartPackage.cs b/LiteCharms.Models/ShoppingCartPackage.cs new file mode 100644 index 0000000..673b577 --- /dev/null +++ b/LiteCharms.Models/ShoppingCartPackage.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Models; + +public class ShoppingCartPackage +{ + public Guid Id { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public Guid ShoppingCartId { get; set; } + + public Guid PackageId { get; set; } +} diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index f43a12e..05f4368 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -1,13 +1,6 @@ - - - - - - - From 394429677e70bd5973d29c759b138c0b373987b4 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 14:18:56 +0200 Subject: [PATCH 017/153] Added package management --- LiteCharms.Entities/ShoppingCart.cs | 2 + LiteCharms.Extensions/EntityModeMappers.cs | 46 +++++++++++++-- .../Commands/AddPackageItemsCommand.cs | 25 ++++++++ .../Commands/CreatePackageCommand.cs | 23 ++++++++ .../Commands/DeletePackageCommand.cs | 25 ++++++++ .../Commands/DeletePackageItemsCommand.cs | 16 ++++++ .../Handlers/AddPackageItemCommandHandler.cs | 38 +++++++++++++ .../Handlers/CreatePackageCommandHandler.cs | 32 +++++++++++ .../DeletePackageItemCommandHandler.cs | 32 +++++++++++ .../DeletePackageItemsCommandHandler.cs | 29 ++++++++++ .../Handlers/UpdatePackageCommandHandler.cs | 33 +++++++++++ .../UpdatePackageStatusCommandHandler.cs | 29 ++++++++++ .../Commands/UpdatePackageCommand.cs | 28 +++++++++ .../Commands/UpdatePackageStatusCommand.cs | 22 +++++++ .../Queries/GetPackageItemsQuery.cs | 18 ++++++ .../CartPackages/Queries/GetPackageQuery.cs | 18 ++++++ .../CartPackages/Queries/GetPackagesQuery.cs | 33 +++++++++++ .../Handlers/GetPackageItemsQueryHandler.cs | 32 +++++++++++ .../Handlers/GetPackageQueryHandler.cs | 27 +++++++++ .../Handlers/GetPackagesQueryHandler.cs | 35 ++++++++++++ .../Commands/CreateNotificationCommand.cs | 57 ++++++++++++------- .../CreateNotificationCommandHandler.cs | 25 ++++---- .../Queries/Handlers/GetOrdersQueryHandler.cs | 1 + .../AddPackageToShoppingCartCommand.cs | 25 ++++++++ .../AddPackageToShoppingCartCommandHandler.cs | 39 +++++++++++++ ...vePackageFromShoppingCartCommandHandler.cs | 35 ++++++++++++ .../RemovePackageFromShoppingCartCommand.cs | 25 ++++++++ .../Queries/GetShoppingCartPackagesQuery.cs | 18 ++++++ .../GetShoppingCartPackagesQueryHandler.cs | 32 +++++++++++ 29 files changed, 765 insertions(+), 35 deletions(-) create mode 100644 LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/CreatePackageCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/DeletePackageCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/DeletePackageItemsCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/UpdatePackageCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Commands/UpdatePackageStatusCommand.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs create mode 100644 LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs create mode 100644 LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs diff --git a/LiteCharms.Entities/ShoppingCart.cs b/LiteCharms.Entities/ShoppingCart.cs index 3c974dc..ead1175 100644 --- a/LiteCharms.Entities/ShoppingCart.cs +++ b/LiteCharms.Entities/ShoppingCart.cs @@ -12,4 +12,6 @@ public class ShoppingCart : Models.ShoppingCart public virtual Quote? Quote { get; set; } public virtual ICollection? ShoppingCartItems { get; set; } + + public virtual ICollection? Packages { get; set; } } diff --git a/LiteCharms.Extensions/EntityModeMappers.cs b/LiteCharms.Extensions/EntityModeMappers.cs index b3b8e64..37ad38e 100644 --- a/LiteCharms.Extensions/EntityModeMappers.cs +++ b/LiteCharms.Extensions/EntityModeMappers.cs @@ -4,6 +4,36 @@ namespace LiteCharms.Extensions; public static class EntityModeMappers { + public static ShoppingCartPackage ToModel(this Entities.ShoppingCartPackage entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + PackageId = entity.PackageId, + ShoppingCartId = entity.ShoppingCartId + }; + + public static PackageItem ToModel(this Entities.PackageItem entity) => + new() + { + Id = entity.Id, + Active = entity.Active, + CreatedAt = entity.CreatedAt, + PackageId = entity.PackageId, + ProductPriceId = entity.ProductPriceId + }; + + public static Package ToModel(this Entities.Package entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + Active = entity.Active, + Description = entity.Description, + Name = entity.Name, + UpdatedAt = entity.UpdatedAt + }; + public static ShoppingCartItem ToModel(this Entities.ShoppingCartItem entity) => new() { @@ -36,7 +66,7 @@ public static class EntityModeMappers ExpiredAt = entity.ExpiredAt, Reason = entity.Reason, ShoppingCartId = entity.ShoppingCartId, - Status = entity.Status + Status = entity.Status }; public static Notification ToModel(this Entities.Notification entity) => @@ -53,7 +83,12 @@ public static class EntityModeMappers Platform = entity.Platform, Recipient = entity.Recipient, Subject = entity.Subject, - Processed = entity.Processed + Processed = entity.Processed, + SenderName = entity.SenderName, + RecipientAddress = entity.RecipientAddress, + Priority = entity.Priority, + UpdatedAt = entity?.UpdatedAt, + IsHtml = entity!.IsHtml }; public static Customer ToModel(this Entities.Customer entity) => @@ -78,7 +113,7 @@ public static class EntityModeMappers Slack = entity.Slack, Tax = entity.Tax, Website = entity.Website, - Whatsapp = entity.Whatsapp + Whatsapp = entity.Whatsapp }; public static Lead ToModel(this Entities.Lead entity) => @@ -113,7 +148,10 @@ public static class EntityModeMappers RefundId = entity.RefundId, QuoteId = entity.QuoteId, Status = entity.Status, - ShoppingCartId = entity.ShoppingCartId + ShoppingCartId = entity.ShoppingCartId, + DepositRequired = entity.DepositRequired, + Requirements = entity.Requirements, + Terms = entity.Terms }; public static OrderRefund ToModel(this Entities.OrderRefund entity) => diff --git a/LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs b/LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs new file mode 100644 index 0000000..be87a47 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class AddPackageItemCommand : IRequest> +{ + public Guid PackageId { get; set; } + + public Guid ProductPriceId { get; set; } + + private AddPackageItemCommand(Guid packageId, Guid productPriceId) + { + PackageId = packageId; + ProductPriceId = productPriceId; + } + + public static AddPackageItemCommand Create(Guid packageId, Guid productPriceId) + { + if (packageId == Guid.Empty) + throw new ArgumentException("Package id is required", nameof(packageId)); + + if (productPriceId == Guid.Empty) + throw new ArgumentException("Product price id is required", nameof(productPriceId)); + + return new(packageId, productPriceId); + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/CreatePackageCommand.cs b/LiteCharms.Features/CartPackages/Commands/CreatePackageCommand.cs new file mode 100644 index 0000000..4a86846 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/CreatePackageCommand.cs @@ -0,0 +1,23 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class CreatePackageCommand : IRequest> +{ + public string? Name { get; set; } + + public string? Description { get; set; } + + private CreatePackageCommand(string? name, string? description) + { + Name = name; + Description = description; + } + + public static CreatePackageCommand Create(string? name, string? description) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name)); + + ArgumentException.ThrowIfNullOrWhiteSpace(description, nameof(description)); + + return new(name, description); + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/DeletePackageCommand.cs b/LiteCharms.Features/CartPackages/Commands/DeletePackageCommand.cs new file mode 100644 index 0000000..5957dce --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/DeletePackageCommand.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class DeletePackageItemCommand : IRequest +{ + public Guid PackageId { get; set; } + + public Guid PackageItemId { get; set; } + + private DeletePackageItemCommand(Guid packageId, Guid packageItemId) + { + PackageId = packageId; + PackageItemId = packageItemId; + } + + public static DeletePackageItemCommand Create(Guid packageId, Guid packageItemId) + { + if (packageId == Guid.Empty) + throw new ArgumentException("Package id is required", nameof(packageId)); + + if (packageItemId == Guid.Empty) + throw new ArgumentException("Product price id is required", nameof(packageItemId)); + + return new(packageId, packageItemId); + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/DeletePackageItemsCommand.cs b/LiteCharms.Features/CartPackages/Commands/DeletePackageItemsCommand.cs new file mode 100644 index 0000000..c9aa3e0 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/DeletePackageItemsCommand.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class DeletePackageItemsCommand : IRequest +{ + public Guid PackageId { get; set; } + + private DeletePackageItemsCommand(Guid packageId) => PackageId = packageId; + + public static DeletePackageItemsCommand Create(Guid packageId) + { + if (packageId == Guid.Empty) + throw new ArgumentException("Package ID is required", nameof(packageId)); + + return new(packageId); + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs new file mode 100644 index 0000000..ffdd24a --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs @@ -0,0 +1,38 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class AddPackageItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(AddPackageItemCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {request.PackageId}"); + + if (!await context.ProductPrices.AnyAsync(p => p.Id == request.ProductPriceId && p.Active == true, cancellationToken)) + return Result.Fail($"Could not find an active product price by ID {request.ProductPriceId}"); + + if (await context.PackageItems.AnyAsync(p => p.ProductPriceId == request.ProductPriceId && p.PackageId == request.PackageId, cancellationToken)) + return Result.Fail($"Product price {request.ProductPriceId} is already added to this package {request.PackageId}"); + + var newPackageItem = context.PackageItems.Add(new Entities.PackageItem + { + PackageId = request.PackageId, + ProductPriceId = request.ProductPriceId, + Active = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newPackageItem.Entity.Id) + : Result.Fail($"Failed to add new package item by ID {request.ProductPriceId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs new file mode 100644 index 0000000..4945a73 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs @@ -0,0 +1,32 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class CreatePackageCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(CreatePackageCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Packages.AnyAsync(p => p.Name == request.Name, cancellationToken)) + return Result.Fail($"A package by the same name already exists: {request.Name}"); + + var newPackage = context.Packages.Add(new Entities.Package + { + Name = request.Name, + Description = request.Description, + Active = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newPackage.Entity.Id) + : Result.Fail($"Failed to create a new package by the name: {request.Name}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs new file mode 100644 index 0000000..5ec6745 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs @@ -0,0 +1,32 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class DeletePackageItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(DeletePackageItemCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {request.PackageId}"); + + var item = await context.PackageItems.FirstOrDefaultAsync(p => p.Id == request.PackageItemId && p.PackageId == request.PackageId, cancellationToken); + + if(item is null) + return Result.Fail($"Product item {request.PackageItemId} is already added to this package {request.PackageId}"); + + context.PackageItems.Remove(item); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to delete package item by id {request.PackageItemId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs new file mode 100644 index 0000000..8f4e8e7 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs @@ -0,0 +1,29 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class DeletePackageItemsCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(DeletePackageItemsCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {request.PackageId}"); + + var items = await context.PackageItems.Where(i => i.PackageId == request.PackageId).ToArrayAsync(cancellationToken); + + context.PackageItems.RemoveRange(items); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to delete package {request.PackageId} items"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs new file mode 100644 index 0000000..7889c52 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs @@ -0,0 +1,33 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class UpdatePackageCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(UpdatePackageCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Packages.AnyAsync(p => p.Name == request.Name, cancellationToken)) + return Result.Fail($"A package by the same name already exists: {request.Name}"); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken); + + if (package is null) + return Result.Fail($"Could not find package by id {request.PackageId}"); + + package.Name = request.Name; + package.Description = request.Description; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update package with id {request.PackageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs b/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs new file mode 100644 index 0000000..5ef4a91 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs @@ -0,0 +1,29 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.CartPackages.Commands.Handlers; + +public class UpdatePackageStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(UpdatePackageStatusCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken); + + if (package is null) + return Result.Fail($"Could not find package by id {request.PackageId}"); + + package.Active = request.Active; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update package with id {request.PackageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/UpdatePackageCommand.cs b/LiteCharms.Features/CartPackages/Commands/UpdatePackageCommand.cs new file mode 100644 index 0000000..938ca44 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/UpdatePackageCommand.cs @@ -0,0 +1,28 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class UpdatePackageCommand : IRequest +{ + public Guid PackageId { get; set; } + + public string? Name { get; set; } + + public string? Description { get; set; } + + private UpdatePackageCommand(Guid packageId, string? name, string? description) + { + PackageId = packageId; + Name = name; + Description = description; + } + + public static UpdatePackageCommand Create(Guid packageId, string? name, string? description) + { + if (packageId == Guid.Empty) + throw new ArgumentException($"Package ID is required", nameof(packageId)); + + ArgumentNullException.ThrowIfNullOrWhiteSpace(name, nameof(name)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(description, nameof(description)); + + return new(packageId, name, description); + } +} diff --git a/LiteCharms.Features/CartPackages/Commands/UpdatePackageStatusCommand.cs b/LiteCharms.Features/CartPackages/Commands/UpdatePackageStatusCommand.cs new file mode 100644 index 0000000..7be4651 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Commands/UpdatePackageStatusCommand.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.CartPackages.Commands; + +public class UpdatePackageStatusCommand : IRequest +{ + public Guid PackageId { get; set; } + + public bool Active { get; set; } + + private UpdatePackageStatusCommand(Guid packageId, bool active) + { + PackageId = packageId; + Active = active; + } + + public static UpdatePackageStatusCommand Create(Guid packageId, bool active) + { + if(packageId == Guid.Empty) + throw new ArgumentException($"Package id is required", nameof(packageId)); + + return new(packageId, active); + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs b/LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs new file mode 100644 index 0000000..e0830af --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs @@ -0,0 +1,18 @@ +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries; + +public class GetPackageItemsQuery : IRequest> +{ + public Guid PackageId { get; set; } + + private GetPackageItemsQuery(Guid packageId) => PackageId = packageId; + + public static GetPackageItemsQuery Create(Guid packageId) + { + if (packageId == Guid.Empty) + throw new ArgumentException("Package ID is required", nameof(packageId)); + + return new(packageId); + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs b/LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs new file mode 100644 index 0000000..1384783 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs @@ -0,0 +1,18 @@ +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries; + +public class GetPackageQuery : IRequest> +{ + public Guid PackageId { get; set; } + + private GetPackageQuery(Guid packageId) => PackageId = packageId; + + public static GetPackageQuery Create(Guid packageId) + { + if(packageId == Guid.Empty) + throw new ArgumentException("Package ID is required", nameof(packageId)); + + return new(packageId); + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs b/LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs new file mode 100644 index 0000000..351a141 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs @@ -0,0 +1,33 @@ +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries; + +public class GetPackagesQuery : IRequest> +{ + public DateOnly From { get; set; } + + public DateOnly To { get; set; } + + public int MaxRecords { get; set; } + + public bool Active { get; set; } + + private GetPackagesQuery(DateOnly from, DateOnly to, int maxRecords = 1000, bool active = true) + { + From = from; + To = to; + MaxRecords = maxRecords; + Active = active; + } + + public static GetPackagesQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000, bool active = true) + { + if (from > to) + throw new ArgumentException("From date cannot be greater than To date."); + + if (maxRecords <= 0) + throw new ArgumentException("MaxRecords must be a positive integer."); + + return new(from, to, maxRecords, active); + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs new file mode 100644 index 0000000..487b203 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs @@ -0,0 +1,32 @@ +using LiteCharms.Extensions; +using LiteCharms.Infrastructure.Database; +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries.Handlers; + +public class GetPackageItemsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(GetPackageItemsQuery request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) + return Result.Fail($"Package could not be found with ID {request.PackageId}"); + + var items = await context.PackageItems.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(p => p.PackageId == request.PackageId) + .ToArrayAsync(cancellationToken); + + return items?.Length > 0 + ? Result.Ok(items.Select(i => i.ToModel()).ToArray()) + : Result.Fail($"Could not find package items by package ID {request.PackageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs new file mode 100644 index 0000000..7a01fb5 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs @@ -0,0 +1,27 @@ +using LiteCharms.Extensions; +using LiteCharms.Infrastructure.Database; +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries.Handlers; + +public class GetPackageQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(GetPackageQuery request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken); + + return package is not null + ? Result.Ok(package.ToModel()) + : Result.Fail($"Failed to find package by ID {request.PackageId}"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs new file mode 100644 index 0000000..b495bb0 --- /dev/null +++ b/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs @@ -0,0 +1,35 @@ +using LiteCharms.Extensions; +using LiteCharms.Infrastructure.Database; +using LiteCharms.Models; + +namespace LiteCharms.Features.CartPackages.Queries.Handlers; + +public class GetPackagesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(GetPackagesQuery request, CancellationToken cancellationToken) + { + try + { + var fromDate = request.From.ToDateTime(TimeOnly.MinValue); + var toDate = request.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var packages = await context.Packages + .AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) + .Where(p => p.Active == request.Active) + .Take(request.MaxRecords) + .ToArrayAsync(cancellationToken); + + return packages?.Length > 0 + ? Result.Ok(packages.Select(o => o.ToModel()).ToArray()) + : Result.Fail(new Error($"No packages found for the specified date range {request.From} - {request.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs index be81438..a9cf063 100644 --- a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs +++ b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs @@ -6,15 +6,21 @@ public class CreateNotificationCommand : IRequest> { public NotificationDirection Direction { get; set; } - public string? Author { get; set; } + public string? Sender { get; set; } - public string? Title { get; set; } + public string? SenderAddress { get; set; } - public string? Description { get; set; } + public string? Subject { get; set; } + + public string? Message { get; set; } public NotificationPlatforms Platform { get; set; } - public string? PlatformAddress { get; set; } + public Priorities Priority { get; set; } + + public string? Recipient { get; set; } + + public string? RecipientAddress { get; set; } public string? CorrelationId { get; set; } @@ -22,39 +28,48 @@ public class CreateNotificationCommand : IRequest> public bool IsInternal { get; set; } - private CreateNotificationCommand(NotificationDirection direction, string author, string title, string description, NotificationPlatforms platform, string platformAddress, string correlationId, string correlationIdType, bool isInternal) + public bool IsHtml { get; set; } + + private CreateNotificationCommand(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, string correlationIdType, bool isInternal, bool isHtml = false) { Direction = direction; - Author = author; - Title = title; - Description = description; + Sender = sender; + SenderAddress = senderAddress; + Subject = subject; + Message = message; Platform = platform; - PlatformAddress = platformAddress; + Priority = priority; + Recipient = recipient; + RecipientAddress = recipientAddress; CorrelationId = correlationId; CorrelationIdType = correlationIdType; IsInternal = isInternal; + IsHtml = isHtml; } - public static CreateNotificationCommand Create(NotificationDirection direction, string author, string title, string description, NotificationPlatforms platform, string platformAddress, string correlationId, string correlationIdType, bool isInternal) + public static CreateNotificationCommand Create(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, string correlationIdType, bool isInternal, bool isHtml = false) { - if (string.IsNullOrWhiteSpace(author)) - throw new ArgumentException("Author cannot be null or whitespace.", nameof(author)); + if (string.IsNullOrWhiteSpace(sender)) + throw new ArgumentException("Sender name is required.", nameof(sender)); - if (string.IsNullOrWhiteSpace(title)) - throw new ArgumentException("Title cannot be null or whitespace.", nameof(title)); + if (string.IsNullOrWhiteSpace(subject)) + throw new ArgumentException("Subject is required.", nameof(subject)); - if (string.IsNullOrWhiteSpace(description)) - throw new ArgumentException("Description cannot be null or whitespace.", nameof(description)); + if (string.IsNullOrWhiteSpace(message)) + throw new ArgumentException("Message is required.", nameof(message)); - if (string.IsNullOrWhiteSpace(platformAddress)) - throw new ArgumentException("PlatformAddress cannot be null or whitespace.", nameof(platformAddress)); + if (string.IsNullOrWhiteSpace(recipient)) + throw new ArgumentException("Recipient name is required.", nameof(recipient)); + + if (string.IsNullOrWhiteSpace(recipientAddress)) + throw new ArgumentException("Recipient address is required.", nameof(recipientAddress)); if (string.IsNullOrWhiteSpace(correlationId)) - throw new ArgumentException("CorrelationId cannot be null or whitespace.", nameof(correlationId)); + throw new ArgumentException("CorrelationId is required.", nameof(correlationId)); if (string.IsNullOrWhiteSpace(correlationIdType)) - throw new ArgumentException("CorrelationIdType cannot be null or whitespace.", nameof(correlationIdType)); + throw new ArgumentException("CorrelationIdType is required.", nameof(correlationIdType)); - return new(direction, author, title, description, platform, platformAddress, correlationId, correlationIdType, isInternal); + return new(direction, sender, senderAddress, subject, message, platform, priority, recipient, recipientAddress, correlationId, correlationIdType, isInternal, isHtml); } } diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs b/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs index d33fad6..e7a3fb4 100644 --- a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs +++ b/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs @@ -6,29 +6,34 @@ public class CreateNotificationCommandHandler(IDbContextFactory c { public async ValueTask> Handle(CreateNotificationCommand request, CancellationToken cancellationToken) { - try - { + try + { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var newNotification = context.Notifications.Add(new Entities.Notification { Direction = request.Direction, - Sender = request.Author, - Subject = request.Title, - Message = request.Description, + SenderName = request.Sender, + Sender = request.SenderAddress, + Recipient = request.Recipient, + RecipientAddress = request.RecipientAddress, + Subject = request.Subject, + Message = request.Message, Platform = request.Platform, - Recipient = request.PlatformAddress, + Priority = request.Priority, CorrelationId = request.CorrelationId, CorrelationIdType = request.CorrelationIdType, IsInternal = request.IsInternal, + IsHtml = request.IsHtml, + Processed = false }); - return newNotification is not null - ? Result.Ok(newNotification.Entity.Id) + return newNotification is not null + ? Result.Ok(newNotification.Entity.Id) : Result.Fail(new Error("Failed to create notification")); } - catch (Exception ex) - { + catch (Exception ex) + { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs b/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs index 8276bb4..f244869 100644 --- a/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs +++ b/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs @@ -16,6 +16,7 @@ public class GetOrdersQueryHandler(IDbContextFactory contextFacto using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var orders = await context.Orders + .AsNoTracking() .OrderByDescending(o => o.CreatedAt) .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) .Take(request.MaxRecords) diff --git a/LiteCharms.Features/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs b/LiteCharms.Features/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs new file mode 100644 index 0000000..81e8bac --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Features.ShoppingCarts.Commands; + +public class AddPackageToShoppingCartCommand : IRequest +{ + public Guid ShoppingCartId { get; set; } + + public Guid PackageId { get; set; } + + private AddPackageToShoppingCartCommand(Guid shoppingCartId, Guid packageId) + { + ShoppingCartId = shoppingCartId; + PackageId = packageId; + } + + public static AddPackageToShoppingCartCommand Create(Guid shoppingCartId, Guid packageId) + { + if (shoppingCartId == Guid.Empty) + throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); + + if (packageId == Guid.Empty) + throw new ArgumentException($"Package ID is required", nameof(packageId)); + + return new(shoppingCartId, packageId); + } +} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs new file mode 100644 index 0000000..67e7949 --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs @@ -0,0 +1,39 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; + +public class AddPackageToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(AddPackageToShoppingCartCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) + return Result.Fail($"Package cold not be found by ID {request.PackageId}"); + + var shoppingCart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == request.ShoppingCartId, cancellationToken); + + if (shoppingCart is null) + return Result.Fail($"Shopping cart could not be found by ID {request.ShoppingCartId}"); + + if (!await context.ShoppingCartPackages.AnyAsync(cp => cp.ShoppingCartId == request.ShoppingCartId, cancellationToken)) + return Result.Fail($"Package {request.PackageId} is already in the cart"); + + var newShoppingCartPackage = context.ShoppingCartPackages.Add(new Entities.ShoppingCartPackage + { + ShoppingCartId = request.ShoppingCartId, + PackageId = request.PackageId + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Could not add package of id {request.PackageId} to shopping cart {request.ShoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs new file mode 100644 index 0000000..6294199 --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs @@ -0,0 +1,35 @@ +using LiteCharms.Infrastructure.Database; + +namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; + +public class RemovePackageFromShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler +{ + public async ValueTask Handle(RemovePackageFromShoppingCartCommand request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == request.ShoppingCartId, cancellationToken)) + return Result.Fail($"Shopping cart could not be found by ID {request.ShoppingCartId}"); + + if (!await context.ShoppingCartPackages.AnyAsync(p => p.Id == request.ShoppingCartPackageId, cancellationToken)) + return Result.Fail($"Shopping cart package {request.ShoppingCartPackageId} is not in the shopping cart {request.ShoppingCartId}"); + + var shoppingCartPackage = await context.ShoppingCartPackages.FirstOrDefaultAsync(cp => cp.Id == request.ShoppingCartPackageId, cancellationToken); + + if (shoppingCartPackage is null) + return Result.Ok(); + + context.ShoppingCartPackages.Remove(shoppingCartPackage!); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Could remove package of id {request.ShoppingCartPackageId} from shopping cart {request.ShoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs b/LiteCharms.Features/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs new file mode 100644 index 0000000..6aa8f25 --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Features.ShoppingCarts.Commands; + +public class RemovePackageFromShoppingCartCommand : IRequest +{ + public Guid ShoppingCartId { get; set; } + + public Guid ShoppingCartPackageId { get; set; } + + private RemovePackageFromShoppingCartCommand(Guid shoppingCartId, Guid shoppingCartPackageId) + { + ShoppingCartId = shoppingCartId; + ShoppingCartPackageId = shoppingCartPackageId; + } + + public static RemovePackageFromShoppingCartCommand Create(Guid shoppingCartId, Guid shoppingCartPackageId) + { + if (shoppingCartId == Guid.Empty) + throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); + + if (shoppingCartPackageId == Guid.Empty) + throw new ArgumentException($"Shopping cart Package ID is required", nameof(shoppingCartPackageId)); + + return new(shoppingCartId, shoppingCartPackageId); + } +} diff --git a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs b/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs new file mode 100644 index 0000000..2f64359 --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs @@ -0,0 +1,18 @@ +using LiteCharms.Models; + +namespace LiteCharms.Features.ShoppingCarts.Queries; + +public class GetShoppingCartPackagesQuery : IRequest> +{ + public Guid ShoppingCartId { get; set; } + + private GetShoppingCartPackagesQuery(Guid shoppingCartId) => ShoppingCartId = shoppingCartId; + + public static GetShoppingCartPackagesQuery Create(Guid shoppingCartId) + { + if (shoppingCartId == Guid.Empty) + throw new ArgumentException("Shopping cart id is required", nameof(shoppingCartId)); + + return new(shoppingCartId); + } +} diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs new file mode 100644 index 0000000..d6727ee --- /dev/null +++ b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs @@ -0,0 +1,32 @@ +using LiteCharms.Extensions; +using LiteCharms.Infrastructure.Database; +using LiteCharms.Models; + +namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; + +public class GetShoppingCartPackagesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> +{ + public async ValueTask> Handle(GetShoppingCartPackagesQuery request, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == request.ShoppingCartId, cancellationToken)) + return Result.Fail($"Shopping cart could not be found by ID {request.ShoppingCartId}"); + + var packages = await context.ShoppingCartPackages.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(cp => cp.ShoppingCartId == request.ShoppingCartId) + .ToArrayAsync(cancellationToken); + + return packages?.Length > 0 + ? Result.Ok(packages.Select(p => p.ToModel()).ToArray()) + : Result.Fail($"Could not find packaged in shopping cart by ID {request.ShoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} From e8e9a85c57f2de17c47a6299d296773464ec3590 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 15:27:26 +0200 Subject: [PATCH 018/153] Migrated database changes after refactoring the Notification model --- .../NotificationConfiguration.cs | 14 +- .../Configuration/QuoteConfiguration.cs | 2 +- LiteCharms.Extensions/EntityModeMappers.cs | 4 +- .../Handlers/SendEmailCommandHandler.cs | 5 +- .../Commands/SendEmailCommand.cs | 2 +- .../Handlers/CreateLeadCommandHandler.cs | 2 +- .../LiteCharms.Features.csproj | 4 + .../UpdateNotificationCommandHandler.cs | 6 + .../Commands/UpdateNotificationCommand.cs | 10 +- .../Orders/Commands/CreateOrderCommand.cs | 18 +- .../Handlers/CreateOrderCommandHandler.cs | 10 +- .../{ => Hash}/Commands/ComputeHashCommand.cs | 2 +- .../Handlers/ComputeHashCommandHandler.cs | 4 +- .../20260510090446_Init.Designer.cs | 631 ------------------ .../20260510091540_AddedPackages.cs | 114 ---- ...ner.cs => 20260510132008_Init.Designer.cs} | 31 +- ...0090446_Init.cs => 20260510132008_Init.cs} | 112 +++- .../Migrations/ShopDbContextModelSnapshot.cs | 27 +- LiteCharms.Models/Enums.cs | 15 + LiteCharms.Models/Notification.cs | 8 +- 20 files changed, 244 insertions(+), 777 deletions(-) rename LiteCharms.Features/{Utilities => Email}/Commands/Handlers/SendEmailCommandHandler.cs (94%) rename LiteCharms.Features/{Utilities => Email}/Commands/SendEmailCommand.cs (98%) rename LiteCharms.Features/Utilities/{ => Hash}/Commands/ComputeHashCommand.cs (86%) rename LiteCharms.Features/Utilities/{ => Hash}/Commands/Handlers/ComputeHashCommandHandler.cs (80%) delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.Designer.cs delete mode 100644 LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs rename LiteCharms.Infrastructure/Database/Migrations/{20260510091540_AddedPackages.Designer.cs => 20260510132008_Init.Designer.cs} (96%) rename LiteCharms.Infrastructure/Database/Migrations/{20260510090446_Init.cs => 20260510132008_Init.cs} (78%) diff --git a/LiteCharms.Entities/Configuration/NotificationConfiguration.cs b/LiteCharms.Entities/Configuration/NotificationConfiguration.cs index bd103f7..45319a4 100644 --- a/LiteCharms.Entities/Configuration/NotificationConfiguration.cs +++ b/LiteCharms.Entities/Configuration/NotificationConfiguration.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Entities.Configuration; +using LiteCharms.Models; + +namespace LiteCharms.Entities.Configuration; public class NotificationConfiguration : IEntityTypeConfiguration { @@ -9,18 +11,20 @@ public class NotificationConfiguration : IEntityTypeConfiguration builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); - builder.Property(f => f.Direction).IsRequired(); - builder.Property(f => f.Platform).IsRequired(); - builder.Property(f => f.Priority).IsRequired(); + builder.Property(f => f.Direction).IsRequired().HasConversion(); + builder.Property(f => f.Platform).IsRequired().HasConversion(); + builder.Property(f => f.Priority).IsRequired().HasConversion(); + builder.Property(f => f.CorrelationIdType).IsRequired().HasConversion(); builder.Property(f => f.Sender).IsRequired(); builder.Property(f => f.Subject).IsRequired(); builder.Property(f => f.Message).IsRequired(); builder.Property(f => f.Recipient).IsRequired(); builder.Property(f => f.RecipientAddress).IsRequired(); builder.Property(f => f.CorrelationId).IsRequired(); - builder.Property(f => f.CorrelationIdType).IsRequired(); builder.Property(f => f.IsHtml).HasDefaultValue(false); builder.Property(f => f.IsInternal).HasDefaultValue(true); builder.Property(f => f.Processed).HasDefaultValue(false); + builder.Property(f => f.HasError).HasDefaultValue(false); + builder.Property(f => f.Errors).HasColumnType("jsonb").IsRequired(false); } } \ No newline at end of file diff --git a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs b/LiteCharms.Entities/Configuration/QuoteConfiguration.cs index de5240a..d48a768 100644 --- a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs +++ b/LiteCharms.Entities/Configuration/QuoteConfiguration.cs @@ -11,7 +11,7 @@ public class QuoteConfiguration : IEntityTypeConfiguration builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); builder.Property(f => f.ExpiredAt).IsRequired(false); builder.Property(f => f.CustomerId).IsRequired(); - builder.Property(f => f.Status).IsRequired(); + builder.Property(f => f.Status).IsRequired().HasConversion(); builder.Property(f => f.ShoppingCartId).IsRequired(); builder.Property(f => f.Reason).IsRequired(false); diff --git a/LiteCharms.Extensions/EntityModeMappers.cs b/LiteCharms.Extensions/EntityModeMappers.cs index 37ad38e..9a1d97a 100644 --- a/LiteCharms.Extensions/EntityModeMappers.cs +++ b/LiteCharms.Extensions/EntityModeMappers.cs @@ -88,7 +88,9 @@ public static class EntityModeMappers RecipientAddress = entity.RecipientAddress, Priority = entity.Priority, UpdatedAt = entity?.UpdatedAt, - IsHtml = entity!.IsHtml + IsHtml = entity!.IsHtml, + HasError = entity.HasError, + Errors = entity.Errors }; public static Customer ToModel(this Entities.Customer entity) => diff --git a/LiteCharms.Features/Utilities/Commands/Handlers/SendEmailCommandHandler.cs b/LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs similarity index 94% rename from LiteCharms.Features/Utilities/Commands/Handlers/SendEmailCommandHandler.cs rename to LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs index e3fcdf7..30f86dc 100644 --- a/LiteCharms.Features/Utilities/Commands/Handlers/SendEmailCommandHandler.cs +++ b/LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs @@ -1,6 +1,7 @@ -using LiteCharms.Models.Configuraton.Email; +using LiteCharms.Features.Email.Commands; +using LiteCharms.Models.Configuraton.Email; -namespace LiteCharms.Features.Utilities.Commands.Handlers; +namespace LiteCharms.Features.Email.Commands.Handlers; public class SendEmailCommandHandler(IOptions smtpOptions) : IRequestHandler { diff --git a/LiteCharms.Features/Utilities/Commands/SendEmailCommand.cs b/LiteCharms.Features/Email/Commands/SendEmailCommand.cs similarity index 98% rename from LiteCharms.Features/Utilities/Commands/SendEmailCommand.cs rename to LiteCharms.Features/Email/Commands/SendEmailCommand.cs index 4393edd..972e496 100644 --- a/LiteCharms.Features/Utilities/Commands/SendEmailCommand.cs +++ b/LiteCharms.Features/Email/Commands/SendEmailCommand.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Utilities.Commands; +namespace LiteCharms.Features.Email.Commands; public class SendEmailCommand : IRequest { diff --git a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs b/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs index bc8295b..256bc22 100644 --- a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs +++ b/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.Utilities.Commands; +using LiteCharms.Features.Utilities.Hash.Commands; using LiteCharms.Infrastructure.Database; namespace LiteCharms.Features.Leads.Commands.Handlers; diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 9a2a0ac..6f9f146 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -61,4 +61,8 @@ + + + + diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs b/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs index 066638a..5ce4e81 100644 --- a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs +++ b/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs @@ -17,6 +17,12 @@ public class UpdateNotificationCommandHandler(IDbContextFactory c notification.Processed = request.Processed; + if (request.HasError) + { + notification.HasError = request.HasError; + notification.Errors = request.Errors; + } + return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail(new Error($"Failed to update notification with id {request.NotificationId}.")); diff --git a/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs b/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs index d5961f2..950442d 100644 --- a/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs +++ b/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs @@ -6,13 +6,19 @@ public class UpdateNotificationCommand : IRequest public bool Processed { get; set; } - private UpdateNotificationCommand(Guid notificationId, bool processed) + public bool HasError { get; set; } + + public string[]? Errors { get; set; } + + private UpdateNotificationCommand(Guid notificationId, bool processed, bool hasError = false, string[]? errors = null) { NotificationId = notificationId; Processed = processed; + HasError = hasError; + Errors = errors; } - public static UpdateNotificationCommand Create(Guid notificationId, bool processed) + public static UpdateNotificationCommand Create(Guid notificationId, bool processed, bool hasError = false, string[]? errors = null) { if(notificationId == Guid.Empty) throw new ArgumentException("Notification ID cannot be empty.", nameof(notificationId)); diff --git a/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs b/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs index e18258e..8baf7e1 100644 --- a/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs +++ b/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs @@ -8,14 +8,26 @@ public class CreateOrderCommand : IRequest> public Guid? QuoteId { get; set; } - private CreateOrderCommand(Guid customerId, Guid shoppingCartId, Guid? quoteId = null) + public string[]? Requirements { get; set; } + + public string[]? Notes { get; set; } + + public string[]? Terms { get; set; } + + public bool DepositRequired { get; set; } + + private CreateOrderCommand(Guid customerId, Guid shoppingCartId, bool depositRequired, Guid? quoteId = null, string[]? requirements = null, string[]? notes = null, string[]? terms = null) { CustomerId = customerId; ShoppingCartId = shoppingCartId; + DepositRequired = depositRequired; QuoteId = quoteId; + Requirements = requirements; + Notes = notes; + Terms = terms; } - public static CreateOrderCommand Create(Guid customerId, Guid shoppingCartId, Guid? quoteId = null) + public static CreateOrderCommand Create(Guid customerId, Guid shoppingCartId, bool depositRequired, Guid? quoteId = null, string[]? requirements = null, string[]? notes = null, string[]? terms = null) { if (customerId == Guid.Empty) throw new ArgumentException("CustomerId is required.", nameof(customerId)); @@ -23,6 +35,6 @@ public class CreateOrderCommand : IRequest> if (shoppingCartId == Guid.Empty) throw new ArgumentException("ShoppingCartId is required.", nameof(shoppingCartId)); - return new(customerId, shoppingCartId, quoteId); + return new(customerId, shoppingCartId, depositRequired, quoteId, requirements, notes, terms); } } diff --git a/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs b/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs index f06e53b..b97f83e 100644 --- a/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs +++ b/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs @@ -1,4 +1,5 @@ using LiteCharms.Infrastructure.Database; +using LiteCharms.Models; namespace LiteCharms.Features.Orders.Commands.Handlers; @@ -21,10 +22,15 @@ public class CreateOrderCommandHandler(IDbContextFactory contextF var newOrder = context.Orders.Add(new Entities.Order { + CreatedAt = DateTime.UtcNow, + Status = OrderStatus.Pending, CustomerId = request.CustomerId, - ShoppingCartId = request.ShoppingCartId, QuoteId = request.QuoteId, - CreatedAt = DateTime.UtcNow + ShoppingCartId = request.ShoppingCartId, + DepositRequired = request.DepositRequired, + Requirements = request.Requirements, + Notes = request.Notes, + Terms = request.Terms }); return await context.SaveChangesAsync(cancellationToken) > 0 diff --git a/LiteCharms.Features/Utilities/Commands/ComputeHashCommand.cs b/LiteCharms.Features/Utilities/Hash/Commands/ComputeHashCommand.cs similarity index 86% rename from LiteCharms.Features/Utilities/Commands/ComputeHashCommand.cs rename to LiteCharms.Features/Utilities/Hash/Commands/ComputeHashCommand.cs index ef235fc..252a9e7 100644 --- a/LiteCharms.Features/Utilities/Commands/ComputeHashCommand.cs +++ b/LiteCharms.Features/Utilities/Hash/Commands/ComputeHashCommand.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Utilities.Commands; +namespace LiteCharms.Features.Utilities.Hash.Commands; public class ComputeHashCommand : IRequest> { diff --git a/LiteCharms.Features/Utilities/Commands/Handlers/ComputeHashCommandHandler.cs b/LiteCharms.Features/Utilities/Hash/Commands/Handlers/ComputeHashCommandHandler.cs similarity index 80% rename from LiteCharms.Features/Utilities/Commands/Handlers/ComputeHashCommandHandler.cs rename to LiteCharms.Features/Utilities/Hash/Commands/Handlers/ComputeHashCommandHandler.cs index d4ef5b4..013ab03 100644 --- a/LiteCharms.Features/Utilities/Commands/Handlers/ComputeHashCommandHandler.cs +++ b/LiteCharms.Features/Utilities/Hash/Commands/Handlers/ComputeHashCommandHandler.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.Utilities.Commands.Handlers; +using LiteCharms.Features.Utilities.Hash.Commands; + +namespace LiteCharms.Features.Utilities.Hash.Commands.Handlers; public class ComputeHashCommandHandler : IRequestHandler> { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.Designer.cs deleted file mode 100644 index 6ae5382..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.Designer.cs +++ /dev/null @@ -1,631 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(ShopDbContext))] - [Migration("20260510090446_Init")] - partial class Init - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttributionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickId") - .HasColumnType("text"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("Source") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Notification", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CorrelationId") - .IsRequired() - .HasColumnType("text"); - - b.Property("CorrelationIdType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Direction") - .HasColumnType("integer"); - - b.Property("IsHtml") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("IsInternal") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Message") - .IsRequired() - .HasColumnType("text"); - - b.Property("Platform") - .HasColumnType("integer"); - - b.Property("Priority") - .HasColumnType("integer"); - - b.Property("Processed") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("Recipient") - .IsRequired() - .HasColumnType("text"); - - b.Property("RecipientAddress") - .IsRequired() - .HasColumnType("text"); - - b.Property("Sender") - .IsRequired() - .HasColumnType("text"); - - b.Property("SenderName") - .HasColumnType("text"); - - b.Property("Subject") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Notification", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("DepositRequired") - .HasColumnType("boolean"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("QuoteId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Requirements") - .HasColumnType("jsonb"); - - b.Property("ShoppingCartId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.PrimitiveCollection("Terms") - .HasColumnType("jsonb"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("QuoteId") - .IsUnique(); - - b.HasIndex("ShoppingCartId") - .IsUnique(); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Quote", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("CustomerId1") - .HasColumnType("uuid"); - - b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Reason") - .HasColumnType("text"); - - b.Property("ShoppingCartId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("CustomerId1"); - - b.HasIndex("ShoppingCartId") - .IsUnique(); - - b.ToTable("Quote", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("QuoteId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("ShoppingCart", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("Quantity") - .HasColumnType("integer"); - - b.Property("ShoppingCartId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductPriceId"); - - b.HasIndex("ShoppingCartId"); - - b.ToTable("ShoppingCartItems"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.Quote", "Quote") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "QuoteId") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("Quote"); - - b.Navigation("ShoppingCart"); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.HasOne("LiteCharms.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Quote", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany() - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.Customer", null) - .WithMany("Quotes") - .HasForeignKey("CustomerId1"); - - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithOne("Quote") - .HasForeignKey("LiteCharms.Entities.Quote", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ShoppingCart"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("ShoppingCarts") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => - { - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithMany("ShoppingCartItems") - .HasForeignKey("ShoppingCartId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ProductPrice"); - - b.Navigation("ShoppingCart"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - - b.Navigation("Quotes"); - - b.Navigation("ShoppingCarts"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Quote", b => - { - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => - { - b.Navigation("Order"); - - b.Navigation("Quote"); - - b.Navigation("ShoppingCartItems"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs deleted file mode 100644 index 351ac6b..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class AddedPackages : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Package", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - Name = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), - Active = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Package", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "PackageItem", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - PackageId1 = table.Column(type: "uuid", nullable: true), - PackageId = table.Column(type: "uuid", nullable: false), - ProductPriceId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - Active = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PackageItem", x => x.Id); - table.ForeignKey( - name: "FK_PackageItem_Package_PackageId", - column: x => x.PackageId, - principalTable: "Package", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_PackageItem_Package_PackageId1", - column: x => x.PackageId1, - principalTable: "Package", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "ShoppingCartPackage", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ShoppingCartId = table.Column(type: "uuid", nullable: false), - PackageId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ShoppingCartPackage", x => x.Id); - table.ForeignKey( - name: "FK_ShoppingCartPackage_Package_PackageId", - column: x => x.PackageId, - principalTable: "Package", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_ShoppingCartPackage_ShoppingCart_ShoppingCartId", - column: x => x.ShoppingCartId, - principalTable: "ShoppingCart", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_PackageItem_PackageId", - table: "PackageItem", - column: "PackageId"); - - migrationBuilder.CreateIndex( - name: "IX_PackageItem_PackageId1", - table: "PackageItem", - column: "PackageId1"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingCartPackage_PackageId", - table: "ShoppingCartPackage", - column: "PackageId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingCartPackage_ShoppingCartId", - table: "ShoppingCartPackage", - column: "ShoppingCartId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "PackageItem"); - - migrationBuilder.DropTable( - name: "ShoppingCartPackage"); - - migrationBuilder.DropTable( - name: "Package"); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.Designer.cs similarity index 96% rename from LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.Designer.cs rename to LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.Designer.cs index f83fabb..4624007 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260510091540_AddedPackages.Designer.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Infrastructure.Database.Migrations { [DbContext(typeof(ShopDbContext))] - [Migration("20260510091540_AddedPackages")] - partial class AddedPackages + [Migration("20260510132008_Init")] + partial class Init { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -171,9 +171,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations .IsRequired() .HasColumnType("text"); - b.Property("CorrelationIdType") - .IsRequired() - .HasColumnType("text"); + b.Property("CorrelationIdType") + .HasColumnType("integer"); b.Property("CreatedAt") .ValueGeneratedOnAdd() @@ -182,6 +181,14 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("Direction") .HasColumnType("integer"); + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("IsHtml") .ValueGeneratedOnAdd() .HasColumnType("boolean") @@ -339,12 +346,17 @@ namespace LiteCharms.Infrastructure.Database.Migrations .IsRequired() .HasColumnType("text"); + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + b.Property("UpdatedAt") .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); + b.HasIndex("ShoppingCartId"); + b.ToTable("Package", (string)null); }); @@ -614,6 +626,13 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Entities.Package", b => + { + b.HasOne("LiteCharms.Entities.ShoppingCart", null) + .WithMany("Packages") + .HasForeignKey("ShoppingCartId"); + }); + modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => { b.HasOne("LiteCharms.Entities.Package", "Package") @@ -746,6 +765,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations { b.Navigation("Order"); + b.Navigation("Packages"); + b.Navigation("Quote"); b.Navigation("ShoppingCartItems"); diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.cs b/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.cs similarity index 78% rename from LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.cs rename to LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.cs index 3bc9554..773f0d6 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260510090446_Init.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.cs @@ -51,6 +51,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations Direction = table.Column(type: "integer", nullable: false), Platform = table.Column(type: "integer", nullable: false), Priority = table.Column(type: "integer", nullable: false), + CorrelationIdType = table.Column(type: "integer", nullable: false), Sender = table.Column(type: "text", nullable: false), SenderName = table.Column(type: "text", nullable: true), Subject = table.Column(type: "text", nullable: false), @@ -58,10 +59,11 @@ namespace LiteCharms.Infrastructure.Database.Migrations Recipient = table.Column(type: "text", nullable: false), RecipientAddress = table.Column(type: "text", nullable: false), CorrelationId = table.Column(type: "text", nullable: false), - CorrelationIdType = table.Column(type: "text", nullable: false), IsHtml = table.Column(type: "boolean", nullable: false, defaultValue: false), IsInternal = table.Column(type: "boolean", nullable: false, defaultValue: true), - Processed = table.Column(type: "boolean", nullable: false, defaultValue: false) + Processed = table.Column(type: "boolean", nullable: false, defaultValue: false), + HasError = table.Column(type: "boolean", nullable: false, defaultValue: false), + Errors = table.Column(type: "jsonb", nullable: true) }, constraints: table => { @@ -157,6 +159,28 @@ namespace LiteCharms.Infrastructure.Database.Migrations onDelete: ReferentialAction.Restrict); }); + migrationBuilder.CreateTable( + name: "Package", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ShoppingCartId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Package", x => x.Id); + table.ForeignKey( + name: "FK_Package_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( name: "Quote", columns: table => new @@ -220,6 +244,56 @@ namespace LiteCharms.Infrastructure.Database.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "PackageItem", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PackageId1 = table.Column(type: "uuid", nullable: true), + PackageId = table.Column(type: "uuid", nullable: false), + ProductPriceId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PackageItem", x => x.Id); + table.ForeignKey( + name: "FK_PackageItem_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_PackageItem_Package_PackageId1", + column: x => x.PackageId1, + principalTable: "Package", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCartPackage", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + PackageId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCartPackage", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCartPackage_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_ShoppingCartPackage_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( name: "Order", columns: table => new @@ -308,6 +382,21 @@ namespace LiteCharms.Infrastructure.Database.Migrations column: "OrderId", unique: true); + migrationBuilder.CreateIndex( + name: "IX_Package_ShoppingCartId", + table: "Package", + column: "ShoppingCartId"); + + migrationBuilder.CreateIndex( + name: "IX_PackageItem_PackageId", + table: "PackageItem", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_PackageItem_PackageId1", + table: "PackageItem", + column: "PackageId1"); + migrationBuilder.CreateIndex( name: "IX_ProductPrice_ProductId", table: "ProductPrice", @@ -343,6 +432,16 @@ namespace LiteCharms.Infrastructure.Database.Migrations name: "IX_ShoppingCartItems_ShoppingCartId", table: "ShoppingCartItems", column: "ShoppingCartId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_PackageId", + table: "ShoppingCartPackage", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_ShoppingCartId", + table: "ShoppingCartPackage", + column: "ShoppingCartId"); } /// @@ -357,15 +456,24 @@ namespace LiteCharms.Infrastructure.Database.Migrations migrationBuilder.DropTable( name: "OrderRefund"); + migrationBuilder.DropTable( + name: "PackageItem"); + migrationBuilder.DropTable( name: "ShoppingCartItems"); + migrationBuilder.DropTable( + name: "ShoppingCartPackage"); + migrationBuilder.DropTable( name: "Order"); migrationBuilder.DropTable( name: "ProductPrice"); + migrationBuilder.DropTable( + name: "Package"); + migrationBuilder.DropTable( name: "Quote"); diff --git a/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs b/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs index 8a32836..6708099 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs @@ -168,9 +168,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations .IsRequired() .HasColumnType("text"); - b.Property("CorrelationIdType") - .IsRequired() - .HasColumnType("text"); + b.Property("CorrelationIdType") + .HasColumnType("integer"); b.Property("CreatedAt") .ValueGeneratedOnAdd() @@ -179,6 +178,14 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("Direction") .HasColumnType("integer"); + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("IsHtml") .ValueGeneratedOnAdd() .HasColumnType("boolean") @@ -336,12 +343,17 @@ namespace LiteCharms.Infrastructure.Database.Migrations .IsRequired() .HasColumnType("text"); + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + b.Property("UpdatedAt") .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); + b.HasIndex("ShoppingCartId"); + b.ToTable("Package", (string)null); }); @@ -611,6 +623,13 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Entities.Package", b => + { + b.HasOne("LiteCharms.Entities.ShoppingCart", null) + .WithMany("Packages") + .HasForeignKey("ShoppingCartId"); + }); + modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => { b.HasOne("LiteCharms.Entities.Package", "Package") @@ -743,6 +762,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations { b.Navigation("Order"); + b.Navigation("Packages"); + b.Navigation("Quote"); b.Navigation("ShoppingCartItems"); diff --git a/LiteCharms.Models/Enums.cs b/LiteCharms.Models/Enums.cs index 385a5b5..b672e25 100644 --- a/LiteCharms.Models/Enums.cs +++ b/LiteCharms.Models/Enums.cs @@ -1,5 +1,20 @@ namespace LiteCharms.Models; +public enum CorrelationIdTypes : int +{ + None = 0, + Email = 1, + Discord = 2, + Slack = 3, + Whatsapp = 4, + Customer = 5, + Order = 6, + Refund = 7, + Lead = 8, + Quote = 9, + LinkedIn = 10 +} + public enum Priorities : int { Low = 0, diff --git a/LiteCharms.Models/Notification.cs b/LiteCharms.Models/Notification.cs index 90ac390..6108cbb 100644 --- a/LiteCharms.Models/Notification.cs +++ b/LiteCharms.Models/Notification.cs @@ -14,6 +14,8 @@ public class Notification public Priorities Priority { get; set; } + public CorrelationIdTypes CorrelationIdType { get; set; } + public string? Sender { get; set; } public string? SenderName { get; set; } @@ -28,11 +30,13 @@ public class Notification public string? CorrelationId { get; set; } - public string? CorrelationIdType { get; set; } - public bool IsHtml { get; set; } public bool IsInternal { get; set; } public bool Processed { get; set; } + + public bool HasError { get; set; } + + public string[]? Errors { get; set; } } From 73ba41beaf17301bbfcb8d8b4a918390120e5ac6 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 16:07:53 +0200 Subject: [PATCH 019/153] Added outgoing email notification processing event --- LiteCharms.Abstractions/Constants.cs | 2 + LiteCharms.Abstractions/EventBase.cs | 12 ++++ .../LiteCharms.Features.csproj | 5 +- .../ProcessEmailNotificationsEventHandler.cs | 70 +++++++++++++++++++ .../Events/ProcessEmailNotificationsEvent.cs | 16 +++++ .../Handlers/GetNotificationQueryHandler.cs | 2 +- 6 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 LiteCharms.Abstractions/EventBase.cs create mode 100644 LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs create mode 100644 LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs diff --git a/LiteCharms.Abstractions/Constants.cs b/LiteCharms.Abstractions/Constants.cs index 31be72a..ae08f94 100644 --- a/LiteCharms.Abstractions/Constants.cs +++ b/LiteCharms.Abstractions/Constants.cs @@ -5,6 +5,8 @@ public static class Constants public const int QueueBounds = 100000; public const string ShopSchedulerName = "shop"; + public const string ShopEmailFromName = "Khongisa Shop"; + public const string ShopEmailFromAddress = "shop@litecharms.co.za"; public const string EmailServiceBus = nameof(EmailServiceBus); public const string GeneralServiceBus = nameof(GeneralServiceBus); diff --git a/LiteCharms.Abstractions/EventBase.cs b/LiteCharms.Abstractions/EventBase.cs new file mode 100644 index 0000000..6c11736 --- /dev/null +++ b/LiteCharms.Abstractions/EventBase.cs @@ -0,0 +1,12 @@ +using static LiteCharms.Abstractions.Timezones; + +namespace LiteCharms.Abstractions; + +public abstract class EventBase +{ + public Guid Id { get; set; } = Guid.CreateVersion7(); + + public DateTimeOffset EnqueueAt { get; set; } = SouthAfricanTimeZone.UtcNow(); + + public string CorrelationId { get; set; } = Guid.CreateVersion7().ToString(); +} diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 6f9f146..f541547 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -54,6 +54,7 @@ + @@ -61,8 +62,4 @@ - - - - diff --git a/LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs b/LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs new file mode 100644 index 0000000..61b24d5 --- /dev/null +++ b/LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs @@ -0,0 +1,70 @@ +using LiteCharms.Features.Email.Commands; +using LiteCharms.Infrastructure.Database; +using static LiteCharms.Abstractions.Constants; + +namespace LiteCharms.Features.Notifications.Events.Handlers; + +public class ProcessEmailNotificationsEventHandler(IDbContextFactory contextFactory, ILogger logger, ISender mediator) : + INotificationHandler +{ + public async ValueTask Handle(ProcessEmailNotificationsEvent message, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var notifications = await context.Notifications + .OrderByDescending(o => o.Priority) + .ThenBy(o => o.CreatedAt) + .Where(n => n.CorrelationIdType == Models.CorrelationIdTypes.Email) + .Where(n => n.Direction == Models.NotificationDirection.Outgoing) + .Take(message.MaxRecords) + .ToListAsync(cancellationToken); + + foreach (var notification in notifications) + { + var sendResult = await SendEmailAsync(notification, cancellationToken); + + if(sendResult.IsFailed) + { + var errors = new List(1000); + + errors.AddRange(sendResult.Errors.Select(e => e.Message)); + + if (sendResult.Reasons?.Count > 0) + errors.AddRange(sendResult.Reasons.Select(e => e.Message)); + + notification.HasError = true; + notification.Errors = [.. errors]; + } + + notification.Processed = true; + } + + await context.SaveChangesAsync(cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, ex.Message); + } + } + + private async Task SendEmailAsync(Entities.Notification notification, CancellationToken cancellationToken = default) + { + try + { + var request = SendEmailCommand.Create(notification.Sender!, notification.SenderName!, ShopEmailFromAddress, + ShopEmailFromName, notification.Subject!, notification.Message!); + + var result = await mediator.Send(request, cancellationToken); + + return result.IsFailed + ? Result.Fail(result.Errors) + : Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs b/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs new file mode 100644 index 0000000..3735ecd --- /dev/null +++ b/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs @@ -0,0 +1,16 @@ +using LiteCharms.Abstractions; + +namespace LiteCharms.Features.Notifications.Events; + +public class ProcessEmailNotificationsEvent : EventBase, IEvent +{ + public string Name { get; set; } = nameof(ProcessEmailNotificationsEvent); + + public int MaxRecords { get; set; } + + public ProcessEmailNotificationsEvent() => MaxRecords = 1000; + + private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords; + + public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords); +} diff --git a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs index 106dd88..5eac5d8 100644 --- a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs +++ b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs @@ -12,7 +12,7 @@ public class GetNotificationQueryHandler(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var notification = await context.Notifications.FindAsync(new object[] { request.NotificationId }, cancellationToken); + var notification = await context.Notifications.FirstOrDefaultAsync(n => n.Id == request.NotificationId, cancellationToken); return notification is not null ? Result.Ok(notification.ToModel()) From cecd9f90e9812a2bd84b2bb8026dbc9cce79741f Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 16:50:36 +0200 Subject: [PATCH 020/153] Implemented service bus handling of emails and notification processing --- .../SendShopEmailEnquiryEventHandler.cs | 18 +++++++++ .../Email/Events/SendShopEmailEnquiryEvent.cs | 40 +++++++++++++++++++ .../Commands/CreateNotificationCommand.cs | 9 ++--- .../Events/ProcessEmailNotificationsEvent.cs | 2 - .../LiteCharms.Infrastructure.csproj | 3 +- .../ServiceBus/Exchanges/EmailExchange.cs | 29 +++++++++++++- 6 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs create mode 100644 LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs diff --git a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs new file mode 100644 index 0000000..9c19500 --- /dev/null +++ b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs @@ -0,0 +1,18 @@ +using LiteCharms.Features.Notifications.Commands; +using static LiteCharms.Abstractions.Constants; + +namespace LiteCharms.Features.Email.Events.Handlers; + +public class SendShopEmailEnquiryEventHandler(ISender mediator) : + INotificationHandler +{ + public async ValueTask Handle(SendShopEmailEnquiryEvent notification, CancellationToken cancellationToken) + { + var command = CreateNotificationCommand.Create(Models.NotificationDirection.Outgoing, notification.SenderName!, + notification.SenderAddress!, notification.Subject!, notification.Message!, Models.NotificationPlatforms.Email, + notification.Priority, ShopEmailFromName, ShopEmailFromAddress, Guid.CreateVersion7().ToString(), + Models.CorrelationIdTypes.None, isInternal: true, isHtml: false); + + await mediator.Send(command, cancellationToken); + } +} diff --git a/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs b/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs new file mode 100644 index 0000000..07e3830 --- /dev/null +++ b/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs @@ -0,0 +1,40 @@ +using LiteCharms.Abstractions; +using LiteCharms.Models; + +namespace LiteCharms.Features.Email.Events; + +public class SendShopEmailEnquiryEvent : EventBase, IEvent +{ + public string Name { get; set; } = nameof(SendShopEmailEnquiryEvent); + + public string? SenderName { get; set; } + + public string? SenderAddress { get; set; } + + public string? Subject { get; set; } + + public string? Message { get; set; } + + public Priorities Priority { get; set; } + + public SendShopEmailEnquiryEvent() { } + + private SendShopEmailEnquiryEvent(string senderName, string senderAddress, string subject, string message, Priorities priority = Priorities.Medium) + { + SenderName = senderName; + SenderAddress = senderAddress; + Subject = subject; + Message = message; + Priority = priority; + } + + public static SendShopEmailEnquiryEvent Create(string senderName, string senderAddress, string subject, string message, Priorities priority = Priorities.Medium) + { + ArgumentNullException.ThrowIfNullOrWhiteSpace(senderName, nameof(senderName)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(senderAddress, nameof(senderAddress)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(subject, nameof(subject)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(message, nameof(message)); + + return new(senderName, senderAddress, subject, message, priority); + } +} diff --git a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs index a9cf063..2b3e933 100644 --- a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs +++ b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs @@ -24,13 +24,13 @@ public class CreateNotificationCommand : IRequest> public string? CorrelationId { get; set; } - public string? CorrelationIdType { get; set; } + public CorrelationIdTypes CorrelationIdType { get; set; } public bool IsInternal { get; set; } public bool IsHtml { get; set; } - private CreateNotificationCommand(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, string correlationIdType, bool isInternal, bool isHtml = false) + private CreateNotificationCommand(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false) { Direction = direction; Sender = sender; @@ -47,7 +47,7 @@ public class CreateNotificationCommand : IRequest> IsHtml = isHtml; } - public static CreateNotificationCommand Create(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, string correlationIdType, bool isInternal, bool isHtml = false) + public static CreateNotificationCommand Create(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false) { if (string.IsNullOrWhiteSpace(sender)) throw new ArgumentException("Sender name is required.", nameof(sender)); @@ -67,9 +67,6 @@ public class CreateNotificationCommand : IRequest> if (string.IsNullOrWhiteSpace(correlationId)) throw new ArgumentException("CorrelationId is required.", nameof(correlationId)); - if (string.IsNullOrWhiteSpace(correlationIdType)) - throw new ArgumentException("CorrelationIdType is required.", nameof(correlationIdType)); - return new(direction, sender, senderAddress, subject, message, platform, priority, recipient, recipientAddress, correlationId, correlationIdType, isInternal, isHtml); } } diff --git a/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs b/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs index 3735ecd..9ac754f 100644 --- a/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs +++ b/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs @@ -8,8 +8,6 @@ public class ProcessEmailNotificationsEvent : EventBase, IEvent public int MaxRecords { get; set; } - public ProcessEmailNotificationsEvent() => MaxRecords = 1000; - private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords; public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords); diff --git a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj b/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj index 765140f..b387b65 100644 --- a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj +++ b/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj @@ -89,10 +89,11 @@ - + + diff --git a/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs b/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs index 52928fd..145ce46 100644 --- a/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs +++ b/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs @@ -2,11 +2,36 @@ namespace LiteCharms.Infrastructure.ServiceBus.Exchanges; -public class EmailExchange(EmailQueue messages) : BackgroundService +public class EmailExchange(EmailQueue messages, ILogger logger, IPublisher mediator) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - if(messages.Incoming.CanCount) + while (!stoppingToken.IsCancellationRequested) + { + while (messages.Incoming.TryRead(out var message)) + { + try + { + switch (message.Name) + { + case "SendShopEmailEnquiryEvent": + await mediator.Publish(message, stoppingToken); + break; + case "ProcessEmailNotificationsEvent": + await mediator.Publish(message, stoppingToken); + break; + default: + logger.LogWarning("Unsupported email event {Event}", message.Name); + break; + } + } + catch (Exception ex) + { + logger.LogError(ex, ex.Message); + } + } + await Task.Delay(1000, stoppingToken); + } } } From 26075cd9a7c04c294053afead63c46fff7b16cfc Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 10 May 2026 17:32:09 +0200 Subject: [PATCH 021/153] Updated job scheduler --- LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs b/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs index c03f3c6..6874541 100644 --- a/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs +++ b/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs @@ -35,7 +35,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra var chainedJobGroup = "scheduled-jobs"; var scheduler = await schedulerFactory.GetScheduler(cancellationToken); - var jobKey = new JobKey($"{notification.Name.ToLower()}-{notification.CorrelationId.ToLower()}", chainedJobGroup); + var jobKey = new JobKey($"{notification.Name.ToLower()}", chainedJobGroup); var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup); var job = JobBuilder.Create>() From a42c51d7b25fb910b593ee7b799d14760713dc4a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 13 May 2026 20:06:24 +0200 Subject: [PATCH 022/153] Retructured solution --- .../LiteCharms.Abstractions.csproj | 40 --- .../Configuration/OrderConfiguration.cs | 32 -- .../Configuration/PackageConfirguration.cs | 16 - .../Configuration/ProductConfiguration.cs | 14 - .../Configuration/QuoteConfiguration.cs | 23 -- .../ShoppingCartConfiguration.cs | 31 -- .../ShoppingCartItemConfiguration.cs | 25 -- .../LiteCharms.Entities.csproj | 45 --- LiteCharms.Entities/Order.cs | 15 - LiteCharms.Entities/PackageItem.cs | 9 - LiteCharms.Extensions/Email.cs | 13 - .../LiteCharms.Extensions.csproj | 113 -------- LiteCharms.Features.Tests/CommonFixture.cs | 34 +++ .../LiteCharms.Features.Tests.csproj | 50 ++++ .../NotificationsFeatureTests.cs | 19 ++ LiteCharms.Features.Tests/appsettings.json | 22 ++ .../Abstractions}/EventBase.cs | 5 +- .../Abstractions}/IEvent.cs | 2 +- .../Handlers/SendEmailCommandHandler.cs | 61 ---- .../Email/Commands/SendEmailCommand.cs | 79 ----- .../Email/Configuration}/Account.cs | 2 +- .../Email/Configuration}/SmtpSettings.cs | 2 +- LiteCharms.Features/Email/EmailService.cs | 192 ++++++++++++ .../SendShopEmailEnquiryEventHandler.cs | 12 +- .../Email/Events/SendShopEmailEnquiryEvent.cs | 4 +- .../Email/Extensions/Constants.cs | 8 + .../Email/Extensions/EmailTelemetry.cs | 9 + LiteCharms.Features/Email/Extensions/Setup.cs | 19 ++ LiteCharms.Features/Email/IEmailService.cs | 15 + .../Email/Models/Attachment.cs | 8 + LiteCharms.Features/Email/Models/Body.cs | 26 ++ .../Email/Models/BodyProperties.cs | 8 + .../Email/Models}/EmailEnquiry.cs | 4 +- LiteCharms.Features/Email/Models/Message.cs | 19 ++ LiteCharms.Features/Email/Models/Party.cs | 8 + LiteCharms.Features/Email/Models/Response.cs | 22 ++ LiteCharms.Features/Extensions/Constants.cs | 5 + .../Extensions}/EntityModeMappers.cs | 0 LiteCharms.Features/Extensions/Hash.cs | 7 + .../Extensions}/HealthChecks.cs | 0 .../Extensions}/Monitoring.cs | 0 .../Extensions}/Postgres.cs | 0 .../Extensions}/Quartz.cs | 0 .../Extensions}/ServiceBus.cs | 0 .../Extensions}/Timezones.cs | 2 +- .../HealthChecks/PostgresHealthCheck.cs | 2 +- .../HealthChecks/QuartzHealthCheck.cs | 2 +- .../LiteCharms.Features.csproj | 95 +++++- .../Mediator/LoggingPipelineBehavior.cs | 29 ++ .../Mediator/MediatorTelemetry.cs | 12 + .../Mediator/TelemetryPipelineBehavior.cs | 66 +++++ .../Quartz/Abstractions}/IJobOrchestrator.cs | 4 +- .../Quartz/JobOrchestrator.cs | 7 +- .../Quartz/MediatorJob.cs | 4 +- .../Quartz/RetryJobListener.cs | 2 +- .../Abstractions}/EventBusQueueBase.cs | 4 +- .../ServiceBus/Abstractions}/IEventBus.cs | 4 +- .../Abstractions}/IEventBusQueue.cs | 4 +- .../ServiceBus}/Constants.cs | 6 +- .../ServiceBus/EmailServiceBus.cs | 7 +- .../ServiceBus/Exchanges/EmailExchange.cs | 33 +++ .../ServiceBus/Exchanges/GeneralExchange.cs | 4 +- .../ServiceBus/Exchanges/SalesExchange.cs | 4 +- .../ServiceBus/GeneralServiceBus.cs | 7 +- .../ServiceBus/Queues/EmailQueue.cs | 5 + .../ServiceBus/Queues/GeneralQueue.cs | 5 + .../ServiceBus/Queues/SalesQueue.cs | 5 + .../ServiceBus/SalesServiceBus.cs | 7 +- .../Commands/AddPackageItemsCommand.cs | 0 .../Commands/CreatePackageCommand.cs | 0 .../Commands/DeletePackageCommand.cs | 0 .../Commands/DeletePackageItemsCommand.cs | 0 .../Handlers/AddPackageItemCommandHandler.cs | 2 +- .../Handlers/CreatePackageCommandHandler.cs | 2 +- .../DeletePackageItemCommandHandler.cs | 2 +- .../DeletePackageItemsCommandHandler.cs | 2 +- .../Handlers/UpdatePackageCommandHandler.cs | 2 +- .../UpdatePackageStatusCommandHandler.cs | 2 +- .../Commands/UpdatePackageCommand.cs | 0 .../Commands/UpdatePackageStatusCommand.cs | 0 .../Shop/CartPackages/Entities}/Package.cs | 4 +- .../Entities/PackageConfirguration.cs | 18 ++ .../Shop/CartPackages/Entities/PackageItem.cs | 11 + .../Entities}/PackageItemConfiguration.cs | 17 +- .../Shop/CartPackages/Models}/Package.cs | 6 +- .../Shop/CartPackages/Models}/PackageItem.cs | 2 +- .../Queries/GetPackageItemsQuery.cs | 2 +- .../CartPackages/Queries/GetPackageQuery.cs | 2 +- .../CartPackages/Queries/GetPackagesQuery.cs | 2 +- .../Handlers/GetPackageItemsQueryHandler.cs | 4 +- .../Handlers/GetPackageQueryHandler.cs | 4 +- .../Handlers/GetPackagesQueryHandler.cs | 4 +- .../Commands/CreateCustomerCommand.cs | 0 .../Handlers/CreateCustomerCommandHandler.cs | 2 +- .../Handlers/UpdateCustomerCommandHandler.cs | 2 +- .../Commands/UpdateCustomerCommand.cs | 0 .../Shop/Customers/Entities}/Customer.cs | 7 +- .../Entities}/CustomerConfiguration.cs | 11 +- .../Shop/Customers/Models}/Customer.cs | 2 +- .../Customers/Queries/GetCustomerQuery.cs | 2 +- .../Customers/Queries/GetCustomersQuery.cs | 2 +- .../Handlers/GetCustomerQueryHandler.cs | 4 +- .../Handlers/GetCustomersQueryHandler.cs | 4 +- .../Shop}/Enums.cs | 17 +- .../Leads/Commands/CreateLeadCommand.cs | 0 .../Handlers/CreateLeadCommandHandler.cs | 4 +- .../Handlers/UpdateLeadCommandHandler.cs | 2 +- .../Leads/Commands/UpdateLeadCommand.cs | 3 +- .../Shop/Leads/Entities}/Lead.cs | 4 +- .../Shop/Leads/Entities}/LeadConfiguration.cs | 6 +- .../Shop/Leads/Models}/Lead.cs | 2 +- .../Leads/Queries/GetCustomerLeadsQuery.cs | 2 +- .../{ => Shop}/Leads/Queries/GetLeadsQuery.cs | 2 +- .../Handlers/GetCustomerLeadsQueryHandler.cs | 4 +- .../Queries/Handlers/GetLeadsQueryHandler.cs | 4 +- .../Commands/CreateNotificationCommand.cs | 9 +- .../CreateNotificationCommandHandler.cs | 6 +- .../UpdateNotificationCommandHandler.cs | 2 +- .../Commands/UpdateNotificationCommand.cs | 0 .../Notifications/Entities}/Notification.cs | 4 +- .../Entities}/NotificationConfiguration.cs | 8 +- .../ProcessEmailNotificationsEventHandler.cs | 7 +- .../Events/ProcessEmailNotificationsEvent.cs | 2 +- .../Notifications/INotificationService.cs | 9 + .../Notifications/Models}/Notification.cs | 2 +- .../Shop/Notifications/Models/Records.cs | 41 +++ .../Shop/Notifications/NotificationService.cs | 5 + .../Queries/GetNotificationQuery.cs | 2 +- .../Queries/GetNotificationsQuery.cs | 2 +- .../Handlers/GetNotificationQueryHandler.cs | 4 +- .../Handlers/GetNotificationsQueryHandler.cs | 4 +- .../Orders/Commands/CreateOrderCommand.cs | 0 .../Handlers/CreateOrderCommandHandler.cs | 3 +- .../UpdateOrderStatusCommandHandler.cs | 2 +- .../Commands/UpdateOrderStatusCommand.cs | 3 +- .../Shop/Orders/Entities/Order.cs | 17 ++ .../Orders/Entities/OrderConfiguration.cs | 25 ++ .../Shop/Orders/Entities}/OrderRefund.cs | 4 +- .../Entities}/OrderRefundConfiguration.cs | 12 +- .../Shop/Orders/Models}/Order.cs | 10 +- .../Shop/Orders/Models}/OrderRefund.cs | 2 +- .../Orders/Queries/GetCustomerOrdersQuery.cs | 2 +- .../Orders/Queries/GetOrderRefundQuery.cs | 2 +- .../Orders/Queries/GetOrdersQuery.cs | 2 +- .../Handlers/GetCustomerOrdersQueryHandler.cs | 4 +- .../Handlers/GetOrderRefundQueryHandler.cs | 4 +- .../Queries/Handlers/GetOrdersQueryHandler.cs | 4 +- .../Handlers/RefundCustomerCommandHandler.cs | 5 +- .../UpdateOrderRefundCommandHandler.cs | 5 +- .../Refunds/Commands/RefundCustomerCommand.cs | 2 +- .../Commands/UpdateOrderRefundCommand.cs | 2 +- .../Queries/GetCustomerRefundsQuery.cs | 4 +- .../Orders}/Refunds/Queries/GetRefundQuery.cs | 4 +- .../GetCustomerRefundsQueryHandler.cs | 8 +- .../Queries/Handlers/GetRefundQueryHandler.cs | 7 +- .../20260512065421_Init.Designer.cs | 218 +++++++------- .../Migrations/20260512065421_Init.cs | 274 +++++++++--------- .../Migrations/ShopDbContextModelSnapshot.cs | 216 +++++++------- .../Shop/Postgres}/ShopDbContext.cs | 11 +- .../Shop/Postgres}/ShopDbContextFactory.cs | 2 +- .../Shop/Products/Entities}/Product.cs | 4 +- .../Products/Entities/ProductConfiguration.cs | 17 ++ .../Shop/Products/Entities}/ProductPrice.cs | 4 +- .../Entities}/ProductPriceConfiguration.cs | 11 +- .../Shop/Products/Models/Product.cs | 18 ++ .../Shop/Products/Models}/ProductPrice.cs | 2 +- .../Products/Queries/GetProductPriceQuery.cs | 2 +- .../Products/Queries/GetProductPricesQuery.cs | 2 +- .../Products/Queries/GetProductQuery.cs | 2 +- .../Products/Queries/GetProductsQuery.cs | 2 +- .../Handlers/GetProductPriceQueryHandler.cs | 4 +- .../Handlers/GetProductPricesQueryHandler.cs | 4 +- .../Handlers/GetProductQueryHandler.cs | 4 +- .../Handlers/GetProductsQueryHandler.cs | 4 +- .../Commands/AssignQuoteToOrderCommand.cs | 0 .../AssignQuoteToShoppingCartCommand.cs | 0 .../AssignQuoteToOrderCommandHandler.cs | 2 +- ...AssignQuoteToShoppingCartCommandHandler.cs | 2 +- .../UpdateQuoteStatusCommandHandler.cs | 2 +- .../Commands/UpdateQuoteStatusCommand.cs | 3 +- .../Shop/Quotes/Entities}/Quote.cs | 10 +- .../Quotes/Entities/QuoteConfiguration.cs | 33 +++ .../Shop/Quotes/Models}/Quote.cs | 8 +- .../Quotes/Queries/GetCustomerQuotesQuery.cs | 2 +- .../Quotes/Queries/GetQuoteQuery.cs | 2 +- .../Quotes/Queries/GetQuotesQuery.cs | 2 +- .../Handlers/GetCustomerQuotesQueryHandler.cs | 4 +- .../Queries/Handlers/GetQuoteQueryHandler.cs | 4 +- .../Queries/Handlers/GetQuotesHandler.cs | 4 +- .../Commands/AddItemToShoppingCartCommand.cs | 0 .../AddPackageToShoppingCartCommand.cs | 0 .../Commands/CreateShoppingCartCommand.cs | 0 .../Commands/EmptyShoppingCartCommand.cs | 0 .../AddItemToShoppingCartCommandHandler.cs | 2 +- .../AddPackageToShoppingCartCommandHandler.cs | 2 +- .../CreateShoppingCartCommandHandler.cs | 2 +- .../EmptyShoppingCartCommandHandler.cs | 2 +- ...vePackageFromShoppingCartCommandHandler.cs | 2 +- .../RemoveShoppingCartItemCommandHandler.cs | 2 +- .../UpdateShoppingCartItemCommandHandler.cs | 2 +- .../RemovePackageFromShoppingCartCommand.cs | 0 .../Commands/RemoveShoppingCartItemCommand.cs | 0 .../Commands/UpdateShoppingCartItemCommand.cs | 0 .../ShoppingCarts/Entities}/ShoppingCart.cs | 8 +- .../Entities/ShoppingCartConfiguration.cs | 26 ++ .../Entities}/ShoppingCartItem.cs | 4 +- .../Entities/ShoppingCartItemConfiguration.cs | 30 ++ .../Entities}/ShoppingCartPackage.cs | 4 +- .../ShoppingCartPackageConfiguration.cs | 22 +- .../ShoppingCarts/Models}/ShoppingCart.cs | 6 +- .../ShoppingCarts/Models}/ShoppingCartItem.cs | 2 +- .../Models}/ShoppingCartPackage.cs | 2 +- .../Queries/GetCustomerShoppingCartsQuery.cs | 2 +- .../Queries/GetShoppingCartItemsQuery.cs | 2 +- .../Queries/GetShoppingCartPackagesQuery.cs | 2 +- .../Queries/GetShoppingCartQuery.cs | 2 +- .../GetCustomerShoppingCartsQueryHandler.cs | 4 +- .../GetShoppingCartItemsQueryHandler.cs | 4 +- .../GetShoppingCartPackagesQueryHandler.cs | 4 +- .../Handlers/GetShoppingCartQueryHandler.cs | 4 +- .../Hash/Commands/ComputeHashCommand.cs | 16 - .../Handlers/ComputeHashCommandHandler.cs | 20 -- .../LiteCharms.Infrastructure.csproj | 109 ------- .../ServiceBus/Exchanges/EmailExchange.cs | 37 --- .../ServiceBus/Queues/EmailQueue.cs | 5 - .../ServiceBus/Queues/GeneralQueue.cs | 5 - .../ServiceBus/Queues/SalesQueue.cs | 5 - LiteCharms.Infrastructure/appsettings.json | 3 - LiteCharms.Models/LiteCharms.Models.csproj | 35 --- LiteCharms.Models/Product.cs | 12 - LiteCharmsShared.slnx | 6 +- 231 files changed, 1618 insertions(+), 1408 deletions(-) delete mode 100644 LiteCharms.Abstractions/LiteCharms.Abstractions.csproj delete mode 100644 LiteCharms.Entities/Configuration/OrderConfiguration.cs delete mode 100644 LiteCharms.Entities/Configuration/PackageConfirguration.cs delete mode 100644 LiteCharms.Entities/Configuration/ProductConfiguration.cs delete mode 100644 LiteCharms.Entities/Configuration/QuoteConfiguration.cs delete mode 100644 LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs delete mode 100644 LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs delete mode 100644 LiteCharms.Entities/LiteCharms.Entities.csproj delete mode 100644 LiteCharms.Entities/Order.cs delete mode 100644 LiteCharms.Entities/PackageItem.cs delete mode 100644 LiteCharms.Extensions/Email.cs delete mode 100644 LiteCharms.Extensions/LiteCharms.Extensions.csproj create mode 100644 LiteCharms.Features.Tests/CommonFixture.cs create mode 100644 LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj create mode 100644 LiteCharms.Features.Tests/NotificationsFeatureTests.cs create mode 100644 LiteCharms.Features.Tests/appsettings.json rename {LiteCharms.Abstractions => LiteCharms.Features/Abstractions}/EventBase.cs (65%) rename {LiteCharms.Abstractions => LiteCharms.Features/Abstractions}/IEvent.cs (79%) delete mode 100644 LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs delete mode 100644 LiteCharms.Features/Email/Commands/SendEmailCommand.cs rename {LiteCharms.Models/Configuraton/Email => LiteCharms.Features/Email/Configuration}/Account.cs (67%) rename {LiteCharms.Models/Configuraton/Email => LiteCharms.Features/Email/Configuration}/SmtpSettings.cs (77%) create mode 100644 LiteCharms.Features/Email/EmailService.cs create mode 100644 LiteCharms.Features/Email/Extensions/Constants.cs create mode 100644 LiteCharms.Features/Email/Extensions/EmailTelemetry.cs create mode 100644 LiteCharms.Features/Email/Extensions/Setup.cs create mode 100644 LiteCharms.Features/Email/IEmailService.cs create mode 100644 LiteCharms.Features/Email/Models/Attachment.cs create mode 100644 LiteCharms.Features/Email/Models/Body.cs create mode 100644 LiteCharms.Features/Email/Models/BodyProperties.cs rename {LiteCharms.Models => LiteCharms.Features/Email/Models}/EmailEnquiry.cs (86%) create mode 100644 LiteCharms.Features/Email/Models/Message.cs create mode 100644 LiteCharms.Features/Email/Models/Party.cs create mode 100644 LiteCharms.Features/Email/Models/Response.cs create mode 100644 LiteCharms.Features/Extensions/Constants.cs rename {LiteCharms.Extensions => LiteCharms.Features/Extensions}/EntityModeMappers.cs (100%) create mode 100644 LiteCharms.Features/Extensions/Hash.cs rename {LiteCharms.Extensions => LiteCharms.Features/Extensions}/HealthChecks.cs (100%) rename {LiteCharms.Extensions => LiteCharms.Features/Extensions}/Monitoring.cs (100%) rename {LiteCharms.Extensions => LiteCharms.Features/Extensions}/Postgres.cs (100%) rename {LiteCharms.Extensions => LiteCharms.Features/Extensions}/Quartz.cs (100%) rename {LiteCharms.Extensions => LiteCharms.Features/Extensions}/ServiceBus.cs (100%) rename {LiteCharms.Abstractions => LiteCharms.Features/Extensions}/Timezones.cs (96%) rename {LiteCharms.Infrastructure => LiteCharms.Features}/HealthChecks/PostgresHealthCheck.cs (94%) rename {LiteCharms.Infrastructure => LiteCharms.Features}/HealthChecks/QuartzHealthCheck.cs (93%) create mode 100644 LiteCharms.Features/Mediator/LoggingPipelineBehavior.cs create mode 100644 LiteCharms.Features/Mediator/MediatorTelemetry.cs create mode 100644 LiteCharms.Features/Mediator/TelemetryPipelineBehavior.cs rename {LiteCharms.Abstractions => LiteCharms.Features/Quartz/Abstractions}/IJobOrchestrator.cs (79%) rename {LiteCharms.Infrastructure => LiteCharms.Features}/Quartz/JobOrchestrator.cs (93%) rename {LiteCharms.Infrastructure => LiteCharms.Features}/Quartz/MediatorJob.cs (87%) rename {LiteCharms.Infrastructure => LiteCharms.Features}/Quartz/RetryJobListener.cs (93%) rename {LiteCharms.Abstractions => LiteCharms.Features/ServiceBus/Abstractions}/EventBusQueueBase.cs (73%) rename {LiteCharms.Abstractions => LiteCharms.Features/ServiceBus/Abstractions}/IEventBus.cs (64%) rename {LiteCharms.Abstractions => LiteCharms.Features/ServiceBus/Abstractions}/IEventBusQueue.cs (56%) rename {LiteCharms.Abstractions => LiteCharms.Features/ServiceBus}/Constants.cs (55%) rename {LiteCharms.Infrastructure => LiteCharms.Features}/ServiceBus/EmailServiceBus.cs (70%) create mode 100644 LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs rename {LiteCharms.Infrastructure => LiteCharms.Features}/ServiceBus/Exchanges/GeneralExchange.cs (69%) rename {LiteCharms.Infrastructure => LiteCharms.Features}/ServiceBus/Exchanges/SalesExchange.cs (69%) rename {LiteCharms.Infrastructure => LiteCharms.Features}/ServiceBus/GeneralServiceBus.cs (73%) create mode 100644 LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs create mode 100644 LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs create mode 100644 LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs rename {LiteCharms.Infrastructure => LiteCharms.Features}/ServiceBus/SalesServiceBus.cs (73%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/AddPackageItemsCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/CreatePackageCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/DeletePackageCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/DeletePackageItemsCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs (97%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs (96%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs (96%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs (96%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs (96%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs (95%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/UpdatePackageCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/CartPackages/Commands/UpdatePackageStatusCommand.cs (100%) rename {LiteCharms.Entities => LiteCharms.Features/Shop/CartPackages/Entities}/Package.cs (69%) create mode 100644 LiteCharms.Features/Shop/CartPackages/Entities/PackageConfirguration.cs create mode 100644 LiteCharms.Features/Shop/CartPackages/Entities/PackageItem.cs rename {LiteCharms.Entities/Configuration => LiteCharms.Features/Shop/CartPackages/Entities}/PackageItemConfiguration.cs (51%) rename {LiteCharms.Models => LiteCharms.Features/Shop/CartPackages/Models}/Package.cs (66%) rename {LiteCharms.Models => LiteCharms.Features/Shop/CartPackages/Models}/PackageItem.cs (80%) rename LiteCharms.Features/{ => Shop}/CartPackages/Queries/GetPackageItemsQuery.cs (89%) rename LiteCharms.Features/{ => Shop}/CartPackages/Queries/GetPackageQuery.cs (89%) rename LiteCharms.Features/{ => Shop}/CartPackages/Queries/GetPackagesQuery.cs (94%) rename LiteCharms.Features/{ => Shop}/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs (90%) rename LiteCharms.Features/{ => Shop}/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/Customers/Commands/CreateCustomerCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs (97%) rename LiteCharms.Features/{ => Shop}/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs (97%) rename LiteCharms.Features/{ => Shop}/Customers/Commands/UpdateCustomerCommand.cs (100%) rename {LiteCharms.Entities => LiteCharms.Features/Shop/Customers/Entities}/Customer.cs (58%) rename {LiteCharms.Entities/Configuration => LiteCharms.Features/Shop/Customers/Entities}/CustomerConfiguration.cs (77%) rename {LiteCharms.Models => LiteCharms.Features/Shop/Customers/Models}/Customer.cs (93%) rename LiteCharms.Features/{ => Shop}/Customers/Queries/GetCustomerQuery.cs (89%) rename LiteCharms.Features/{ => Shop}/Customers/Queries/GetCustomersQuery.cs (93%) rename LiteCharms.Features/{ => Shop}/Customers/Queries/Handlers/GetCustomerQueryHandler.cs (90%) rename LiteCharms.Features/{ => Shop}/Customers/Queries/Handlers/GetCustomersQueryHandler.cs (93%) rename {LiteCharms.Models => LiteCharms.Features/Shop}/Enums.cs (76%) rename LiteCharms.Features/{ => Shop}/Leads/Commands/CreateLeadCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/Leads/Commands/Handlers/CreateLeadCommandHandler.cs (95%) rename LiteCharms.Features/{ => Shop}/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs (95%) rename LiteCharms.Features/{ => Shop}/Leads/Commands/UpdateLeadCommand.cs (92%) rename {LiteCharms.Entities => LiteCharms.Features/Shop/Leads/Entities}/Lead.cs (57%) rename {LiteCharms.Entities/Configuration => LiteCharms.Features/Shop/Leads/Entities}/LeadConfiguration.cs (86%) rename {LiteCharms.Models => LiteCharms.Features/Shop/Leads/Models}/Lead.cs (93%) rename LiteCharms.Features/{ => Shop}/Leads/Queries/GetCustomerLeadsQuery.cs (94%) rename LiteCharms.Features/{ => Shop}/Leads/Queries/GetLeadsQuery.cs (93%) rename LiteCharms.Features/{ => Shop}/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/Leads/Queries/Handlers/GetLeadsQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/Notifications/Commands/CreateNotificationCommand.cs (73%) rename LiteCharms.Features/{ => Shop}/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs (92%) rename LiteCharms.Features/{ => Shop}/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs (96%) rename LiteCharms.Features/{ => Shop}/Notifications/Commands/UpdateNotificationCommand.cs (100%) rename {LiteCharms.Entities => LiteCharms.Features/Shop/Notifications/Entities}/Notification.cs (60%) rename {LiteCharms.Entities/Configuration => LiteCharms.Features/Shop/Notifications/Entities}/NotificationConfiguration.cs (88%) rename LiteCharms.Features/{ => Shop}/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs (89%) rename LiteCharms.Features/{ => Shop}/Notifications/Events/ProcessEmailNotificationsEvent.cs (91%) create mode 100644 LiteCharms.Features/Shop/Notifications/INotificationService.cs rename {LiteCharms.Models => LiteCharms.Features/Shop/Notifications/Models}/Notification.cs (93%) create mode 100644 LiteCharms.Features/Shop/Notifications/Models/Records.cs create mode 100644 LiteCharms.Features/Shop/Notifications/NotificationService.cs rename LiteCharms.Features/{ => Shop}/Notifications/Queries/GetNotificationQuery.cs (90%) rename LiteCharms.Features/{ => Shop}/Notifications/Queries/GetNotificationsQuery.cs (93%) rename LiteCharms.Features/{ => Shop}/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs (91%) rename LiteCharms.Features/{ => Shop}/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/Orders/Commands/CreateOrderCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/Orders/Commands/Handlers/CreateOrderCommandHandler.cs (96%) rename LiteCharms.Features/{ => Shop}/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs (95%) rename LiteCharms.Features/{ => Shop}/Orders/Commands/UpdateOrderStatusCommand.cs (93%) create mode 100644 LiteCharms.Features/Shop/Orders/Entities/Order.cs create mode 100644 LiteCharms.Features/Shop/Orders/Entities/OrderConfiguration.cs rename {LiteCharms.Entities => LiteCharms.Features/Shop/Orders/Entities}/OrderRefund.cs (68%) rename {LiteCharms.Entities/Configuration => LiteCharms.Features/Shop/Orders/Entities}/OrderRefundConfiguration.cs (63%) rename {LiteCharms.Models => LiteCharms.Features/Shop/Orders/Models}/Order.cs (64%) rename {LiteCharms.Models => LiteCharms.Features/Shop/Orders/Models}/OrderRefund.cs (81%) rename LiteCharms.Features/{ => Shop}/Orders/Queries/GetCustomerOrdersQuery.cs (90%) rename LiteCharms.Features/{ => Shop}/Orders/Queries/GetOrderRefundQuery.cs (93%) rename LiteCharms.Features/{ => Shop}/Orders/Queries/GetOrdersQuery.cs (93%) rename LiteCharms.Features/{ => Shop}/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs (92%) rename LiteCharms.Features/{ => Shop}/Orders/Queries/Handlers/GetOrdersQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop/Orders}/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs (90%) rename LiteCharms.Features/{ => Shop/Orders}/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs (85%) rename LiteCharms.Features/{ => Shop/Orders}/Refunds/Commands/RefundCustomerCommand.cs (94%) rename LiteCharms.Features/{ => Shop/Orders}/Refunds/Commands/UpdateOrderRefundCommand.cs (92%) rename LiteCharms.Features/{ => Shop/Orders}/Refunds/Queries/GetCustomerRefundsQuery.cs (80%) rename LiteCharms.Features/{ => Shop/Orders}/Refunds/Queries/GetRefundQuery.cs (80%) rename LiteCharms.Features/{ => Shop/Orders}/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs (85%) rename LiteCharms.Features/{ => Shop/Orders}/Refunds/Queries/Handlers/GetRefundQueryHandler.cs (80%) rename LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.Designer.cs => LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.Designer.cs (86%) rename LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.cs => LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.cs (86%) rename {LiteCharms.Infrastructure/Database => LiteCharms.Features/Shop/Postgres}/Migrations/ShopDbContextModelSnapshot.cs (86%) rename {LiteCharms.Infrastructure/Database => LiteCharms.Features/Shop/Postgres}/ShopDbContext.cs (64%) rename {LiteCharms.Infrastructure/Database => LiteCharms.Features/Shop/Postgres}/ShopDbContextFactory.cs (93%) rename {LiteCharms.Entities => LiteCharms.Features/Shop/Products/Entities}/Product.cs (69%) create mode 100644 LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs rename {LiteCharms.Entities => LiteCharms.Features/Shop/Products/Entities}/ProductPrice.cs (69%) rename {LiteCharms.Entities/Configuration => LiteCharms.Features/Shop/Products/Entities}/ProductPriceConfiguration.cs (70%) create mode 100644 LiteCharms.Features/Shop/Products/Models/Product.cs rename {LiteCharms.Models => LiteCharms.Features/Shop/Products/Models}/ProductPrice.cs (85%) rename LiteCharms.Features/{ => Shop}/Products/Queries/GetProductPriceQuery.cs (90%) rename LiteCharms.Features/{ => Shop}/Products/Queries/GetProductPricesQuery.cs (90%) rename LiteCharms.Features/{ => Shop}/Products/Queries/GetProductQuery.cs (89%) rename LiteCharms.Features/{ => Shop}/Products/Queries/GetProductsQuery.cs (89%) rename LiteCharms.Features/{ => Shop}/Products/Queries/Handlers/GetProductPriceQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/Products/Queries/Handlers/GetProductPricesQueryHandler.cs (91%) rename LiteCharms.Features/{ => Shop}/Products/Queries/Handlers/GetProductQueryHandler.cs (90%) rename LiteCharms.Features/{ => Shop}/Products/Queries/Handlers/GetProductsQueryHandler.cs (90%) rename LiteCharms.Features/{ => Shop}/Quotes/Commands/AssignQuoteToOrderCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs (97%) rename LiteCharms.Features/{ => Shop}/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs (96%) rename LiteCharms.Features/{ => Shop}/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs (95%) rename LiteCharms.Features/{ => Shop}/Quotes/Commands/UpdateQuoteStatusCommand.cs (90%) rename {LiteCharms.Entities => LiteCharms.Features/Shop/Quotes/Entities}/Quote.cs (54%) create mode 100644 LiteCharms.Features/Shop/Quotes/Entities/QuoteConfiguration.cs rename {LiteCharms.Models => LiteCharms.Features/Shop/Quotes/Models}/Quote.cs (64%) rename LiteCharms.Features/{ => Shop}/Quotes/Queries/GetCustomerQuotesQuery.cs (90%) rename LiteCharms.Features/{ => Shop}/Quotes/Queries/GetQuoteQuery.cs (89%) rename LiteCharms.Features/{ => Shop}/Quotes/Queries/GetQuotesQuery.cs (93%) rename LiteCharms.Features/{ => Shop}/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs (91%) rename LiteCharms.Features/{ => Shop}/Quotes/Queries/Handlers/GetQuotesHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/CreateShoppingCartCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs (97%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs (97%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs (96%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs (96%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs (97%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs (97%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs (96%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs (100%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs (100%) rename {LiteCharms.Entities => LiteCharms.Features/Shop/ShoppingCarts/Entities}/ShoppingCart.cs (54%) create mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs rename {LiteCharms.Entities => LiteCharms.Features/Shop/ShoppingCarts/Entities}/ShoppingCartItem.cs (61%) create mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs rename {LiteCharms.Entities => LiteCharms.Features/Shop/ShoppingCarts/Entities}/ShoppingCartPackage.cs (69%) rename {LiteCharms.Entities/Configuration => LiteCharms.Features/Shop/ShoppingCarts/Entities}/ShoppingCartPackageConfiguration.cs (54%) rename {LiteCharms.Models => LiteCharms.Features/Shop/ShoppingCarts/Models}/ShoppingCart.cs (64%) rename {LiteCharms.Models => LiteCharms.Features/Shop/ShoppingCarts/Models}/ShoppingCartItem.cs (83%) rename {LiteCharms.Models => LiteCharms.Features/Shop/ShoppingCarts/Models}/ShoppingCartPackage.cs (77%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs (90%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs (90%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs (90%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Queries/GetShoppingCartQuery.cs (90%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs (93%) rename LiteCharms.Features/{ => Shop}/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs (91%) delete mode 100644 LiteCharms.Features/Utilities/Hash/Commands/ComputeHashCommand.cs delete mode 100644 LiteCharms.Features/Utilities/Hash/Commands/Handlers/ComputeHashCommandHandler.cs delete mode 100644 LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj delete mode 100644 LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs delete mode 100644 LiteCharms.Infrastructure/ServiceBus/Queues/EmailQueue.cs delete mode 100644 LiteCharms.Infrastructure/ServiceBus/Queues/GeneralQueue.cs delete mode 100644 LiteCharms.Infrastructure/ServiceBus/Queues/SalesQueue.cs delete mode 100644 LiteCharms.Infrastructure/appsettings.json delete mode 100644 LiteCharms.Models/LiteCharms.Models.csproj delete mode 100644 LiteCharms.Models/Product.cs diff --git a/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj b/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj deleted file mode 100644 index 4074ad4..0000000 --- a/LiteCharms.Abstractions/LiteCharms.Abstractions.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - net10.0 - enable - enable - True - ..\LiteCharms.snk - - - - - LiteCharms.Abstractions - 1.0.20 - Khwezi Mngoma - Lite Charms (PTY) Ltd - Shared abstractions for Lite Charms applications. - https://gitea.khongisa.co.za/litecharms/components - https://gitea.khongisa.co.za/litecharms/components.git - git - LICENSE - utility;dotnet - icon.png - - - - - - - - - - - - - - - - - diff --git a/LiteCharms.Entities/Configuration/OrderConfiguration.cs b/LiteCharms.Entities/Configuration/OrderConfiguration.cs deleted file mode 100644 index 138e136..0000000 --- a/LiteCharms.Entities/Configuration/OrderConfiguration.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class OrderConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(Order)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); - builder.Property(f => f.CustomerId).IsRequired(); - builder.Property(f => f.QuoteId).IsRequired(false); - builder.Property(f => f.RefundId).IsRequired(false); - builder.Property(f => f.ShoppingCartId).IsRequired(); - builder.Property(f => f.Status).HasConversion().IsRequired(); - builder.Property(f => f.Requirements).HasColumnType("jsonb").IsRequired(false); - builder.Property(f => f.Notes).HasColumnType("jsonb").IsRequired(false); - builder.Property(f => f.Terms).HasColumnType("jsonb").IsRequired(false); - builder.Property(f => f.DepositRequired); - - builder.HasOne(f => f.Quote) - .WithOne(f => f.Order) - .HasForeignKey(f => f.QuoteId) - .OnDelete(DeleteBehavior.Restrict); - - builder.HasOne(f => f.Customer) - .WithMany(f => f.Orders) - .HasForeignKey(f => f.CustomerId) - .OnDelete(DeleteBehavior.Restrict); - } -} diff --git a/LiteCharms.Entities/Configuration/PackageConfirguration.cs b/LiteCharms.Entities/Configuration/PackageConfirguration.cs deleted file mode 100644 index 71aa4fc..0000000 --- a/LiteCharms.Entities/Configuration/PackageConfirguration.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class PackageConfirguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(Package)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); - builder.Property(f => f.Name).IsRequired(); - builder.Property(f => f.Description).IsRequired(); - builder.Property(f => f.Active); - } -} diff --git a/LiteCharms.Entities/Configuration/ProductConfiguration.cs b/LiteCharms.Entities/Configuration/ProductConfiguration.cs deleted file mode 100644 index 3b5ca6d..0000000 --- a/LiteCharms.Entities/Configuration/ProductConfiguration.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class ProductConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(Product)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.Name).IsRequired(); - builder.Property(f => f.Description).IsRequired(); - builder.Property(f => f.Active).HasDefaultValue(true); - } -} diff --git a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs b/LiteCharms.Entities/Configuration/QuoteConfiguration.cs deleted file mode 100644 index d48a768..0000000 --- a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class QuoteConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(Quote)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); - builder.Property(f => f.ExpiredAt).IsRequired(false); - builder.Property(f => f.CustomerId).IsRequired(); - builder.Property(f => f.Status).IsRequired().HasConversion(); - builder.Property(f => f.ShoppingCartId).IsRequired(); - builder.Property(f => f.Reason).IsRequired(false); - - builder.HasOne(f => f.Customer) - .WithMany() - .HasForeignKey(f => f.CustomerId) - .OnDelete(DeleteBehavior.Cascade); - } -} diff --git a/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs b/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs deleted file mode 100644 index 109fd99..0000000 --- a/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class ShoppingCartConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(ShoppingCart)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); - builder.Property(f => f.CustomerId).IsRequired(false); - builder.Property(f => f.OrderId).IsRequired(false); - builder.Property(f => f.QuoteId).IsRequired(false); - - builder.HasOne(f => f.Customer) - .WithMany(c => c.ShoppingCarts) - .HasForeignKey(f => f.CustomerId) - .OnDelete(DeleteBehavior.NoAction); - - builder.HasOne(f => f.Order) - .WithOne(o => o.ShoppingCart) - .HasForeignKey(o => o.ShoppingCartId) - .OnDelete(DeleteBehavior.NoAction); - - builder.HasOne(f => f.Quote) - .WithOne(o => o.ShoppingCart) - .HasForeignKey(o => o.ShoppingCartId) - .OnDelete(DeleteBehavior.NoAction); - } -} diff --git a/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs b/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs deleted file mode 100644 index 948f988..0000000 --- a/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class ShoppingCartItemConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(ShoppingCartItem)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); - builder.Property(f => f.Quantity).IsRequired().HasDefaultValue(1); - builder.Property(f => f.ProductPriceId).IsRequired(); - - builder.HasOne(f => f.ProductPrice) - .WithMany() - .HasForeignKey(f => f.ProductPriceId) - .OnDelete(DeleteBehavior.NoAction); - - builder.HasOne(f => f.ShoppingCart) - .WithMany(f => f.ShoppingCartItems) - .HasForeignKey(f => f.ShoppingCartId) - .OnDelete(DeleteBehavior.NoAction); - } -} diff --git a/LiteCharms.Entities/LiteCharms.Entities.csproj b/LiteCharms.Entities/LiteCharms.Entities.csproj deleted file mode 100644 index b32a09d..0000000 --- a/LiteCharms.Entities/LiteCharms.Entities.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - - net10.0 - enable - enable - True - ..\LiteCharms.snk - - - - - LiteCharms.Entities - 1.0.20 - Khwezi Mngoma - Lite Charms (PTY) Ltd - Shared entities for Lite Charms applications. - https://gitea.khongisa.co.za/litecharms/components - https://gitea.khongisa.co.za/litecharms/components.git - git - LICENSE - utility;dotnet - icon.png - - - - - - - - - - - - - - - - - - - - - - diff --git a/LiteCharms.Entities/Order.cs b/LiteCharms.Entities/Order.cs deleted file mode 100644 index 5f7d5dc..0000000 --- a/LiteCharms.Entities/Order.cs +++ /dev/null @@ -1,15 +0,0 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; - -[EntityTypeConfiguration] -public class Order : Models.Order -{ - public virtual OrderRefund? Refund { get; set; } - - public virtual Customer? Customer { get; set; } - - public virtual Quote? Quote { get; set; } - - public virtual ShoppingCart? ShoppingCart { get; set; } -} diff --git a/LiteCharms.Entities/PackageItem.cs b/LiteCharms.Entities/PackageItem.cs deleted file mode 100644 index 986fc75..0000000 --- a/LiteCharms.Entities/PackageItem.cs +++ /dev/null @@ -1,9 +0,0 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; - -[EntityTypeConfiguration] -public class PackageItem : Models.PackageItem -{ - public virtual Package? Package { get; set; } -} diff --git a/LiteCharms.Extensions/Email.cs b/LiteCharms.Extensions/Email.cs deleted file mode 100644 index e081f4e..0000000 --- a/LiteCharms.Extensions/Email.cs +++ /dev/null @@ -1,13 +0,0 @@ -using LiteCharms.Models.Configuraton.Email; - -namespace LiteCharms.Extensions; - -public static class Email -{ - public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration.GetSection("Email")); - - return services; - } -} diff --git a/LiteCharms.Extensions/LiteCharms.Extensions.csproj b/LiteCharms.Extensions/LiteCharms.Extensions.csproj deleted file mode 100644 index 3f531b7..0000000 --- a/LiteCharms.Extensions/LiteCharms.Extensions.csproj +++ /dev/null @@ -1,113 +0,0 @@ - - - - net10.0 - enable - enable - True - ..\LiteCharms.snk - true - - - - - $(NoWarn);MA0004 - - $(NoWarn);AD0001 - true - $(NoWarn);IL2080;IL2065;IL2075;IL2087;IL2057;IL2060;IL2070;IL2067;IL2072;IL2026;IL2104 - $(NoWarn);IL2110;IL2111 - - - - - LiteCharms.Extensions - 1.0.20 - Khwezi Mngoma - Lite Charms (PTY) Ltd - Extension components for Lite Charms applications. - https://gitea.khongisa.co.za/litecharms/components - https://gitea.khongisa.co.za/litecharms/components.git - git - LICENSE - utility;dotnet - icon.png - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LiteCharms.Features.Tests/CommonFixture.cs b/LiteCharms.Features.Tests/CommonFixture.cs new file mode 100644 index 0000000..b237b8f --- /dev/null +++ b/LiteCharms.Features.Tests/CommonFixture.cs @@ -0,0 +1,34 @@ +using LiteCharms.Extensions; + +namespace LiteCharms.Features.Tests; + +public class CommonFixture : IDisposable +{ + public IConfiguration Configuration { get; set; } + + public IServiceProvider Services { get; set; } + + public IMediator Mediator { get; set; } + + public CommonFixture() + { + Configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .AddUserSecrets() + .AddEnvironmentVariables() + .Build(); + + Services = new ServiceCollection() + .AddMediator() + .AddLogging() + .AddEmailServiceBus() + .AddShopDatabase(Configuration) + .AddEmailServices(Configuration) + .BuildServiceProvider(); + + Mediator = Services.GetRequiredService(); + } + + public void Dispose() { } +} diff --git a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj new file mode 100644 index 0000000..7070851 --- /dev/null +++ b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj @@ -0,0 +1,50 @@ + + + + net10.0 + enable + enable + false + 62fa604a-1340-4edb-9ddd-3305fcf46fca + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs new file mode 100644 index 0000000..53b8154 --- /dev/null +++ b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs @@ -0,0 +1,19 @@ +using LiteCharms.Features.Notifications.Commands; + +namespace LiteCharms.Features.Tests; + +public class NotificationsFeatureTests(CommonFixture fixture) : IClassFixture +{ + [Fact] + public async Task CreateNotificationCommand_ShouldSucceed() + { + var command = CreateNotification.Create(Models.NotificationDirection.Outgoing, "UnitTest", "khwezi@mngoma.co.za", + "CreateNotificationCommand_ShouldSucceed Test", "Test Message", Models.NotificationPlatforms.Email, Models.Priorities.Medium, + "Khngisa Shop - Test", "shop@litecharms.co.za", Guid.NewGuid().ToString(), Models.CorrelationIdTypes.None, + true, false); + + var result = await fixture.Mediator.Send(command); + + Assert.True(result.IsSuccess); + } +} diff --git a/LiteCharms.Features.Tests/appsettings.json b/LiteCharms.Features.Tests/appsettings.json new file mode 100644 index 0000000..aec5c2e --- /dev/null +++ b/LiteCharms.Features.Tests/appsettings.json @@ -0,0 +1,22 @@ +{ + "Email": { + "Credentials": { + "Username": "shop@litecharms.co.za" + }, + "Port": 465, + "Host": "mail.litecharms.co.za", + "UseSsl": true + }, + "Monitoring": { + "ApiKey": "", + "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", + "ServiceName": "LiteCharms.LeadGenerator" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/LiteCharms.Abstractions/EventBase.cs b/LiteCharms.Features/Abstractions/EventBase.cs similarity index 65% rename from LiteCharms.Abstractions/EventBase.cs rename to LiteCharms.Features/Abstractions/EventBase.cs index 6c11736..32def99 100644 --- a/LiteCharms.Abstractions/EventBase.cs +++ b/LiteCharms.Features/Abstractions/EventBase.cs @@ -1,6 +1,7 @@ -using static LiteCharms.Abstractions.Timezones; +using LiteCharms.Features.Extensions; +using static LiteCharms.Features.Extensions.Timezones; -namespace LiteCharms.Abstractions; +namespace LiteCharms.Features.Abstractions; public abstract class EventBase { diff --git a/LiteCharms.Abstractions/IEvent.cs b/LiteCharms.Features/Abstractions/IEvent.cs similarity index 79% rename from LiteCharms.Abstractions/IEvent.cs rename to LiteCharms.Features/Abstractions/IEvent.cs index 08bc091..366ad86 100644 --- a/LiteCharms.Abstractions/IEvent.cs +++ b/LiteCharms.Features/Abstractions/IEvent.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Abstractions; +namespace LiteCharms.Features.Abstractions; public interface IEvent : INotification { diff --git a/LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs b/LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs deleted file mode 100644 index 30f86dc..0000000 --- a/LiteCharms.Features/Email/Commands/Handlers/SendEmailCommandHandler.cs +++ /dev/null @@ -1,61 +0,0 @@ -using LiteCharms.Features.Email.Commands; -using LiteCharms.Models.Configuraton.Email; - -namespace LiteCharms.Features.Email.Commands.Handlers; - -public class SendEmailCommandHandler(IOptions smtpOptions) : IRequestHandler -{ - public async ValueTask Handle(SendEmailCommand request, CancellationToken cancellationToken) - { - try - { - var settings = smtpOptions.Value; - - if(settings == null) - return Result.Fail(new Error("SMTP settings are not configured.")); - - if(settings.Credentials == null) - return Result.Fail(new Error("SMTP credentials are not configured.")); - - if(string.IsNullOrWhiteSpace(settings?.Credentials.Username) || string.IsNullOrWhiteSpace(settings.Credentials.Password)) - return Result.Fail(new Error("SMTP credentials are incomplete.")); - - if(string.IsNullOrWhiteSpace(settings.Host) || settings.Port == 0) - return Result.Fail(new Error("SMTP host and port must be configured.")); - - var message = new MimeMessage(); - message.From.Add(new MailboxAddress(request.SenderName, request.From!)); - message.To.Add(new MailboxAddress(request.RecipientName, request.To!)); - message.Subject = request.Subject!; - - var bodyBuilder = new BodyBuilder(); - - if(request.Attachment?.Length > 0 && !string.IsNullOrEmpty(request.AttachmentFileName)) - bodyBuilder.Attachments.Add(request.AttachmentFileName!, request.Attachment!, cancellationToken); - - if (!request.IsHtml) bodyBuilder.TextBody = request.Message; - if (request.IsHtml) bodyBuilder.HtmlBody = request.Message; - - message.Body = bodyBuilder.ToMessageBody(); - - using var client = new SmtpClient(); - - await client.ConnectAsync(settings.Host!, settings.Port, settings.UseSsl, cancellationToken); - await client.AuthenticateAsync(settings.Credentials!.Username!, settings.Credentials.Password!, cancellationToken); - - var response = await client.SendAsync(message, cancellationToken); - - bool emailSent = response.Contains("OK", StringComparison.InvariantCultureIgnoreCase); - - await client.DisconnectAsync(true, cancellationToken); - - return emailSent - ? Result.Ok() - : Result.Fail(new Error("Failed to send email. SMTP response: " + response)); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Email/Commands/SendEmailCommand.cs b/LiteCharms.Features/Email/Commands/SendEmailCommand.cs deleted file mode 100644 index 972e496..0000000 --- a/LiteCharms.Features/Email/Commands/SendEmailCommand.cs +++ /dev/null @@ -1,79 +0,0 @@ -namespace LiteCharms.Features.Email.Commands; - -public class SendEmailCommand : IRequest -{ - public string? From { get; set; } - - public string? SenderName { get; set; } - - public string? To { get; set; } - - public string? RecipientName { get; set; } - - public string? Subject { get; set; } - - public string? Message { get; set; } - - public bool IsHtml { get; set; } - - public Stream? Attachment { get; set; } - - public string? AttachmentFileName { get; set; } - - private SendEmailCommand(string from, string senderName, string to, string recipientName, string subject, string message, bool isHtml = false, Stream? attachment = null, string? attachmentFileName = null) - { - From = from; - To = to; - Subject = subject; - Message = message; - IsHtml = isHtml; - Attachment = attachment; - AttachmentFileName = attachmentFileName; - SenderName = senderName; - RecipientName = recipientName; - } - - public static SendEmailCommand Create(string from, string senderName, string to, string recipientName, string subject, string message, bool isHtml = false, Stream? attachment = null, string? attachmentFileName = null) - { - if (string.IsNullOrWhiteSpace(from)) - throw new ArgumentException("From address is required."); - - if (string.IsNullOrWhiteSpace(senderName)) - throw new ArgumentException("Sender name is required."); - - if (!string.IsNullOrWhiteSpace(senderName) && senderName?.Length > 255) - throw new ArgumentException("Sender name cannot exceed 255 characters."); - - if (string.IsNullOrWhiteSpace(to)) - throw new ArgumentException("To address is required."); - - if (string.IsNullOrWhiteSpace(recipientName)) - throw new ArgumentException("Recipient name is required."); - - if (!string.IsNullOrWhiteSpace(recipientName) && recipientName?.Length > 255) - throw new ArgumentException("Recipient name cannot exceed 255 characters."); - - if (string.IsNullOrWhiteSpace(subject)) - throw new ArgumentException("Subject is required."); - - if (!string.IsNullOrWhiteSpace(subject) && subject?.Length > 2048) - throw new ArgumentException("Subject cannot exceed 2048 characters."); - - if (string.IsNullOrWhiteSpace(message)) - throw new ArgumentException("Message is required."); - - if (message.Length > 10485760) - throw new ArgumentException("Message cannot exceed 10 MB."); - - if (attachment != null && string.IsNullOrWhiteSpace(attachmentFileName)) - throw new ArgumentException("Attachment file name must be provided when an attachment is included."); - - if (attachment is not null && attachment.Length > 10485760) - throw new ArgumentException("Attachment cannot exceed 10 MB."); - - if (!string.IsNullOrWhiteSpace(attachmentFileName) && attachmentFileName.Length > 255) - throw new ArgumentException("Attachment file name cannot exceed 255 characters."); - - return new(from, senderName!, to, recipientName!, subject!, message, isHtml, attachment, attachmentFileName); - } -} diff --git a/LiteCharms.Models/Configuraton/Email/Account.cs b/LiteCharms.Features/Email/Configuration/Account.cs similarity index 67% rename from LiteCharms.Models/Configuraton/Email/Account.cs rename to LiteCharms.Features/Email/Configuration/Account.cs index 96424ce..e0c65e7 100644 --- a/LiteCharms.Models/Configuraton/Email/Account.cs +++ b/LiteCharms.Features/Email/Configuration/Account.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models.Configuraton.Email; +namespace LiteCharms.Features.Email.Configuration; public class Account { diff --git a/LiteCharms.Models/Configuraton/Email/SmtpSettings.cs b/LiteCharms.Features/Email/Configuration/SmtpSettings.cs similarity index 77% rename from LiteCharms.Models/Configuraton/Email/SmtpSettings.cs rename to LiteCharms.Features/Email/Configuration/SmtpSettings.cs index c44fbbe..78798f2 100644 --- a/LiteCharms.Models/Configuraton/Email/SmtpSettings.cs +++ b/LiteCharms.Features/Email/Configuration/SmtpSettings.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models.Configuraton.Email; +namespace LiteCharms.Features.Email.Configuration; public class SmtpSettings { diff --git a/LiteCharms.Features/Email/EmailService.cs b/LiteCharms.Features/Email/EmailService.cs new file mode 100644 index 0000000..8cb3a82 --- /dev/null +++ b/LiteCharms.Features/Email/EmailService.cs @@ -0,0 +1,192 @@ +using LiteCharms.Features.Email.Configuration; +using LiteCharms.Features.Email.Extensions; +using LiteCharms.Features.Email.Models; +using LiteCharms.Features.Shop; + +namespace LiteCharms.Features.Email; + +public class EmailService(IOptions options) : IEmailService +{ + private readonly SmtpSettings settings = options.Value; + private readonly SmtpClient client = new(); + private readonly int sendMaxCount = 10; + private int sendCount = 0; + + public EmailStatuses Status { get; private set; } = EmailStatuses.Disconnected; + + public async Task> SendEmailAsync(Message message, CancellationToken cancellationToken = default) + { + using var activity = EmailTelemetry.Source.StartActivity("Email Send"); + activity?.SetTag("email.recipient", message.Recipient?.Address); + + try + { + if (Status != EmailStatuses.Connected) + { + activity?.SetStatus(ActivityStatusCode.Error, "Disconnected"); + + return Result.Fail("Smtp service is disconnected."); + } + + var email = new MimeMessage(); + email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!)); + email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!)); + email.Subject = message.Subject!; + + var bodyBuilder = new BodyBuilder(); + + foreach (var attachment in message.Body?.Attachments!) + bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken); + + if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message; + if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message; + + email.Body = bodyBuilder.ToMessageBody(); + + var response = await client.SendAsync(email, cancellationToken); + + bool emailSent = response.Contains("OK", StringComparison.InvariantCultureIgnoreCase); + + message.Dispose(); + + Interlocked.Increment(ref sendCount); + + if (sendCount % sendMaxCount == 0) + { + using var delayActivity = EmailTelemetry.Source.StartActivity("Rate Limit Pause"); + + sendCount = 0; + + await Task.Delay(1000, cancellationToken); + } + + if (emailSent) + { + EmailTelemetry.EmailsSent.Add(1, new TagList { { "host", settings.Host } }); + + return Result.Ok(Response.Create(EmailStatuses.Success)); + } + + await DisconnectAsync(cancellationToken); + + if (response.Contains("421")) + { + Status = EmailStatuses.TooManyConnections; + + return Result.Fail(response); + } + + if (response.Contains("451")) + { + Status = EmailStatuses.ConnectionAborted; + + return Result.Fail(response); + } + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } }); + + Status = EmailStatuses.Disconnected; + + return Result.Fail("General error, disconnected"); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task> ConnectAsync(CancellationToken cancellationToken = default) + { + using var activity = EmailTelemetry.Source.StartActivity("Email Connect"); + activity?.SetTag("email.smtp.connect", settings.Host); + + try + { + if (Status is EmailStatuses.Connected) return Result.Ok(Response.Create(Status)); + + await client.ConnectAsync(settings.Host!, settings.Port, settings.UseSsl, cancellationToken); + await client.AuthenticateAsync(settings.Credentials!.Username!, settings.Credentials.Password!, cancellationToken); + + Status = EmailStatuses.Connected; + + activity?.SetStatus(ActivityStatusCode.Ok, "Connected"); + + return Result.Ok(Response.Create(Status)); + } + catch (MailKit.ProtocolException ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.ProtocolError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + catch (Exception ex) when (ex is MailKit.Security.SslHandshakeException || ex is MailKit.Security.AuthenticationException) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.AuthenticationError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.GeneralError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task DisconnectAsync(CancellationToken cancellationToken = default) + { + using var activity = EmailTelemetry.Source.StartActivity("Email Disconnect"); + activity?.SetTag("email.smtp.disconnect", settings.Host); + + try + { + if (Status is EmailStatuses.Disconnected) return Result.Ok(); + + await client.DisconnectAsync(true, cancellationToken); + + activity?.SetStatus(ActivityStatusCode.Ok, "Disconnected"); + + Status = EmailStatuses.Disconnected; + + return Result.Ok(); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.GeneralError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public void Dispose() + { + client.Dispose(); + + GC.SuppressFinalize(this); + } +} diff --git a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs index 9c19500..f3e703f 100644 --- a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs +++ b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs @@ -1,18 +1,22 @@ using LiteCharms.Features.Notifications.Commands; -using static LiteCharms.Abstractions.Constants; +using LiteCharms.Features.Shop; +using static LiteCharms.Features.Email.Extensions.Constants; namespace LiteCharms.Features.Email.Events.Handlers; +// TODO: Inject the INotificationService public class SendShopEmailEnquiryEventHandler(ISender mediator) : INotificationHandler { public async ValueTask Handle(SendShopEmailEnquiryEvent notification, CancellationToken cancellationToken) { - var command = CreateNotificationCommand.Create(Models.NotificationDirection.Outgoing, notification.SenderName!, - notification.SenderAddress!, notification.Subject!, notification.Message!, Models.NotificationPlatforms.Email, + // TODO: Refactor this to use the NotificationService + var command = CreateNotification.Create(NotificationDirection.Outgoing, notification.SenderName!, + notification.SenderAddress!, notification.Subject!, notification.Message!, NotificationPlatforms.Email, notification.Priority, ShopEmailFromName, ShopEmailFromAddress, Guid.CreateVersion7().ToString(), - Models.CorrelationIdTypes.None, isInternal: true, isHtml: false); + CorrelationIdTypes.None, isInternal: true, isHtml: false); + // TODO: Remove, deprecated await mediator.Send(command, cancellationToken); } } diff --git a/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs b/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs index 07e3830..3377b44 100644 --- a/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs +++ b/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs @@ -1,5 +1,5 @@ -using LiteCharms.Abstractions; -using LiteCharms.Models; +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Shop; namespace LiteCharms.Features.Email.Events; diff --git a/LiteCharms.Features/Email/Extensions/Constants.cs b/LiteCharms.Features/Email/Extensions/Constants.cs new file mode 100644 index 0000000..1d02879 --- /dev/null +++ b/LiteCharms.Features/Email/Extensions/Constants.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.Email.Extensions; + +public static class Constants +{ + public const string ShopSchedulerName = "shop"; + public const string ShopEmailFromName = "Khongisa Shop"; + public const string ShopEmailFromAddress = "shop@litecharms.co.za"; +} diff --git a/LiteCharms.Features/Email/Extensions/EmailTelemetry.cs b/LiteCharms.Features/Email/Extensions/EmailTelemetry.cs new file mode 100644 index 0000000..48a9a4a --- /dev/null +++ b/LiteCharms.Features/Email/Extensions/EmailTelemetry.cs @@ -0,0 +1,9 @@ +namespace LiteCharms.Features.Email.Extensions; + +public static class EmailTelemetry +{ + public static readonly ActivitySource Source = new("LiteCharms.EmailService"); + public static readonly Meter Meter = new("LiteCharms.EmailService"); + public static readonly Counter EmailsSent = Meter.CreateCounter("emails_sent_total", "count", "Total successful emails sent"); + public static readonly Counter EmailsFailed = Meter.CreateCounter("emails_failed_total", "count", "Total failed email attempts"); +} diff --git a/LiteCharms.Features/Email/Extensions/Setup.cs b/LiteCharms.Features/Email/Extensions/Setup.cs new file mode 100644 index 0000000..f937dda --- /dev/null +++ b/LiteCharms.Features/Email/Extensions/Setup.cs @@ -0,0 +1,19 @@ +using LiteCharms.Features.Email.Configuration; + +namespace LiteCharms.Features.Email.Extensions; + +public static class Setup +{ + public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("Email")); + + services.AddSingleton(); + + services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddSource("LiteCharms.EmailService")) + .WithMetrics(metrics => metrics.AddMeter("LiteCharms.EmailService")); + + return services; + } +} diff --git a/LiteCharms.Features/Email/IEmailService.cs b/LiteCharms.Features/Email/IEmailService.cs new file mode 100644 index 0000000..bda6a6f --- /dev/null +++ b/LiteCharms.Features/Email/IEmailService.cs @@ -0,0 +1,15 @@ +using LiteCharms.Features.Email.Models; +using LiteCharms.Features.Shop; + +namespace LiteCharms.Features.Email; + +public interface IEmailService : IDisposable +{ + EmailStatuses Status { get; } + + Task> SendEmailAsync(Message message, CancellationToken cancellationToken = default); + + Task> ConnectAsync(CancellationToken cancellationToken = default); + + Task DisconnectAsync(CancellationToken cancellationToken = default); +} diff --git a/LiteCharms.Features/Email/Models/Attachment.cs b/LiteCharms.Features/Email/Models/Attachment.cs new file mode 100644 index 0000000..6558058 --- /dev/null +++ b/LiteCharms.Features/Email/Models/Attachment.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.Email.Models; + +public class Attachment +{ + public string? Name { get; set; } + + public Stream? FileStream { get; set; } +} diff --git a/LiteCharms.Features/Email/Models/Body.cs b/LiteCharms.Features/Email/Models/Body.cs new file mode 100644 index 0000000..99156d8 --- /dev/null +++ b/LiteCharms.Features/Email/Models/Body.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.Email.Models; + +public class Body : IDisposable +{ + public string? Message { get; set; } + + public ReadOnlyCollection? Attachments { get; set; } + + public BodyProperties Properties { get; set; } = new(); + + public void Dispose() + { + if (Attachments is null) return; + + foreach (var attachment in Attachments!) + { + if (attachment is not null) + { + attachment.FileStream!.Close(); + attachment.FileStream!.Dispose(); + } + } + + GC.SuppressFinalize(this); + } +} diff --git a/LiteCharms.Features/Email/Models/BodyProperties.cs b/LiteCharms.Features/Email/Models/BodyProperties.cs new file mode 100644 index 0000000..7bdfa01 --- /dev/null +++ b/LiteCharms.Features/Email/Models/BodyProperties.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.Email.Models; + +public class BodyProperties +{ + public bool IsHtml { get; set; } + + public bool HasAttachments { get; set; } +} diff --git a/LiteCharms.Models/EmailEnquiry.cs b/LiteCharms.Features/Email/Models/EmailEnquiry.cs similarity index 86% rename from LiteCharms.Models/EmailEnquiry.cs rename to LiteCharms.Features/Email/Models/EmailEnquiry.cs index a858328..97c2fe3 100644 --- a/LiteCharms.Models/EmailEnquiry.cs +++ b/LiteCharms.Features/Email/Models/EmailEnquiry.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Models; +using System.ComponentModel.DataAnnotations; + +namespace LiteCharms.Features.Email.Models; public sealed class EmailEnquiry { diff --git a/LiteCharms.Features/Email/Models/Message.cs b/LiteCharms.Features/Email/Models/Message.cs new file mode 100644 index 0000000..7432545 --- /dev/null +++ b/LiteCharms.Features/Email/Models/Message.cs @@ -0,0 +1,19 @@ +namespace LiteCharms.Features.Email.Models; + +public class Message : IDisposable +{ + public Party? Sender { get; set; } + + public Party? Recipient { get; set; } + + public string? Subject { get; set; } + + public Body? Body { get; set; } + + public void Dispose() + { + Body?.Dispose(); + + GC.SuppressFinalize(this); + } +} diff --git a/LiteCharms.Features/Email/Models/Party.cs b/LiteCharms.Features/Email/Models/Party.cs new file mode 100644 index 0000000..65c2e85 --- /dev/null +++ b/LiteCharms.Features/Email/Models/Party.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.Email.Models; + +public class Party +{ + public string? Name { get; set; } + + public string? Address { get; set; } +} diff --git a/LiteCharms.Features/Email/Models/Response.cs b/LiteCharms.Features/Email/Models/Response.cs new file mode 100644 index 0000000..4474d9e --- /dev/null +++ b/LiteCharms.Features/Email/Models/Response.cs @@ -0,0 +1,22 @@ +using LiteCharms.Features.Shop; + +namespace LiteCharms.Features.Email.Models; + +public class Response +{ + public int Code { get; set; } + + public string? Error { get; set; } + + public EmailStatuses Status { get; set; } + + private Response(EmailStatuses status, int code = 0, string? error = null) + { + Status = status; + Code = code; + Error = error; + } + + public static Response Create(EmailStatuses status, int code = 0, string? error = null) => + new(status, code, error); +} diff --git a/LiteCharms.Features/Extensions/Constants.cs b/LiteCharms.Features/Extensions/Constants.cs new file mode 100644 index 0000000..b9d11e1 --- /dev/null +++ b/LiteCharms.Features/Extensions/Constants.cs @@ -0,0 +1,5 @@ +namespace LiteCharms.Features.Extensions; + +public static class Constants +{ +} diff --git a/LiteCharms.Extensions/EntityModeMappers.cs b/LiteCharms.Features/Extensions/EntityModeMappers.cs similarity index 100% rename from LiteCharms.Extensions/EntityModeMappers.cs rename to LiteCharms.Features/Extensions/EntityModeMappers.cs diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs new file mode 100644 index 0000000..ab76ecc --- /dev/null +++ b/LiteCharms.Features/Extensions/Hash.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.Extensions; + +public static class Hash +{ + public static Func GenerateSha256HashString = (input) => + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!))); +} diff --git a/LiteCharms.Extensions/HealthChecks.cs b/LiteCharms.Features/Extensions/HealthChecks.cs similarity index 100% rename from LiteCharms.Extensions/HealthChecks.cs rename to LiteCharms.Features/Extensions/HealthChecks.cs diff --git a/LiteCharms.Extensions/Monitoring.cs b/LiteCharms.Features/Extensions/Monitoring.cs similarity index 100% rename from LiteCharms.Extensions/Monitoring.cs rename to LiteCharms.Features/Extensions/Monitoring.cs diff --git a/LiteCharms.Extensions/Postgres.cs b/LiteCharms.Features/Extensions/Postgres.cs similarity index 100% rename from LiteCharms.Extensions/Postgres.cs rename to LiteCharms.Features/Extensions/Postgres.cs diff --git a/LiteCharms.Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs similarity index 100% rename from LiteCharms.Extensions/Quartz.cs rename to LiteCharms.Features/Extensions/Quartz.cs diff --git a/LiteCharms.Extensions/ServiceBus.cs b/LiteCharms.Features/Extensions/ServiceBus.cs similarity index 100% rename from LiteCharms.Extensions/ServiceBus.cs rename to LiteCharms.Features/Extensions/ServiceBus.cs diff --git a/LiteCharms.Abstractions/Timezones.cs b/LiteCharms.Features/Extensions/Timezones.cs similarity index 96% rename from LiteCharms.Abstractions/Timezones.cs rename to LiteCharms.Features/Extensions/Timezones.cs index 5457130..bbddd5d 100644 --- a/LiteCharms.Abstractions/Timezones.cs +++ b/LiteCharms.Features/Extensions/Timezones.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Abstractions; +namespace LiteCharms.Features.Extensions; public static class Timezones { diff --git a/LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs b/LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs similarity index 94% rename from LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs rename to LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs index 9cb5f03..377da08 100644 --- a/LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs +++ b/LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Infrastructure.HealthChecks; +namespace LiteCharms.Features.HealthChecks; public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck { diff --git a/LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs b/LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs similarity index 93% rename from LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs rename to LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs index d1a8bd0..59eb397 100644 --- a/LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs +++ b/LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Infrastructure.HealthChecks; +namespace LiteCharms.Features.HealthChecks; public class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck { diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index f541547..3d6552b 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -28,6 +28,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + @@ -50,16 +130,17 @@ + - - + + + + + + + - - - - - diff --git a/LiteCharms.Features/Mediator/LoggingPipelineBehavior.cs b/LiteCharms.Features/Mediator/LoggingPipelineBehavior.cs new file mode 100644 index 0000000..2bf0410 --- /dev/null +++ b/LiteCharms.Features/Mediator/LoggingPipelineBehavior.cs @@ -0,0 +1,29 @@ +namespace LiteCharms.Features.Mediator; + +public sealed class LoggingPipelineBehavior(ILogger> logger) : + IPipelineBehavior + where TRequest : IRequest + where TResponse : ResultBase, new() +{ + public async ValueTask Handle(TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + TResponse? response = await next(message, cancellationToken); + + if (response is null) + logger.LogCritical("{Request} {TypeName} was returned as null", typeof(TRequest).Name, typeof(TRequest).Name); + + if(response?.IsFailed == true || response?.Errors?.Any() == true) + { + foreach (var error in response.Errors) + { + if (!string.IsNullOrWhiteSpace(error.Message)) + logger.LogWarning("{Request} {Error}", typeof(TRequest).Name, error.Message); + + if (error?.Reasons?.Count > 0) + error.Reasons.ForEach(r => logger.LogError("{Request} {Reason}", typeof(TRequest).Name, r.ToString())); + } + } + + return response; + } +} diff --git a/LiteCharms.Features/Mediator/MediatorTelemetry.cs b/LiteCharms.Features/Mediator/MediatorTelemetry.cs new file mode 100644 index 0000000..00493c1 --- /dev/null +++ b/LiteCharms.Features/Mediator/MediatorTelemetry.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.Mediator; + +public static class MediatorTelemetry +{ + public const string ServiceName = "LiteCharms.Mediator"; + + public static readonly ActivitySource Source = new(ServiceName); + public static readonly Meter Meter = new(ServiceName); + + public static readonly Counter RequestCounter = Meter.CreateCounter("mediator_requests_total"); + public static readonly Histogram RequestDuration = Meter.CreateHistogram("mediator_request_duration_ms"); +} diff --git a/LiteCharms.Features/Mediator/TelemetryPipelineBehavior.cs b/LiteCharms.Features/Mediator/TelemetryPipelineBehavior.cs new file mode 100644 index 0000000..0a9efbd --- /dev/null +++ b/LiteCharms.Features/Mediator/TelemetryPipelineBehavior.cs @@ -0,0 +1,66 @@ +namespace LiteCharms.Features.Mediator; + +public sealed class TelemetryPipelineBehavior : + IPipelineBehavior + where TRequest : IRequest + where TResponse : ResultBase, new() +{ + public async ValueTask Handle(TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + var requestName = typeof(TRequest).Name; + + using var activity = MediatorTelemetry.Source.StartActivity(requestName); + + activity?.SetTag("mediator.request_type", typeof(TRequest).FullName); + + var stopWatch = Stopwatch.StartNew(); + var status = "Success"; + + try + { + TResponse? response = await next(message, cancellationToken); + + if (response is null) + { + status = "NullResponse"; + activity?.SetStatus(ActivityStatusCode.Error, "Response was null"); + + return response; + } + + if (response.IsFailed) + { + status = "Failed"; + activity?.SetStatus(ActivityStatusCode.Error, "Request failed"); + + var firstError = response.Errors.FirstOrDefault()?.Message ?? "Unknown Error"; + activity?.SetTag("error.message", firstError); + + foreach (var error in response.Errors) + activity?.AddEvent(new ActivityEvent("Result Error", tags: new() { { "message", error.Message } })); + } + else + activity?.SetStatus(ActivityStatusCode.Ok); + + return response; + } + catch (Exception ex) + { + status = "Exception"; + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + + activity?.AddException(ex); + + throw; + } + finally + { + stopWatch.Stop(); + + var tags = new TagList { { "request", requestName }, { "status", status } }; + + MediatorTelemetry.RequestCounter.Add(1, tags); + MediatorTelemetry.RequestDuration.Record(stopWatch.Elapsed.TotalMilliseconds, tags); + } + } +} \ No newline at end of file diff --git a/LiteCharms.Abstractions/IJobOrchestrator.cs b/LiteCharms.Features/Quartz/Abstractions/IJobOrchestrator.cs similarity index 79% rename from LiteCharms.Abstractions/IJobOrchestrator.cs rename to LiteCharms.Features/Quartz/Abstractions/IJobOrchestrator.cs index d98c155..8ce2c33 100644 --- a/LiteCharms.Abstractions/IJobOrchestrator.cs +++ b/LiteCharms.Features/Quartz/Abstractions/IJobOrchestrator.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Abstractions; +using LiteCharms.Features.Abstractions; + +namespace LiteCharms.Features.Quartz.Abstractions; public interface IJobOrchestrator { diff --git a/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs b/LiteCharms.Features/Quartz/JobOrchestrator.cs similarity index 93% rename from LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs rename to LiteCharms.Features/Quartz/JobOrchestrator.cs index 6874541..4f578b5 100644 --- a/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs +++ b/LiteCharms.Features/Quartz/JobOrchestrator.cs @@ -1,7 +1,8 @@ -using LiteCharms.Abstractions; -using static LiteCharms.Abstractions.Timezones; +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Quartz.Abstractions; +using static LiteCharms.Features.Extensions.Timezones; -namespace LiteCharms.Infrastructure.Quartz; +namespace LiteCharms.Features.Quartz; public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator { diff --git a/LiteCharms.Infrastructure/Quartz/MediatorJob.cs b/LiteCharms.Features/Quartz/MediatorJob.cs similarity index 87% rename from LiteCharms.Infrastructure/Quartz/MediatorJob.cs rename to LiteCharms.Features/Quartz/MediatorJob.cs index 8cf9c75..b3ea928 100644 --- a/LiteCharms.Infrastructure/Quartz/MediatorJob.cs +++ b/LiteCharms.Features/Quartz/MediatorJob.cs @@ -1,6 +1,6 @@ -using LiteCharms.Abstractions; +using LiteCharms.Features.Abstractions; -namespace LiteCharms.Infrastructure.Quartz; +namespace LiteCharms.Features.Quartz; [DisallowConcurrentExecution] public class MediatorJob(IMediator mediator) : IJob where TNotification : IEvent diff --git a/LiteCharms.Infrastructure/Quartz/RetryJobListener.cs b/LiteCharms.Features/Quartz/RetryJobListener.cs similarity index 93% rename from LiteCharms.Infrastructure/Quartz/RetryJobListener.cs rename to LiteCharms.Features/Quartz/RetryJobListener.cs index afe84e7..968b8bb 100644 --- a/LiteCharms.Infrastructure/Quartz/RetryJobListener.cs +++ b/LiteCharms.Features/Quartz/RetryJobListener.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Infrastructure.Quartz; +namespace LiteCharms.Features.Quartz; public class RetryJobListener : IJobListener { diff --git a/LiteCharms.Abstractions/EventBusQueueBase.cs b/LiteCharms.Features/ServiceBus/Abstractions/EventBusQueueBase.cs similarity index 73% rename from LiteCharms.Abstractions/EventBusQueueBase.cs rename to LiteCharms.Features/ServiceBus/Abstractions/EventBusQueueBase.cs index 29f7265..31391af 100644 --- a/LiteCharms.Abstractions/EventBusQueueBase.cs +++ b/LiteCharms.Features/ServiceBus/Abstractions/EventBusQueueBase.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Abstractions; +using LiteCharms.Features.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Abstractions; public abstract class EventBusQueueBase { diff --git a/LiteCharms.Abstractions/IEventBus.cs b/LiteCharms.Features/ServiceBus/Abstractions/IEventBus.cs similarity index 64% rename from LiteCharms.Abstractions/IEventBus.cs rename to LiteCharms.Features/ServiceBus/Abstractions/IEventBus.cs index f40d372..4f3ebb0 100644 --- a/LiteCharms.Abstractions/IEventBus.cs +++ b/LiteCharms.Features/ServiceBus/Abstractions/IEventBus.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Abstractions; +using LiteCharms.Features.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Abstractions; public interface IEventBus { diff --git a/LiteCharms.Abstractions/IEventBusQueue.cs b/LiteCharms.Features/ServiceBus/Abstractions/IEventBusQueue.cs similarity index 56% rename from LiteCharms.Abstractions/IEventBusQueue.cs rename to LiteCharms.Features/ServiceBus/Abstractions/IEventBusQueue.cs index 98750b4..1877555 100644 --- a/LiteCharms.Abstractions/IEventBusQueue.cs +++ b/LiteCharms.Features/ServiceBus/Abstractions/IEventBusQueue.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Abstractions; +using LiteCharms.Features.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Abstractions; public interface IEventBusQueue { diff --git a/LiteCharms.Abstractions/Constants.cs b/LiteCharms.Features/ServiceBus/Constants.cs similarity index 55% rename from LiteCharms.Abstractions/Constants.cs rename to LiteCharms.Features/ServiceBus/Constants.cs index ae08f94..ff8d4fe 100644 --- a/LiteCharms.Abstractions/Constants.cs +++ b/LiteCharms.Features/ServiceBus/Constants.cs @@ -1,13 +1,9 @@ -namespace LiteCharms.Abstractions; +namespace LiteCharms.Features.ServiceBus; public static class Constants { public const int QueueBounds = 100000; - public const string ShopSchedulerName = "shop"; - public const string ShopEmailFromName = "Khongisa Shop"; - public const string ShopEmailFromAddress = "shop@litecharms.co.za"; - public const string EmailServiceBus = nameof(EmailServiceBus); public const string GeneralServiceBus = nameof(GeneralServiceBus); public const string SalesServiceBus = nameof(SalesServiceBus); diff --git a/LiteCharms.Infrastructure/ServiceBus/EmailServiceBus.cs b/LiteCharms.Features/ServiceBus/EmailServiceBus.cs similarity index 70% rename from LiteCharms.Infrastructure/ServiceBus/EmailServiceBus.cs rename to LiteCharms.Features/ServiceBus/EmailServiceBus.cs index 5205283..c79438e 100644 --- a/LiteCharms.Infrastructure/ServiceBus/EmailServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/EmailServiceBus.cs @@ -1,7 +1,8 @@ -using LiteCharms.Abstractions; -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.ServiceBus.Abstractions; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Infrastructure.ServiceBus; +namespace LiteCharms.Features.ServiceBus; public class EmailServiceBus(EmailQueue messages) : IEventBus { diff --git a/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs new file mode 100644 index 0000000..5ccae66 --- /dev/null +++ b/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs @@ -0,0 +1,33 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.ServiceBus.Queues; + +namespace LiteCharms.Features.ServiceBus.Exchanges; + +public class EmailExchange(EmailQueue messages, ILogger logger, IPublisher mediator) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await foreach(IEvent? message in messages.Incoming.ReadAllAsync(stoppingToken)) + { + try + { + switch (message.Name) + { + case "SendShopEmailEnquiryEvent": + await mediator.Publish(message, stoppingToken); + break; + case "ProcessEmailNotificationsEvent": + await mediator.Publish(message, stoppingToken); + break; + default: + logger.LogWarning("Unsupported email event {Event}", message.Name); + break; + } + } + catch (Exception ex) + { + logger.LogError(ex, ex.Message); + } + } + } +} diff --git a/LiteCharms.Infrastructure/ServiceBus/Exchanges/GeneralExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs similarity index 69% rename from LiteCharms.Infrastructure/ServiceBus/Exchanges/GeneralExchange.cs rename to LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs index f21d566..c94fb5d 100644 --- a/LiteCharms.Infrastructure/ServiceBus/Exchanges/GeneralExchange.cs +++ b/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs @@ -1,6 +1,6 @@ -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Infrastructure.ServiceBus.Exchanges; +namespace LiteCharms.Features.ServiceBus.Exchanges; public class GeneralExchange(GeneralQueue messages) : BackgroundService { diff --git a/LiteCharms.Infrastructure/ServiceBus/Exchanges/SalesExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs similarity index 69% rename from LiteCharms.Infrastructure/ServiceBus/Exchanges/SalesExchange.cs rename to LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs index 6288734..645ab49 100644 --- a/LiteCharms.Infrastructure/ServiceBus/Exchanges/SalesExchange.cs +++ b/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs @@ -1,6 +1,6 @@ -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Infrastructure.ServiceBus.Exchanges; +namespace LiteCharms.Features.ServiceBus.Exchanges; public class SalesExchange(SalesQueue messages) : BackgroundService { diff --git a/LiteCharms.Infrastructure/ServiceBus/GeneralServiceBus.cs b/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs similarity index 73% rename from LiteCharms.Infrastructure/ServiceBus/GeneralServiceBus.cs rename to LiteCharms.Features/ServiceBus/GeneralServiceBus.cs index 28d6088..94edb37 100644 --- a/LiteCharms.Infrastructure/ServiceBus/GeneralServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs @@ -1,7 +1,8 @@ -using LiteCharms.Abstractions; -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.ServiceBus.Abstractions; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Infrastructure.ServiceBus; +namespace LiteCharms.Features.ServiceBus; public class GeneralServiceBus(GeneralQueue messages) : IEventBus { diff --git a/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs b/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs new file mode 100644 index 0000000..508ad5f --- /dev/null +++ b/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs @@ -0,0 +1,5 @@ +using LiteCharms.Features.ServiceBus.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Queues; + +public class EmailQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs b/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs new file mode 100644 index 0000000..3d79a2f --- /dev/null +++ b/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs @@ -0,0 +1,5 @@ +using LiteCharms.Features.ServiceBus.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Queues; + +public class GeneralQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs b/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs new file mode 100644 index 0000000..8dc5601 --- /dev/null +++ b/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs @@ -0,0 +1,5 @@ +using LiteCharms.Features.ServiceBus.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Queues; + +public class SalesQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Infrastructure/ServiceBus/SalesServiceBus.cs b/LiteCharms.Features/ServiceBus/SalesServiceBus.cs similarity index 73% rename from LiteCharms.Infrastructure/ServiceBus/SalesServiceBus.cs rename to LiteCharms.Features/ServiceBus/SalesServiceBus.cs index bb3412c..853657b 100644 --- a/LiteCharms.Infrastructure/ServiceBus/SalesServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/SalesServiceBus.cs @@ -1,7 +1,8 @@ -using LiteCharms.Abstractions; -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.ServiceBus.Abstractions; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Infrastructure.ServiceBus; +namespace LiteCharms.Features.ServiceBus; public class SalesServiceBus(SalesQueue messages) : IEventBus { diff --git a/LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/AddPackageItemsCommand.cs similarity index 100% rename from LiteCharms.Features/CartPackages/Commands/AddPackageItemsCommand.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/AddPackageItemsCommand.cs diff --git a/LiteCharms.Features/CartPackages/Commands/CreatePackageCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/CreatePackageCommand.cs similarity index 100% rename from LiteCharms.Features/CartPackages/Commands/CreatePackageCommand.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/CreatePackageCommand.cs diff --git a/LiteCharms.Features/CartPackages/Commands/DeletePackageCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageCommand.cs similarity index 100% rename from LiteCharms.Features/CartPackages/Commands/DeletePackageCommand.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageCommand.cs diff --git a/LiteCharms.Features/CartPackages/Commands/DeletePackageItemsCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageItemsCommand.cs similarity index 100% rename from LiteCharms.Features/CartPackages/Commands/DeletePackageItemsCommand.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageItemsCommand.cs diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs similarity index 97% rename from LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs index ffdd24a..71c54f7 100644 --- a/LiteCharms.Features/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs +++ b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.CartPackages.Commands.Handlers; diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs similarity index 96% rename from LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs index 4945a73..ff7847e 100644 --- a/LiteCharms.Features/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs +++ b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.CartPackages.Commands.Handlers; diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs similarity index 96% rename from LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs index 5ec6745..7d7284e 100644 --- a/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs +++ b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.CartPackages.Commands.Handlers; diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs similarity index 96% rename from LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs index 8f4e8e7..bad0e89 100644 --- a/LiteCharms.Features/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs +++ b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.CartPackages.Commands.Handlers; diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs similarity index 96% rename from LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs index 7889c52..15945ff 100644 --- a/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs +++ b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.CartPackages.Commands.Handlers; diff --git a/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs similarity index 95% rename from LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs index 5ef4a91..16c6482 100644 --- a/LiteCharms.Features/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs +++ b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.CartPackages.Commands.Handlers; diff --git a/LiteCharms.Features/CartPackages/Commands/UpdatePackageCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageCommand.cs similarity index 100% rename from LiteCharms.Features/CartPackages/Commands/UpdatePackageCommand.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageCommand.cs diff --git a/LiteCharms.Features/CartPackages/Commands/UpdatePackageStatusCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageStatusCommand.cs similarity index 100% rename from LiteCharms.Features/CartPackages/Commands/UpdatePackageStatusCommand.cs rename to LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageStatusCommand.cs diff --git a/LiteCharms.Entities/Package.cs b/LiteCharms.Features/Shop/CartPackages/Entities/Package.cs similarity index 69% rename from LiteCharms.Entities/Package.cs rename to LiteCharms.Features/Shop/CartPackages/Entities/Package.cs index ce8e799..3e4d210 100644 --- a/LiteCharms.Entities/Package.cs +++ b/LiteCharms.Features/Shop/CartPackages/Entities/Package.cs @@ -1,6 +1,4 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; +namespace LiteCharms.Features.Shop.CartPackages.Entities; [EntityTypeConfiguration] public class Package : Models.Package diff --git a/LiteCharms.Features/Shop/CartPackages/Entities/PackageConfirguration.cs b/LiteCharms.Features/Shop/CartPackages/Entities/PackageConfirguration.cs new file mode 100644 index 0000000..e6ff029 --- /dev/null +++ b/LiteCharms.Features/Shop/CartPackages/Entities/PackageConfirguration.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.Shop.CartPackages.Entities; + +public class PackageConfirguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Package)); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.Name).IsRequired(); + builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); + builder.Property(f => f.Description).IsRequired().HasMaxLength(2048); + builder.Property(f => f.ImageUrl).IsRequired(false).HasMaxLength(2048); + builder.Property(f => f.Active); + } +} diff --git a/LiteCharms.Features/Shop/CartPackages/Entities/PackageItem.cs b/LiteCharms.Features/Shop/CartPackages/Entities/PackageItem.cs new file mode 100644 index 0000000..306975e --- /dev/null +++ b/LiteCharms.Features/Shop/CartPackages/Entities/PackageItem.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.Shop.Products.Entities; + +namespace LiteCharms.Features.Shop.CartPackages.Entities; + +[EntityTypeConfiguration] +public class PackageItem : Models.PackageItem +{ + public virtual Package? Package { get; set; } + + public virtual ProductPrice? ProductPrice { get; set; } +} diff --git a/LiteCharms.Entities/Configuration/PackageItemConfiguration.cs b/LiteCharms.Features/Shop/CartPackages/Entities/PackageItemConfiguration.cs similarity index 51% rename from LiteCharms.Entities/Configuration/PackageItemConfiguration.cs rename to LiteCharms.Features/Shop/CartPackages/Entities/PackageItemConfiguration.cs index bf31659..c6f1009 100644 --- a/LiteCharms.Entities/Configuration/PackageItemConfiguration.cs +++ b/LiteCharms.Features/Shop/CartPackages/Entities/PackageItemConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Entities.Configuration; +namespace LiteCharms.Features.Shop.CartPackages.Entities; public class PackageItemConfiguration : IEntityTypeConfiguration { @@ -7,14 +7,21 @@ public class PackageItemConfiguration : IEntityTypeConfiguration builder.ToTable(nameof(PackageItem)); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(f => f.PackageId).IsRequired(); builder.Property(f => f.ProductPriceId).IsRequired(); builder.Property(f => f.Active); builder.HasOne(f => f.Package) - .WithMany() - .HasForeignKey(f => f.PackageId) - .OnDelete(DeleteBehavior.NoAction); + .WithMany(f => f.PackageItems) + .HasForeignKey(pi => pi.PackageId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.ProductPrice) + .WithMany() + .HasForeignKey(pi => pi.ProductPriceId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); } } diff --git a/LiteCharms.Models/Package.cs b/LiteCharms.Features/Shop/CartPackages/Models/Package.cs similarity index 66% rename from LiteCharms.Models/Package.cs rename to LiteCharms.Features/Shop/CartPackages/Models/Package.cs index cfc0feb..fcc560b 100644 --- a/LiteCharms.Models/Package.cs +++ b/LiteCharms.Features/Shop/CartPackages/Models/Package.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.CartPackages.Models; public class Package { @@ -10,7 +10,11 @@ public class Package public string? Name { get; set; } + public string? Summary { get; set; } + public string? Description { get; set; } + public string? ImageUrl { get; set; } + public bool Active { get; set; } } diff --git a/LiteCharms.Models/PackageItem.cs b/LiteCharms.Features/Shop/CartPackages/Models/PackageItem.cs similarity index 80% rename from LiteCharms.Models/PackageItem.cs rename to LiteCharms.Features/Shop/CartPackages/Models/PackageItem.cs index 2c27ad3..ff81c0a 100644 --- a/LiteCharms.Models/PackageItem.cs +++ b/LiteCharms.Features/Shop/CartPackages/Models/PackageItem.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.CartPackages.Models; public class PackageItem { diff --git a/LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs b/LiteCharms.Features/Shop/CartPackages/Queries/GetPackageItemsQuery.cs similarity index 89% rename from LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs rename to LiteCharms.Features/Shop/CartPackages/Queries/GetPackageItemsQuery.cs index e0830af..3315726 100644 --- a/LiteCharms.Features/CartPackages/Queries/GetPackageItemsQuery.cs +++ b/LiteCharms.Features/Shop/CartPackages/Queries/GetPackageItemsQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.CartPackages.Models; namespace LiteCharms.Features.CartPackages.Queries; diff --git a/LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs b/LiteCharms.Features/Shop/CartPackages/Queries/GetPackageQuery.cs similarity index 89% rename from LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs rename to LiteCharms.Features/Shop/CartPackages/Queries/GetPackageQuery.cs index 1384783..a05264e 100644 --- a/LiteCharms.Features/CartPackages/Queries/GetPackageQuery.cs +++ b/LiteCharms.Features/Shop/CartPackages/Queries/GetPackageQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.CartPackages.Models; namespace LiteCharms.Features.CartPackages.Queries; diff --git a/LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs b/LiteCharms.Features/Shop/CartPackages/Queries/GetPackagesQuery.cs similarity index 94% rename from LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs rename to LiteCharms.Features/Shop/CartPackages/Queries/GetPackagesQuery.cs index 351a141..036ff09 100644 --- a/LiteCharms.Features/CartPackages/Queries/GetPackagesQuery.cs +++ b/LiteCharms.Features/Shop/CartPackages/Queries/GetPackagesQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.CartPackages.Models; namespace LiteCharms.Features.CartPackages.Queries; diff --git a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs b/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs similarity index 93% rename from LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs rename to LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs index 487b203..3b19cdc 100644 --- a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs +++ b/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.CartPackages.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.CartPackages.Queries.Handlers; diff --git a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs b/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs similarity index 90% rename from LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs rename to LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs index 7a01fb5..d822584 100644 --- a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs +++ b/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.CartPackages.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.CartPackages.Queries.Handlers; diff --git a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs b/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs similarity index 93% rename from LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs rename to LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs index b495bb0..1ef691f 100644 --- a/LiteCharms.Features/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs +++ b/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.CartPackages.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.CartPackages.Queries.Handlers; diff --git a/LiteCharms.Features/Customers/Commands/CreateCustomerCommand.cs b/LiteCharms.Features/Shop/Customers/Commands/CreateCustomerCommand.cs similarity index 100% rename from LiteCharms.Features/Customers/Commands/CreateCustomerCommand.cs rename to LiteCharms.Features/Shop/Customers/Commands/CreateCustomerCommand.cs diff --git a/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs b/LiteCharms.Features/Shop/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs similarity index 97% rename from LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs rename to LiteCharms.Features/Shop/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs index 82f836b..bf1a5d6 100644 --- a/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs +++ b/LiteCharms.Features/Shop/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Customers.Commands.Handlers; diff --git a/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs b/LiteCharms.Features/Shop/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs similarity index 97% rename from LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs rename to LiteCharms.Features/Shop/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs index 72b9109..031f591 100644 --- a/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs +++ b/LiteCharms.Features/Shop/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Customers.Commands.Handlers; diff --git a/LiteCharms.Features/Customers/Commands/UpdateCustomerCommand.cs b/LiteCharms.Features/Shop/Customers/Commands/UpdateCustomerCommand.cs similarity index 100% rename from LiteCharms.Features/Customers/Commands/UpdateCustomerCommand.cs rename to LiteCharms.Features/Shop/Customers/Commands/UpdateCustomerCommand.cs diff --git a/LiteCharms.Entities/Customer.cs b/LiteCharms.Features/Shop/Customers/Entities/Customer.cs similarity index 58% rename from LiteCharms.Entities/Customer.cs rename to LiteCharms.Features/Shop/Customers/Entities/Customer.cs index bd3bf40..7b78bc8 100644 --- a/LiteCharms.Entities/Customer.cs +++ b/LiteCharms.Features/Shop/Customers/Entities/Customer.cs @@ -1,6 +1,9 @@ -using LiteCharms.Entities.Configuration; +using LiteCharms.Features.Shop.Leads.Entities; +using LiteCharms.Features.Shop.Orders.Entities; +using LiteCharms.Features.Shop.Quotes.Entities; +using LiteCharms.Features.Shop.ShoppingCarts.Entities; -namespace LiteCharms.Entities; +namespace LiteCharms.Features.Shop.Customers.Entities; [EntityTypeConfiguration] public class Customer : Models.Customer diff --git a/LiteCharms.Entities/Configuration/CustomerConfiguration.cs b/LiteCharms.Features/Shop/Customers/Entities/CustomerConfiguration.cs similarity index 77% rename from LiteCharms.Entities/Configuration/CustomerConfiguration.cs rename to LiteCharms.Features/Shop/Customers/Entities/CustomerConfiguration.cs index 9792251..335c1b0 100644 --- a/LiteCharms.Entities/Configuration/CustomerConfiguration.cs +++ b/LiteCharms.Features/Shop/Customers/Entities/CustomerConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Entities.Configuration; +namespace LiteCharms.Features.Shop.Customers.Entities; public class CustomerConfiguration : IEntityTypeConfiguration { @@ -7,8 +7,8 @@ public class CustomerConfiguration : IEntityTypeConfiguration builder.ToTable(nameof(Customer)); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); + builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.Company); builder.Property(f => f.Name).IsRequired(); builder.Property(f => f.LastName).IsRequired(); @@ -26,10 +26,5 @@ public class CustomerConfiguration : IEntityTypeConfiguration builder.Property(f => f.Country); builder.Property(f => f.PostalCode); builder.Property(f => f.Active).HasDefaultValue(true); - - builder.HasMany(f => f.Leads) - .WithOne(f => f.Customer) - .HasForeignKey(f => f.CustomerId) - .OnDelete(DeleteBehavior.NoAction); } } \ No newline at end of file diff --git a/LiteCharms.Models/Customer.cs b/LiteCharms.Features/Shop/Customers/Models/Customer.cs similarity index 93% rename from LiteCharms.Models/Customer.cs rename to LiteCharms.Features/Shop/Customers/Models/Customer.cs index b322f8a..14387f6 100644 --- a/LiteCharms.Models/Customer.cs +++ b/LiteCharms.Features/Shop/Customers/Models/Customer.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.Customers.Models; public class Customer { diff --git a/LiteCharms.Features/Customers/Queries/GetCustomerQuery.cs b/LiteCharms.Features/Shop/Customers/Queries/GetCustomerQuery.cs similarity index 89% rename from LiteCharms.Features/Customers/Queries/GetCustomerQuery.cs rename to LiteCharms.Features/Shop/Customers/Queries/GetCustomerQuery.cs index 5ea7394..c6ee4b0 100644 --- a/LiteCharms.Features/Customers/Queries/GetCustomerQuery.cs +++ b/LiteCharms.Features/Shop/Customers/Queries/GetCustomerQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Customers.Models; namespace LiteCharms.Features.Customers.Queries; diff --git a/LiteCharms.Features/Customers/Queries/GetCustomersQuery.cs b/LiteCharms.Features/Shop/Customers/Queries/GetCustomersQuery.cs similarity index 93% rename from LiteCharms.Features/Customers/Queries/GetCustomersQuery.cs rename to LiteCharms.Features/Shop/Customers/Queries/GetCustomersQuery.cs index 3f271b3..9c830b4 100644 --- a/LiteCharms.Features/Customers/Queries/GetCustomersQuery.cs +++ b/LiteCharms.Features/Shop/Customers/Queries/GetCustomersQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Customers.Models; namespace LiteCharms.Features.Customers.Queries; diff --git a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs b/LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomerQueryHandler.cs similarity index 90% rename from LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs rename to LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomerQueryHandler.cs index 0f921db..cbe478d 100644 --- a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs +++ b/LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomerQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Customers.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Customers.Queries.Handlers; diff --git a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs b/LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomersQueryHandler.cs similarity index 93% rename from LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs rename to LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomersQueryHandler.cs index 960ef3a..b9d1e99 100644 --- a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs +++ b/LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomersQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Customers.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Customers.Queries.Handlers; diff --git a/LiteCharms.Models/Enums.cs b/LiteCharms.Features/Shop/Enums.cs similarity index 76% rename from LiteCharms.Models/Enums.cs rename to LiteCharms.Features/Shop/Enums.cs index b672e25..6f25546 100644 --- a/LiteCharms.Models/Enums.cs +++ b/LiteCharms.Features/Shop/Enums.cs @@ -1,4 +1,16 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop; + +public enum EmailStatuses : int +{ + GeneralError = 0, + AuthenticationError = 1, + ProtocolError = 2, + Connected = 3, + Disconnected = 4, + TooManyConnections = 5, + ConnectionAborted = 6, + Success = 7 +} public enum CorrelationIdTypes : int { @@ -17,7 +29,7 @@ public enum CorrelationIdTypes : int public enum Priorities : int { - Low = 0, + Low = 0, Medium = 1, High = 2, } @@ -66,3 +78,4 @@ public enum NotificationDirection : int Outgoing = 1, Neutral = 2 } + diff --git a/LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs b/LiteCharms.Features/Shop/Leads/Commands/CreateLeadCommand.cs similarity index 100% rename from LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs rename to LiteCharms.Features/Shop/Leads/Commands/CreateLeadCommand.cs diff --git a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs b/LiteCharms.Features/Shop/Leads/Commands/Handlers/CreateLeadCommandHandler.cs similarity index 95% rename from LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs rename to LiteCharms.Features/Shop/Leads/Commands/Handlers/CreateLeadCommandHandler.cs index 256bc22..a51d31a 100644 --- a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs +++ b/LiteCharms.Features/Shop/Leads/Commands/Handlers/CreateLeadCommandHandler.cs @@ -1,5 +1,5 @@ -using LiteCharms.Features.Utilities.Hash.Commands; -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Utilities.Hash.Commands; namespace LiteCharms.Features.Leads.Commands.Handlers; diff --git a/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs b/LiteCharms.Features/Shop/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs similarity index 95% rename from LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs rename to LiteCharms.Features/Shop/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs index afaa15c..67f14fc 100644 --- a/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs +++ b/LiteCharms.Features/Shop/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Leads.Commands.Handlers; diff --git a/LiteCharms.Features/Leads/Commands/UpdateLeadCommand.cs b/LiteCharms.Features/Shop/Leads/Commands/UpdateLeadCommand.cs similarity index 92% rename from LiteCharms.Features/Leads/Commands/UpdateLeadCommand.cs rename to LiteCharms.Features/Shop/Leads/Commands/UpdateLeadCommand.cs index a201b70..ef31b01 100644 --- a/LiteCharms.Features/Leads/Commands/UpdateLeadCommand.cs +++ b/LiteCharms.Features/Shop/Leads/Commands/UpdateLeadCommand.cs @@ -1,4 +1,5 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop; +using LiteCharms.Models; namespace LiteCharms.Features.Leads.Commands; diff --git a/LiteCharms.Entities/Lead.cs b/LiteCharms.Features/Shop/Leads/Entities/Lead.cs similarity index 57% rename from LiteCharms.Entities/Lead.cs rename to LiteCharms.Features/Shop/Leads/Entities/Lead.cs index b1bdf62..deda2d6 100644 --- a/LiteCharms.Entities/Lead.cs +++ b/LiteCharms.Features/Shop/Leads/Entities/Lead.cs @@ -1,6 +1,6 @@ -using LiteCharms.Entities.Configuration; +using LiteCharms.Features.Shop.Customers.Models; -namespace LiteCharms.Entities; +namespace LiteCharms.Features.Shop.Leads.Entities; [EntityTypeConfiguration] public class Lead : Models.Lead diff --git a/LiteCharms.Entities/Configuration/LeadConfiguration.cs b/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs similarity index 86% rename from LiteCharms.Entities/Configuration/LeadConfiguration.cs rename to LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs index 8c8d004..482677c 100644 --- a/LiteCharms.Entities/Configuration/LeadConfiguration.cs +++ b/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Entities.Configuration; +namespace LiteCharms.Features.Shop.Leads.Entities; public class LeadConfiguration : IEntityTypeConfiguration { @@ -7,8 +7,8 @@ public class LeadConfiguration : IEntityTypeConfiguration builder.ToTable(nameof(Lead)); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); + builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.CustomerId).IsRequired(false); builder.Property(f => f.Source); builder.Property(f => f.ClickId); diff --git a/LiteCharms.Models/Lead.cs b/LiteCharms.Features/Shop/Leads/Models/Lead.cs similarity index 93% rename from LiteCharms.Models/Lead.cs rename to LiteCharms.Features/Shop/Leads/Models/Lead.cs index 5222114..0cb9fa1 100644 --- a/LiteCharms.Models/Lead.cs +++ b/LiteCharms.Features/Shop/Leads/Models/Lead.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.Leads.Models; public class Lead { diff --git a/LiteCharms.Features/Leads/Queries/GetCustomerLeadsQuery.cs b/LiteCharms.Features/Shop/Leads/Queries/GetCustomerLeadsQuery.cs similarity index 94% rename from LiteCharms.Features/Leads/Queries/GetCustomerLeadsQuery.cs rename to LiteCharms.Features/Shop/Leads/Queries/GetCustomerLeadsQuery.cs index ddede3c..de53634 100644 --- a/LiteCharms.Features/Leads/Queries/GetCustomerLeadsQuery.cs +++ b/LiteCharms.Features/Shop/Leads/Queries/GetCustomerLeadsQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Leads.Models; namespace LiteCharms.Features.Leads.Queries; diff --git a/LiteCharms.Features/Leads/Queries/GetLeadsQuery.cs b/LiteCharms.Features/Shop/Leads/Queries/GetLeadsQuery.cs similarity index 93% rename from LiteCharms.Features/Leads/Queries/GetLeadsQuery.cs rename to LiteCharms.Features/Shop/Leads/Queries/GetLeadsQuery.cs index 3278bf7..d280e65 100644 --- a/LiteCharms.Features/Leads/Queries/GetLeadsQuery.cs +++ b/LiteCharms.Features/Shop/Leads/Queries/GetLeadsQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Leads.Models; namespace LiteCharms.Features.Leads.Queries; diff --git a/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs b/LiteCharms.Features/Shop/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs similarity index 93% rename from LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs rename to LiteCharms.Features/Shop/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs index ed90212..dd9ba15 100644 --- a/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs +++ b/LiteCharms.Features/Shop/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Leads.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Leads.Queries.Handlers; diff --git a/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs b/LiteCharms.Features/Shop/Leads/Queries/Handlers/GetLeadsQueryHandler.cs similarity index 93% rename from LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs rename to LiteCharms.Features/Shop/Leads/Queries/Handlers/GetLeadsQueryHandler.cs index 0ebaafb..417ff5c 100644 --- a/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs +++ b/LiteCharms.Features/Shop/Leads/Queries/Handlers/GetLeadsQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Leads.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Leads.Queries.Handlers; diff --git a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs b/LiteCharms.Features/Shop/Notifications/Commands/CreateNotificationCommand.cs similarity index 73% rename from LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs rename to LiteCharms.Features/Shop/Notifications/Commands/CreateNotificationCommand.cs index 2b3e933..59e69d6 100644 --- a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs +++ b/LiteCharms.Features/Shop/Notifications/Commands/CreateNotificationCommand.cs @@ -1,8 +1,9 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop; +using LiteCharms.Models; namespace LiteCharms.Features.Notifications.Commands; -public class CreateNotificationCommand : IRequest> +public class CreateNotification : IRequest> { public NotificationDirection Direction { get; set; } @@ -30,7 +31,7 @@ public class CreateNotificationCommand : IRequest> public bool IsHtml { get; set; } - private CreateNotificationCommand(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false) + private CreateNotification(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false) { Direction = direction; Sender = sender; @@ -47,7 +48,7 @@ public class CreateNotificationCommand : IRequest> IsHtml = isHtml; } - public static CreateNotificationCommand Create(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false) + public static CreateNotification Create(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false) { if (string.IsNullOrWhiteSpace(sender)) throw new ArgumentException("Sender name is required.", nameof(sender)); diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs b/LiteCharms.Features/Shop/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs similarity index 92% rename from LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs rename to LiteCharms.Features/Shop/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs index e7a3fb4..facd9a3 100644 --- a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs +++ b/LiteCharms.Features/Shop/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs @@ -1,10 +1,10 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Notifications.Commands.Handlers; -public class CreateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> +public class CreateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> { - public async ValueTask> Handle(CreateNotificationCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(CreateNotification request, CancellationToken cancellationToken) { try { diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs b/LiteCharms.Features/Shop/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs similarity index 96% rename from LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs rename to LiteCharms.Features/Shop/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs index 5ce4e81..6d32acc 100644 --- a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs +++ b/LiteCharms.Features/Shop/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Notifications.Commands.Handlers; diff --git a/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs b/LiteCharms.Features/Shop/Notifications/Commands/UpdateNotificationCommand.cs similarity index 100% rename from LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs rename to LiteCharms.Features/Shop/Notifications/Commands/UpdateNotificationCommand.cs diff --git a/LiteCharms.Entities/Notification.cs b/LiteCharms.Features/Shop/Notifications/Entities/Notification.cs similarity index 60% rename from LiteCharms.Entities/Notification.cs rename to LiteCharms.Features/Shop/Notifications/Entities/Notification.cs index 2f3bf11..36a7dbc 100644 --- a/LiteCharms.Entities/Notification.cs +++ b/LiteCharms.Features/Shop/Notifications/Entities/Notification.cs @@ -1,6 +1,4 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; +namespace LiteCharms.Features.Shop.Notifications.Entities; [EntityTypeConfiguration] public class Notification : Models.Notification; diff --git a/LiteCharms.Entities/Configuration/NotificationConfiguration.cs b/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs similarity index 88% rename from LiteCharms.Entities/Configuration/NotificationConfiguration.cs rename to LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs index 45319a4..37b7659 100644 --- a/LiteCharms.Entities/Configuration/NotificationConfiguration.cs +++ b/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs @@ -1,6 +1,4 @@ -using LiteCharms.Models; - -namespace LiteCharms.Entities.Configuration; +namespace LiteCharms.Features.Shop.Notifications.Entities; public class NotificationConfiguration : IEntityTypeConfiguration { @@ -9,8 +7,8 @@ public class NotificationConfiguration : IEntityTypeConfiguration builder.ToTable(nameof(Notification)); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.Direction).IsRequired().HasConversion(); builder.Property(f => f.Platform).IsRequired().HasConversion(); builder.Property(f => f.Priority).IsRequired().HasConversion(); diff --git a/LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs similarity index 89% rename from LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs rename to LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs index 61b24d5..332ec86 100644 --- a/LiteCharms.Features/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs +++ b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs @@ -1,6 +1,7 @@ using LiteCharms.Features.Email.Commands; -using LiteCharms.Infrastructure.Database; -using static LiteCharms.Abstractions.Constants; +using LiteCharms.Features.Shop.Notifications.Entities; +using LiteCharms.Features.Shop.Postgres; +using static LiteCharms.Features.ServiceBus.Constants; namespace LiteCharms.Features.Notifications.Events.Handlers; @@ -49,7 +50,7 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory SendEmailAsync(Entities.Notification notification, CancellationToken cancellationToken = default) + private async Task SendEmailAsync(Notification notification, CancellationToken cancellationToken = default) { try { diff --git a/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs b/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs similarity index 91% rename from LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs rename to LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs index 9ac754f..bbc4da4 100644 --- a/LiteCharms.Features/Notifications/Events/ProcessEmailNotificationsEvent.cs +++ b/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs @@ -1,4 +1,4 @@ -using LiteCharms.Abstractions; +using LiteCharms.Features.Abstractions; namespace LiteCharms.Features.Notifications.Events; diff --git a/LiteCharms.Features/Shop/Notifications/INotificationService.cs b/LiteCharms.Features/Shop/Notifications/INotificationService.cs new file mode 100644 index 0000000..d036de3 --- /dev/null +++ b/LiteCharms.Features/Shop/Notifications/INotificationService.cs @@ -0,0 +1,9 @@ +using LiteCharms.Features.Shop.Notifications.Models; + +namespace LiteCharms.Features.Shop.Notifications; + +public interface INotificationService +{ + Task> CreateNotificationAsync(CreateNotification request, CancellationToken cancellationToken = default); + Task UpdateNotificationAsync(UpdateNotification request, CancellationToken cancellationToken = default); +} diff --git a/LiteCharms.Models/Notification.cs b/LiteCharms.Features/Shop/Notifications/Models/Notification.cs similarity index 93% rename from LiteCharms.Models/Notification.cs rename to LiteCharms.Features/Shop/Notifications/Models/Notification.cs index 6108cbb..cba815c 100644 --- a/LiteCharms.Models/Notification.cs +++ b/LiteCharms.Features/Shop/Notifications/Models/Notification.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.Notifications.Models; public class Notification { diff --git a/LiteCharms.Features/Shop/Notifications/Models/Records.cs b/LiteCharms.Features/Shop/Notifications/Models/Records.cs new file mode 100644 index 0000000..2f0f949 --- /dev/null +++ b/LiteCharms.Features/Shop/Notifications/Models/Records.cs @@ -0,0 +1,41 @@ +namespace LiteCharms.Features.Shop.Notifications.Models; + +public record CreateNotification +{ + public NotificationDirection Direction { get; set; } + + public string? Sender { get; set; } + + public string? SenderAddress { get; set; } + + public string? Subject { get; set; } + + public string? Message { get; set; } + + public NotificationPlatforms Platform { get; set; } + + public Priorities Priority { get; set; } + + public string? Recipient { get; set; } + + public string? RecipientAddress { get; set; } + + public string? CorrelationId { get; set; } + + public CorrelationIdTypes CorrelationIdType { get; set; } + + public bool IsInternal { get; set; } + + public bool IsHtml { get; set; } +} + +public class UpdateNotification +{ + public Guid NotificationId { get; set; } + + public bool Processed { get; set; } + + public bool HasError { get; set; } + + public string[]? Errors { get; set; } +} diff --git a/LiteCharms.Features/Shop/Notifications/NotificationService.cs b/LiteCharms.Features/Shop/Notifications/NotificationService.cs new file mode 100644 index 0000000..02ee707 --- /dev/null +++ b/LiteCharms.Features/Shop/Notifications/NotificationService.cs @@ -0,0 +1,5 @@ +namespace LiteCharms.Features.Shop.Notifications; + +public class NotificationService : INotificationService +{ +} diff --git a/LiteCharms.Features/Notifications/Queries/GetNotificationQuery.cs b/LiteCharms.Features/Shop/Notifications/Queries/GetNotificationQuery.cs similarity index 90% rename from LiteCharms.Features/Notifications/Queries/GetNotificationQuery.cs rename to LiteCharms.Features/Shop/Notifications/Queries/GetNotificationQuery.cs index f41aea5..6c599fb 100644 --- a/LiteCharms.Features/Notifications/Queries/GetNotificationQuery.cs +++ b/LiteCharms.Features/Shop/Notifications/Queries/GetNotificationQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Notifications.Models; namespace LiteCharms.Features.Notifications.Queries; diff --git a/LiteCharms.Features/Notifications/Queries/GetNotificationsQuery.cs b/LiteCharms.Features/Shop/Notifications/Queries/GetNotificationsQuery.cs similarity index 93% rename from LiteCharms.Features/Notifications/Queries/GetNotificationsQuery.cs rename to LiteCharms.Features/Shop/Notifications/Queries/GetNotificationsQuery.cs index 6eef589..6a10a4e 100644 --- a/LiteCharms.Features/Notifications/Queries/GetNotificationsQuery.cs +++ b/LiteCharms.Features/Shop/Notifications/Queries/GetNotificationsQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Notifications.Models; namespace LiteCharms.Features.Notifications.Queries; diff --git a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs b/LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs similarity index 91% rename from LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs rename to LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs index 5eac5d8..11d5553 100644 --- a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs +++ b/LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Notifications.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Notifications.Queries.Handlers; diff --git a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs b/LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs similarity index 93% rename from LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs rename to LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs index 70e448f..17e6094 100644 --- a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs +++ b/LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Notifications.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Notifications.Queries.Handlers; diff --git a/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs b/LiteCharms.Features/Shop/Orders/Commands/CreateOrderCommand.cs similarity index 100% rename from LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs rename to LiteCharms.Features/Shop/Orders/Commands/CreateOrderCommand.cs diff --git a/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs b/LiteCharms.Features/Shop/Orders/Commands/Handlers/CreateOrderCommandHandler.cs similarity index 96% rename from LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs rename to LiteCharms.Features/Shop/Orders/Commands/Handlers/CreateOrderCommandHandler.cs index b97f83e..95a599a 100644 --- a/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs +++ b/LiteCharms.Features/Shop/Orders/Commands/Handlers/CreateOrderCommandHandler.cs @@ -1,4 +1,5 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop; +using LiteCharms.Features.Shop.Postgres; using LiteCharms.Models; namespace LiteCharms.Features.Orders.Commands.Handlers; diff --git a/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs b/LiteCharms.Features/Shop/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs similarity index 95% rename from LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs rename to LiteCharms.Features/Shop/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs index 0cfe348..2c0052f 100644 --- a/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs +++ b/LiteCharms.Features/Shop/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Orders.Commands.Handlers; diff --git a/LiteCharms.Features/Orders/Commands/UpdateOrderStatusCommand.cs b/LiteCharms.Features/Shop/Orders/Commands/UpdateOrderStatusCommand.cs similarity index 93% rename from LiteCharms.Features/Orders/Commands/UpdateOrderStatusCommand.cs rename to LiteCharms.Features/Shop/Orders/Commands/UpdateOrderStatusCommand.cs index 3e6d1c6..eefc734 100644 --- a/LiteCharms.Features/Orders/Commands/UpdateOrderStatusCommand.cs +++ b/LiteCharms.Features/Shop/Orders/Commands/UpdateOrderStatusCommand.cs @@ -1,4 +1,5 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop; +using LiteCharms.Models; namespace LiteCharms.Features.Orders.Commands; diff --git a/LiteCharms.Features/Shop/Orders/Entities/Order.cs b/LiteCharms.Features/Shop/Orders/Entities/Order.cs new file mode 100644 index 0000000..f5dd1d0 --- /dev/null +++ b/LiteCharms.Features/Shop/Orders/Entities/Order.cs @@ -0,0 +1,17 @@ +using LiteCharms.Features.Shop.Customers.Entities; +using LiteCharms.Features.Shop.Quotes.Entities; +using LiteCharms.Features.Shop.ShoppingCarts.Entities; + +namespace LiteCharms.Features.Shop.Orders.Entities; + +[EntityTypeConfiguration] +public class Order : Models.Order +{ + public virtual ICollection? Refunds { get; set; } + + public virtual Customer? Customer { get; set; } + + public virtual Quote? Quote { get; set; } + + public virtual ShoppingCart? ShoppingCart { get; set; } +} diff --git a/LiteCharms.Features/Shop/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features/Shop/Orders/Entities/OrderConfiguration.cs new file mode 100644 index 0000000..d79fb0e --- /dev/null +++ b/LiteCharms.Features/Shop/Orders/Entities/OrderConfiguration.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Features.Shop.Orders.Entities; + +public class OrderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Order)); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.CustomerId).IsRequired(); + builder.Property(f => f.Status).HasConversion().IsRequired(); + builder.Property(f => f.Requirements).HasColumnType("jsonb").IsRequired(false); + builder.Property(f => f.Notes).HasColumnType("jsonb").IsRequired(false); + builder.Property(f => f.Terms).HasColumnType("jsonb").IsRequired(false); + builder.Property(f => f.InvoiceUrl).IsRequired(false).HasMaxLength(2048); + + builder.HasOne(o => o.Customer) + .WithMany(c => c.Orders) + .HasForeignKey(o => o.CustomerId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Entities/OrderRefund.cs b/LiteCharms.Features/Shop/Orders/Entities/OrderRefund.cs similarity index 68% rename from LiteCharms.Entities/OrderRefund.cs rename to LiteCharms.Features/Shop/Orders/Entities/OrderRefund.cs index e5a285d..33591d7 100644 --- a/LiteCharms.Entities/OrderRefund.cs +++ b/LiteCharms.Features/Shop/Orders/Entities/OrderRefund.cs @@ -1,6 +1,4 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; +namespace LiteCharms.Features.Shop.Orders.Entities; [EntityTypeConfiguration] public class OrderRefund : Models.OrderRefund diff --git a/LiteCharms.Entities/Configuration/OrderRefundConfiguration.cs b/LiteCharms.Features/Shop/Orders/Entities/OrderRefundConfiguration.cs similarity index 63% rename from LiteCharms.Entities/Configuration/OrderRefundConfiguration.cs rename to LiteCharms.Features/Shop/Orders/Entities/OrderRefundConfiguration.cs index 5550805..f353123 100644 --- a/LiteCharms.Entities/Configuration/OrderRefundConfiguration.cs +++ b/LiteCharms.Features/Shop/Orders/Entities/OrderRefundConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Entities.Configuration; +namespace LiteCharms.Features.Shop.Orders.Entities; public class OrderRefundConfiguration : IEntityTypeConfiguration { @@ -7,13 +7,15 @@ public class OrderRefundConfiguration : IEntityTypeConfiguration builder.ToTable(nameof(OrderRefund)); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd(); + builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(f => f.OrderId).IsRequired(); builder.Property(f => f.Reason).IsRequired(); builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); - builder.HasOne(f => f.Order) - .WithOne(o => o.Refund) - .HasForeignKey(o => o.OrderId); + builder.HasOne(r => r.Order) + .WithMany(o => o.Refunds) + .HasForeignKey(r => r.OrderId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); } } diff --git a/LiteCharms.Models/Order.cs b/LiteCharms.Features/Shop/Orders/Models/Order.cs similarity index 64% rename from LiteCharms.Models/Order.cs rename to LiteCharms.Features/Shop/Orders/Models/Order.cs index 3c55c81..d093993 100644 --- a/LiteCharms.Models/Order.cs +++ b/LiteCharms.Features/Shop/Orders/Models/Order.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.Orders.Models; public class Order { @@ -10,12 +10,6 @@ public class Order public Guid CustomerId { get; set; } - public Guid? QuoteId { get; set; } - - public Guid ShoppingCartId { get; set; } - - public Guid? RefundId { get; set; } - public OrderStatus Status { get; set; } public string[]? Requirements { get; set; } @@ -24,5 +18,5 @@ public class Order public string[]? Terms { get; set; } - public bool DepositRequired { get; set; } + public string? InvoiceUrl { get; set; } } diff --git a/LiteCharms.Models/OrderRefund.cs b/LiteCharms.Features/Shop/Orders/Models/OrderRefund.cs similarity index 81% rename from LiteCharms.Models/OrderRefund.cs rename to LiteCharms.Features/Shop/Orders/Models/OrderRefund.cs index 292e918..34668a7 100644 --- a/LiteCharms.Models/OrderRefund.cs +++ b/LiteCharms.Features/Shop/Orders/Models/OrderRefund.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.Orders.Models; public class OrderRefund { diff --git a/LiteCharms.Features/Orders/Queries/GetCustomerOrdersQuery.cs b/LiteCharms.Features/Shop/Orders/Queries/GetCustomerOrdersQuery.cs similarity index 90% rename from LiteCharms.Features/Orders/Queries/GetCustomerOrdersQuery.cs rename to LiteCharms.Features/Shop/Orders/Queries/GetCustomerOrdersQuery.cs index 7d11f91..f74f5c4 100644 --- a/LiteCharms.Features/Orders/Queries/GetCustomerOrdersQuery.cs +++ b/LiteCharms.Features/Shop/Orders/Queries/GetCustomerOrdersQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Orders.Models; namespace LiteCharms.Features.Orders.Queries; diff --git a/LiteCharms.Features/Orders/Queries/GetOrderRefundQuery.cs b/LiteCharms.Features/Shop/Orders/Queries/GetOrderRefundQuery.cs similarity index 93% rename from LiteCharms.Features/Orders/Queries/GetOrderRefundQuery.cs rename to LiteCharms.Features/Shop/Orders/Queries/GetOrderRefundQuery.cs index 887e03b..01295cf 100644 --- a/LiteCharms.Features/Orders/Queries/GetOrderRefundQuery.cs +++ b/LiteCharms.Features/Shop/Orders/Queries/GetOrderRefundQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Orders.Models; namespace LiteCharms.Features.Orders.Queries; diff --git a/LiteCharms.Features/Orders/Queries/GetOrdersQuery.cs b/LiteCharms.Features/Shop/Orders/Queries/GetOrdersQuery.cs similarity index 93% rename from LiteCharms.Features/Orders/Queries/GetOrdersQuery.cs rename to LiteCharms.Features/Shop/Orders/Queries/GetOrdersQuery.cs index c0c2c3b..0fc45d8 100644 --- a/LiteCharms.Features/Orders/Queries/GetOrdersQuery.cs +++ b/LiteCharms.Features/Shop/Orders/Queries/GetOrdersQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Orders.Models; namespace LiteCharms.Features.Orders.Queries; diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs b/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs similarity index 93% rename from LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs rename to LiteCharms.Features/Shop/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs index eb9e79f..a2f65d9 100644 --- a/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs +++ b/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Orders.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Orders.Queries.Handlers; diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs b/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs similarity index 92% rename from LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs rename to LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs index 47dd481..3493b11 100644 --- a/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs +++ b/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Orders.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Orders.Queries.Handlers; diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs b/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrdersQueryHandler.cs similarity index 93% rename from LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs rename to LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrdersQueryHandler.cs index f244869..898f3fc 100644 --- a/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs +++ b/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrdersQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Orders.Models; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Orders.Queries.Handlers; diff --git a/LiteCharms.Features/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs b/LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs similarity index 90% rename from LiteCharms.Features/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs rename to LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs index c4021f7..9013f10 100644 --- a/LiteCharms.Features/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs +++ b/LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs @@ -1,6 +1,7 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Orders.Refunds.Commands; +using LiteCharms.Features.Shop.Postgres; -namespace LiteCharms.Features.Refunds.Commands.Handlers; +namespace LiteCharms.Features.Shop.Orders.Refunds.Commands.Handlers; public class RefundCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> { diff --git a/LiteCharms.Features/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs b/LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs similarity index 85% rename from LiteCharms.Features/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs rename to LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs index 9f6fb00..ac88a49 100644 --- a/LiteCharms.Features/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs +++ b/LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs @@ -1,6 +1,7 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Orders.Refunds.Commands; +using LiteCharms.Features.Shop.Postgres; -namespace LiteCharms.Features.Refunds.Commands.Handlers; +namespace LiteCharms.Features.Shop.Orders.Refunds.Commands.Handlers; public class UpdateOrderRefundCommandHandler(IDbContextFactory contextFactory) : IRequestHandler { diff --git a/LiteCharms.Features/Refunds/Commands/RefundCustomerCommand.cs b/LiteCharms.Features/Shop/Orders/Refunds/Commands/RefundCustomerCommand.cs similarity index 94% rename from LiteCharms.Features/Refunds/Commands/RefundCustomerCommand.cs rename to LiteCharms.Features/Shop/Orders/Refunds/Commands/RefundCustomerCommand.cs index ccfda6e..ac5c75b 100644 --- a/LiteCharms.Features/Refunds/Commands/RefundCustomerCommand.cs +++ b/LiteCharms.Features/Shop/Orders/Refunds/Commands/RefundCustomerCommand.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Refunds.Commands; +namespace LiteCharms.Features.Shop.Orders.Refunds.Commands; public class RefundCustomerCommand : IRequest> { diff --git a/LiteCharms.Features/Refunds/Commands/UpdateOrderRefundCommand.cs b/LiteCharms.Features/Shop/Orders/Refunds/Commands/UpdateOrderRefundCommand.cs similarity index 92% rename from LiteCharms.Features/Refunds/Commands/UpdateOrderRefundCommand.cs rename to LiteCharms.Features/Shop/Orders/Refunds/Commands/UpdateOrderRefundCommand.cs index bd9ab59..9f69b2f 100644 --- a/LiteCharms.Features/Refunds/Commands/UpdateOrderRefundCommand.cs +++ b/LiteCharms.Features/Shop/Orders/Refunds/Commands/UpdateOrderRefundCommand.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Refunds.Commands; +namespace LiteCharms.Features.Shop.Orders.Refunds.Commands; public class UpdateOrderRefundCommand : IRequest { diff --git a/LiteCharms.Features/Refunds/Queries/GetCustomerRefundsQuery.cs b/LiteCharms.Features/Shop/Orders/Refunds/Queries/GetCustomerRefundsQuery.cs similarity index 80% rename from LiteCharms.Features/Refunds/Queries/GetCustomerRefundsQuery.cs rename to LiteCharms.Features/Shop/Orders/Refunds/Queries/GetCustomerRefundsQuery.cs index 24d2cc9..56d19f6 100644 --- a/LiteCharms.Features/Refunds/Queries/GetCustomerRefundsQuery.cs +++ b/LiteCharms.Features/Shop/Orders/Refunds/Queries/GetCustomerRefundsQuery.cs @@ -1,6 +1,6 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Orders.Models; -namespace LiteCharms.Features.Refunds.Queries; +namespace LiteCharms.Features.Shop.Orders.Refunds.Queries; public class GetCustomerRefundsQuery : IRequest> { diff --git a/LiteCharms.Features/Refunds/Queries/GetRefundQuery.cs b/LiteCharms.Features/Shop/Orders/Refunds/Queries/GetRefundQuery.cs similarity index 80% rename from LiteCharms.Features/Refunds/Queries/GetRefundQuery.cs rename to LiteCharms.Features/Shop/Orders/Refunds/Queries/GetRefundQuery.cs index 9f4d375..ddeaa7b 100644 --- a/LiteCharms.Features/Refunds/Queries/GetRefundQuery.cs +++ b/LiteCharms.Features/Shop/Orders/Refunds/Queries/GetRefundQuery.cs @@ -1,6 +1,6 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Orders.Models; -namespace LiteCharms.Features.Refunds.Queries; +namespace LiteCharms.Features.Shop.Orders.Refunds.Queries; public class GetRefundQuery : IRequest> { diff --git a/LiteCharms.Features/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs b/LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs similarity index 85% rename from LiteCharms.Features/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs rename to LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs index c198b76..8439f88 100644 --- a/LiteCharms.Features/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs +++ b/LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs @@ -1,9 +1,9 @@ using LiteCharms.Extensions; -using LiteCharms.Features.Refunds.Queries; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Orders.Models; +using LiteCharms.Features.Shop.Orders.Refunds.Queries; +using LiteCharms.Features.Shop.Postgres; -namespace LiteCharms.Features.Refunds.Queries.Handlers; +namespace LiteCharms.Features.Shop.Orders.Refunds.Queries.Handlers; public class GetCustomerRefundsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { diff --git a/LiteCharms.Features/Refunds/Queries/Handlers/GetRefundQueryHandler.cs b/LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetRefundQueryHandler.cs similarity index 80% rename from LiteCharms.Features/Refunds/Queries/Handlers/GetRefundQueryHandler.cs rename to LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetRefundQueryHandler.cs index 2561031..a363945 100644 --- a/LiteCharms.Features/Refunds/Queries/Handlers/GetRefundQueryHandler.cs +++ b/LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetRefundQueryHandler.cs @@ -1,8 +1,9 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Orders.Models; +using LiteCharms.Features.Shop.Orders.Refunds.Queries; +using LiteCharms.Features.Shop.Postgres; -namespace LiteCharms.Features.Refunds.Queries.Handlers; +namespace LiteCharms.Features.Shop.Orders.Refunds.Queries.Handlers; public class GetRefundQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.Designer.cs b/LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.Designer.cs similarity index 86% rename from LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.Designer.cs rename to LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.Designer.cs index 4624007..5900074 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.Designer.cs +++ b/LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.Designer.cs @@ -1,6 +1,6 @@ // using System; -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -9,10 +9,10 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace LiteCharms.Infrastructure.Database.Migrations +namespace LiteCharms.Features.Shop.Postgres.Migrations { [DbContext(typeof(ShopDbContext))] - [Migration("20260510132008_Init")] + [Migration("20260512065421_Init")] partial class Init { /// @@ -50,7 +50,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Discord") .HasColumnType("text"); @@ -86,7 +87,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("text"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.Property("Website") @@ -130,7 +130,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); @@ -148,7 +149,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("bigint"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.Property("WebClickId") @@ -176,7 +176,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Direction") .HasColumnType("integer"); @@ -234,7 +235,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("text"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -250,29 +250,22 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); - b.Property("DepositRequired") - .HasColumnType("boolean"); + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); b.PrimitiveCollection("Notes") .HasColumnType("jsonb"); - b.Property("QuoteId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - b.PrimitiveCollection("Requirements") .HasColumnType("jsonb"); - b.Property("ShoppingCartId") - .HasColumnType("uuid"); - b.Property("Status") .HasColumnType("integer"); @@ -280,19 +273,12 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("jsonb"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); - b.HasIndex("QuoteId") - .IsUnique(); - - b.HasIndex("ShoppingCartId") - .IsUnique(); - b.ToTable("Order", (string)null); }); @@ -308,7 +294,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("OrderId") .HasColumnType("uuid"); @@ -319,8 +306,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.HasKey("Id"); - b.HasIndex("OrderId") - .IsUnique(); + b.HasIndex("OrderId"); b.ToTable("OrderRefund", (string)null); }); @@ -336,27 +322,32 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Description") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); b.Property("Name") .IsRequired() .HasColumnType("text"); - b.Property("ShoppingCartId") - .HasColumnType("uuid"); + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); - b.HasIndex("ShoppingCartId"); - b.ToTable("Package", (string)null); }); @@ -371,14 +362,12 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("PackageId") .HasColumnType("uuid"); - b.Property("PackageId1") - .HasColumnType("uuid"); - b.Property("ProductPriceId") .HasColumnType("uuid"); @@ -386,7 +375,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.HasIndex("PackageId"); - b.HasIndex("PackageId1"); + b.HasIndex("ProductPriceId"); b.ToTable("PackageItem", (string)null); }); @@ -404,12 +393,25 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("Description") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); b.Property("Name") .IsRequired() .HasColumnType("text"); + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("Thumbnails") + .HasColumnType("jsonb"); + b.HasKey("Id"); b.ToTable("Product", (string)null); @@ -426,7 +428,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Discount") .HasPrecision(18, 2) @@ -440,7 +443,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("uuid"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -458,35 +460,40 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); - b.Property("CustomerId1") - .HasColumnType("uuid"); - b.Property("ExpiredAt") .HasColumnType("timestamp with time zone"); + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + b.Property("Reason") .HasColumnType("text"); - b.Property("ShoppingCartId") + b.Property("ShoppingCartId") .HasColumnType("uuid"); b.Property("Status") .HasColumnType("integer"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); - b.HasIndex("CustomerId1"); + b.HasIndex("OrderId") + .IsUnique(); b.HasIndex("ShoppingCartId") .IsUnique(); @@ -502,25 +509,25 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); - b.Property("CustomerId") + b.Property("CustomerId") .HasColumnType("uuid"); b.Property("OrderId") .HasColumnType("uuid"); - b.Property("QuoteId") - .HasColumnType("uuid"); - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); + b.HasIndex("OrderId") + .IsUnique(); + b.ToTable("ShoppingCart", (string)null); }); @@ -562,7 +569,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("PackageId") .HasColumnType("uuid"); @@ -583,8 +591,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations { b.HasOne("LiteCharms.Entities.Customer", "Customer") .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); + .HasForeignKey("CustomerId"); b.Navigation("Customer"); }); @@ -597,55 +604,37 @@ namespace LiteCharms.Infrastructure.Database.Migrations .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("LiteCharms.Entities.Quote", "Quote") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "QuoteId") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - b.Navigation("Customer"); - - b.Navigation("Quote"); - - b.Navigation("ShoppingCart"); }); modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => { b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") + .WithMany("Refunds") + .HasForeignKey("OrderId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Order"); }); - modelBuilder.Entity("LiteCharms.Entities.Package", b => - { - b.HasOne("LiteCharms.Entities.ShoppingCart", null) - .WithMany("Packages") - .HasForeignKey("ShoppingCartId"); - }); - modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => { b.HasOne("LiteCharms.Entities.Package", "Package") - .WithMany() + .WithMany("PackageItems") .HasForeignKey("PackageId") - .OnDelete(DeleteBehavior.NoAction) + .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Entities.Package", null) - .WithMany("PackageItems") - .HasForeignKey("PackageId1"); + b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); b.Navigation("Package"); + + b.Navigation("ProductPrice"); }); modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => @@ -662,23 +651,23 @@ namespace LiteCharms.Infrastructure.Database.Migrations modelBuilder.Entity("LiteCharms.Entities.Quote", b => { b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany() + .WithMany("Quotes") .HasForeignKey("CustomerId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Entities.Customer", null) - .WithMany("Quotes") - .HasForeignKey("CustomerId1"); + b.HasOne("LiteCharms.Entities.Order", "Order") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Entities.Quote", "OrderId"); b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") .WithOne("Quote") - .HasForeignKey("LiteCharms.Entities.Quote", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); + .HasForeignKey("LiteCharms.Entities.Quote", "ShoppingCartId"); b.Navigation("Customer"); + b.Navigation("Order"); + b.Navigation("ShoppingCart"); }); @@ -687,9 +676,17 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.HasOne("LiteCharms.Entities.Customer", "Customer") .WithMany("ShoppingCarts") .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.Order", "Order") + .WithOne("ShoppingCart") + .HasForeignKey("LiteCharms.Entities.ShoppingCart", "OrderId") + .OnDelete(DeleteBehavior.SetNull); b.Navigation("Customer"); + + b.Navigation("Order"); }); modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => @@ -716,13 +713,13 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.HasOne("LiteCharms.Entities.Package", "Package") .WithMany() .HasForeignKey("PackageId") - .OnDelete(DeleteBehavior.NoAction) + .OnDelete(DeleteBehavior.Restrict) .IsRequired(); b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithMany() + .WithMany("ShoppingCartPackages") .HasForeignKey("ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) + .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Package"); @@ -743,7 +740,11 @@ namespace LiteCharms.Infrastructure.Database.Migrations modelBuilder.Entity("LiteCharms.Entities.Order", b => { - b.Navigation("Refund"); + b.Navigation("Quote"); + + b.Navigation("Refunds"); + + b.Navigation("ShoppingCart"); }); modelBuilder.Entity("LiteCharms.Entities.Package", b => @@ -756,20 +757,13 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Navigation("ProductPrices"); }); - modelBuilder.Entity("LiteCharms.Entities.Quote", b => - { - b.Navigation("Order"); - }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => { - b.Navigation("Order"); - - b.Navigation("Packages"); - b.Navigation("Quote"); b.Navigation("ShoppingCartItems"); + + b.Navigation("ShoppingCartPackages"); }); #pragma warning restore 612, 618 } diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.cs b/LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.cs similarity index 86% rename from LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.cs rename to LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.cs index 773f0d6..1220ecc 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260510132008_Init.cs +++ b/LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace LiteCharms.Infrastructure.Database.Migrations +namespace LiteCharms.Features.Shop.Postgres.Migrations { /// public partial class Init : Migration @@ -16,7 +16,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), Company = table.Column(type: "text", nullable: true), Name = table.Column(type: "text", nullable: false), @@ -46,7 +46,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), Direction = table.Column(type: "integer", nullable: false), Platform = table.Column(type: "integer", nullable: false), @@ -70,13 +70,34 @@ namespace LiteCharms.Infrastructure.Database.Migrations table.PrimaryKey("PK_Notification", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Package", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Name = table.Column(type: "text", nullable: false), + Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Description = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Package", x => x.Id); + }); + migrationBuilder.CreateTable( name: "Product", columns: table => new { Id = table.Column(type: "uuid", nullable: false), Name = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), + Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Description = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Thumbnails = table.Column(type: "jsonb", nullable: true), Active = table.Column(type: "boolean", nullable: false, defaultValue: true) }, constraints: table => @@ -89,7 +110,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), CustomerId = table.Column(type: "uuid", nullable: true), Source = table.Column(type: "text", nullable: true), @@ -116,24 +137,28 @@ namespace LiteCharms.Infrastructure.Database.Migrations }); migrationBuilder.CreateTable( - name: "ShoppingCart", + name: "Order", columns: table => new { Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - CustomerId = table.Column(type: "uuid", nullable: true), - OrderId = table.Column(type: "uuid", nullable: true), - QuoteId = table.Column(type: "uuid", nullable: true) + CustomerId = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Requirements = table.Column(type: "jsonb", nullable: true), + Notes = table.Column(type: "jsonb", nullable: true), + Terms = table.Column(type: "jsonb", nullable: true), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true) }, constraints: table => { - table.PrimaryKey("PK_ShoppingCart", x => x.Id); + table.PrimaryKey("PK_Order", x => x.Id); table.ForeignKey( - name: "FK_ShoppingCart_Customer_CustomerId", + name: "FK_Order_Customer_CustomerId", column: x => x.CustomerId, principalTable: "Customer", - principalColumn: "Id"); + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); }); migrationBuilder.CreateTable( @@ -141,7 +166,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), ProductId = table.Column(type: "uuid", nullable: false), Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), @@ -160,25 +185,78 @@ namespace LiteCharms.Infrastructure.Database.Migrations }); migrationBuilder.CreateTable( - name: "Package", + name: "OrderRefund", columns: table => new { Id = table.Column(type: "uuid", nullable: false), - ShoppingCartId = table.Column(type: "uuid", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + OrderId = table.Column(type: "uuid", nullable: false), + Reason = table.Column(type: "text", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderRefund", x => x.Id); + table.ForeignKey( + name: "FK_OrderRefund_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCart", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - Name = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCart", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCart_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingCart_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "PackageItem", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PackageId = table.Column(type: "uuid", nullable: false), + ProductPriceId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), Active = table.Column(type: "boolean", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_Package", x => x.Id); + table.PrimaryKey("PK_PackageItem", x => x.Id); table.ForeignKey( - name: "FK_Package_ShoppingCart_ShoppingCartId", - column: x => x.ShoppingCartId, - principalTable: "ShoppingCart", - principalColumn: "Id"); + name: "FK_PackageItem_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PackageItem_ProductPrice_ProductPriceId", + column: x => x.ProductPriceId, + principalTable: "ProductPrice", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); }); migrationBuilder.CreateTable( @@ -186,13 +264,14 @@ namespace LiteCharms.Infrastructure.Database.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - CustomerId1 = table.Column(type: "uuid", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true), CustomerId = table.Column(type: "uuid", nullable: false), - ShoppingCartId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true), + ShoppingCartId = table.Column(type: "uuid", nullable: true), Status = table.Column(type: "integer", nullable: false), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), Reason = table.Column(type: "text", nullable: true) }, constraints: table => @@ -205,9 +284,9 @@ namespace LiteCharms.Infrastructure.Database.Migrations principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_Quote_Customer_CustomerId1", - column: x => x.CustomerId1, - principalTable: "Customer", + name: "FK_Quote_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", principalColumn: "Id"); table.ForeignKey( name: "FK_Quote_ShoppingCart_ShoppingCartId", @@ -244,38 +323,12 @@ namespace LiteCharms.Infrastructure.Database.Migrations onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateTable( - name: "PackageItem", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - PackageId1 = table.Column(type: "uuid", nullable: true), - PackageId = table.Column(type: "uuid", nullable: false), - ProductPriceId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - Active = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PackageItem", x => x.Id); - table.ForeignKey( - name: "FK_PackageItem_Package_PackageId", - column: x => x.PackageId, - principalTable: "Package", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_PackageItem_Package_PackageId1", - column: x => x.PackageId1, - principalTable: "Package", - principalColumn: "Id"); - }); - migrationBuilder.CreateTable( name: "ShoppingCartPackage", columns: table => new { Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), ShoppingCartId = table.Column(type: "uuid", nullable: false), PackageId = table.Column(type: "uuid", nullable: false) }, @@ -286,70 +339,12 @@ namespace LiteCharms.Infrastructure.Database.Migrations name: "FK_ShoppingCartPackage_Package_PackageId", column: x => x.PackageId, principalTable: "Package", - principalColumn: "Id"); + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); table.ForeignKey( name: "FK_ShoppingCartPackage_ShoppingCart_ShoppingCartId", column: x => x.ShoppingCartId, principalTable: "ShoppingCart", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "Order", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - CustomerId = table.Column(type: "uuid", nullable: false), - QuoteId = table.Column(type: "uuid", nullable: true), - ShoppingCartId = table.Column(type: "uuid", nullable: false), - RefundId = table.Column(type: "uuid", nullable: true), - Status = table.Column(type: "integer", nullable: false), - Requirements = table.Column(type: "jsonb", nullable: true), - Notes = table.Column(type: "jsonb", nullable: true), - Terms = table.Column(type: "jsonb", nullable: true), - DepositRequired = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Order", x => x.Id); - table.ForeignKey( - name: "FK_Order_Customer_CustomerId", - column: x => x.CustomerId, - principalTable: "Customer", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_Order_Quote_QuoteId", - column: x => x.QuoteId, - principalTable: "Quote", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_Order_ShoppingCart_ShoppingCartId", - column: x => x.ShoppingCartId, - principalTable: "ShoppingCart", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "OrderRefund", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - OrderId = table.Column(type: "uuid", nullable: false), - Reason = table.Column(type: "text", nullable: false), - Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_OrderRefund", x => x.Id); - table.ForeignKey( - name: "FK_OrderRefund_Order_OrderId", - column: x => x.OrderId, - principalTable: "Order", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); @@ -364,28 +359,10 @@ namespace LiteCharms.Infrastructure.Database.Migrations table: "Order", column: "CustomerId"); - migrationBuilder.CreateIndex( - name: "IX_Order_QuoteId", - table: "Order", - column: "QuoteId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Order_ShoppingCartId", - table: "Order", - column: "ShoppingCartId", - unique: true); - migrationBuilder.CreateIndex( name: "IX_OrderRefund_OrderId", table: "OrderRefund", - column: "OrderId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Package_ShoppingCartId", - table: "Package", - column: "ShoppingCartId"); + column: "OrderId"); migrationBuilder.CreateIndex( name: "IX_PackageItem_PackageId", @@ -393,9 +370,9 @@ namespace LiteCharms.Infrastructure.Database.Migrations column: "PackageId"); migrationBuilder.CreateIndex( - name: "IX_PackageItem_PackageId1", + name: "IX_PackageItem_ProductPriceId", table: "PackageItem", - column: "PackageId1"); + column: "ProductPriceId"); migrationBuilder.CreateIndex( name: "IX_ProductPrice_ProductId", @@ -408,9 +385,10 @@ namespace LiteCharms.Infrastructure.Database.Migrations column: "CustomerId"); migrationBuilder.CreateIndex( - name: "IX_Quote_CustomerId1", + name: "IX_Quote_OrderId", table: "Quote", - column: "CustomerId1"); + column: "OrderId", + unique: true); migrationBuilder.CreateIndex( name: "IX_Quote_ShoppingCartId", @@ -423,6 +401,12 @@ namespace LiteCharms.Infrastructure.Database.Migrations table: "ShoppingCart", column: "CustomerId"); + migrationBuilder.CreateIndex( + name: "IX_ShoppingCart_OrderId", + table: "ShoppingCart", + column: "OrderId", + unique: true); + migrationBuilder.CreateIndex( name: "IX_ShoppingCartItems_ProductPriceId", table: "ShoppingCartItems", @@ -459,15 +443,15 @@ namespace LiteCharms.Infrastructure.Database.Migrations migrationBuilder.DropTable( name: "PackageItem"); + migrationBuilder.DropTable( + name: "Quote"); + migrationBuilder.DropTable( name: "ShoppingCartItems"); migrationBuilder.DropTable( name: "ShoppingCartPackage"); - migrationBuilder.DropTable( - name: "Order"); - migrationBuilder.DropTable( name: "ProductPrice"); @@ -475,13 +459,13 @@ namespace LiteCharms.Infrastructure.Database.Migrations name: "Package"); migrationBuilder.DropTable( - name: "Quote"); + name: "ShoppingCart"); migrationBuilder.DropTable( name: "Product"); migrationBuilder.DropTable( - name: "ShoppingCart"); + name: "Order"); migrationBuilder.DropTable( name: "Customer"); diff --git a/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs b/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs similarity index 86% rename from LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs rename to LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs index 6708099..26d9a29 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/ShopDbContextModelSnapshot.cs +++ b/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs @@ -1,6 +1,6 @@ // using System; -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -8,7 +8,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace LiteCharms.Infrastructure.Database.Migrations +namespace LiteCharms.Features.Shop.Postgres.Migrations { [DbContext(typeof(ShopDbContext))] partial class ShopDbContextModelSnapshot : ModelSnapshot @@ -47,7 +47,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Discord") .HasColumnType("text"); @@ -83,7 +84,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("text"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.Property("Website") @@ -127,7 +127,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); @@ -145,7 +146,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("bigint"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.Property("WebClickId") @@ -173,7 +173,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Direction") .HasColumnType("integer"); @@ -231,7 +232,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("text"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -247,29 +247,22 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); - b.Property("DepositRequired") - .HasColumnType("boolean"); + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); b.PrimitiveCollection("Notes") .HasColumnType("jsonb"); - b.Property("QuoteId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - b.PrimitiveCollection("Requirements") .HasColumnType("jsonb"); - b.Property("ShoppingCartId") - .HasColumnType("uuid"); - b.Property("Status") .HasColumnType("integer"); @@ -277,19 +270,12 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("jsonb"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); - b.HasIndex("QuoteId") - .IsUnique(); - - b.HasIndex("ShoppingCartId") - .IsUnique(); - b.ToTable("Order", (string)null); }); @@ -305,7 +291,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("OrderId") .HasColumnType("uuid"); @@ -316,8 +303,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.HasKey("Id"); - b.HasIndex("OrderId") - .IsUnique(); + b.HasIndex("OrderId"); b.ToTable("OrderRefund", (string)null); }); @@ -333,27 +319,32 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Description") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); b.Property("Name") .IsRequired() .HasColumnType("text"); - b.Property("ShoppingCartId") - .HasColumnType("uuid"); + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); - b.HasIndex("ShoppingCartId"); - b.ToTable("Package", (string)null); }); @@ -368,14 +359,12 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("PackageId") .HasColumnType("uuid"); - b.Property("PackageId1") - .HasColumnType("uuid"); - b.Property("ProductPriceId") .HasColumnType("uuid"); @@ -383,7 +372,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.HasIndex("PackageId"); - b.HasIndex("PackageId1"); + b.HasIndex("ProductPriceId"); b.ToTable("PackageItem", (string)null); }); @@ -401,12 +390,25 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("Description") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); b.Property("Name") .IsRequired() .HasColumnType("text"); + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("Thumbnails") + .HasColumnType("jsonb"); + b.HasKey("Id"); b.ToTable("Product", (string)null); @@ -423,7 +425,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Discount") .HasPrecision(18, 2) @@ -437,7 +440,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("uuid"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -455,35 +457,40 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); - b.Property("CustomerId1") - .HasColumnType("uuid"); - b.Property("ExpiredAt") .HasColumnType("timestamp with time zone"); + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + b.Property("Reason") .HasColumnType("text"); - b.Property("ShoppingCartId") + b.Property("ShoppingCartId") .HasColumnType("uuid"); b.Property("Status") .HasColumnType("integer"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); - b.HasIndex("CustomerId1"); + b.HasIndex("OrderId") + .IsUnique(); b.HasIndex("ShoppingCartId") .IsUnique(); @@ -499,25 +506,25 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); - b.Property("CustomerId") + b.Property("CustomerId") .HasColumnType("uuid"); b.Property("OrderId") .HasColumnType("uuid"); - b.Property("QuoteId") - .HasColumnType("uuid"); - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); + b.HasIndex("OrderId") + .IsUnique(); + b.ToTable("ShoppingCart", (string)null); }); @@ -559,7 +566,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("PackageId") .HasColumnType("uuid"); @@ -580,8 +588,7 @@ namespace LiteCharms.Infrastructure.Database.Migrations { b.HasOne("LiteCharms.Entities.Customer", "Customer") .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); + .HasForeignKey("CustomerId"); b.Navigation("Customer"); }); @@ -594,55 +601,37 @@ namespace LiteCharms.Infrastructure.Database.Migrations .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("LiteCharms.Entities.Quote", "Quote") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "QuoteId") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - b.Navigation("Customer"); - - b.Navigation("Quote"); - - b.Navigation("ShoppingCart"); }); modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => { b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") + .WithMany("Refunds") + .HasForeignKey("OrderId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Order"); }); - modelBuilder.Entity("LiteCharms.Entities.Package", b => - { - b.HasOne("LiteCharms.Entities.ShoppingCart", null) - .WithMany("Packages") - .HasForeignKey("ShoppingCartId"); - }); - modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => { b.HasOne("LiteCharms.Entities.Package", "Package") - .WithMany() + .WithMany("PackageItems") .HasForeignKey("PackageId") - .OnDelete(DeleteBehavior.NoAction) + .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Entities.Package", null) - .WithMany("PackageItems") - .HasForeignKey("PackageId1"); + b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); b.Navigation("Package"); + + b.Navigation("ProductPrice"); }); modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => @@ -659,23 +648,23 @@ namespace LiteCharms.Infrastructure.Database.Migrations modelBuilder.Entity("LiteCharms.Entities.Quote", b => { b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany() + .WithMany("Quotes") .HasForeignKey("CustomerId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Entities.Customer", null) - .WithMany("Quotes") - .HasForeignKey("CustomerId1"); + b.HasOne("LiteCharms.Entities.Order", "Order") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Entities.Quote", "OrderId"); b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") .WithOne("Quote") - .HasForeignKey("LiteCharms.Entities.Quote", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); + .HasForeignKey("LiteCharms.Entities.Quote", "ShoppingCartId"); b.Navigation("Customer"); + b.Navigation("Order"); + b.Navigation("ShoppingCart"); }); @@ -684,9 +673,17 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.HasOne("LiteCharms.Entities.Customer", "Customer") .WithMany("ShoppingCarts") .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.Order", "Order") + .WithOne("ShoppingCart") + .HasForeignKey("LiteCharms.Entities.ShoppingCart", "OrderId") + .OnDelete(DeleteBehavior.SetNull); b.Navigation("Customer"); + + b.Navigation("Order"); }); modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => @@ -713,13 +710,13 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.HasOne("LiteCharms.Entities.Package", "Package") .WithMany() .HasForeignKey("PackageId") - .OnDelete(DeleteBehavior.NoAction) + .OnDelete(DeleteBehavior.Restrict) .IsRequired(); b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithMany() + .WithMany("ShoppingCartPackages") .HasForeignKey("ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) + .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Package"); @@ -740,7 +737,11 @@ namespace LiteCharms.Infrastructure.Database.Migrations modelBuilder.Entity("LiteCharms.Entities.Order", b => { - b.Navigation("Refund"); + b.Navigation("Quote"); + + b.Navigation("Refunds"); + + b.Navigation("ShoppingCart"); }); modelBuilder.Entity("LiteCharms.Entities.Package", b => @@ -753,20 +754,13 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Navigation("ProductPrices"); }); - modelBuilder.Entity("LiteCharms.Entities.Quote", b => - { - b.Navigation("Order"); - }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => { - b.Navigation("Order"); - - b.Navigation("Packages"); - b.Navigation("Quote"); b.Navigation("ShoppingCartItems"); + + b.Navigation("ShoppingCartPackages"); }); #pragma warning restore 612, 618 } diff --git a/LiteCharms.Infrastructure/Database/ShopDbContext.cs b/LiteCharms.Features/Shop/Postgres/ShopDbContext.cs similarity index 64% rename from LiteCharms.Infrastructure/Database/ShopDbContext.cs rename to LiteCharms.Features/Shop/Postgres/ShopDbContext.cs index c3d0679..b02f630 100644 --- a/LiteCharms.Infrastructure/Database/ShopDbContext.cs +++ b/LiteCharms.Features/Shop/Postgres/ShopDbContext.cs @@ -1,6 +1,13 @@ -using LiteCharms.Entities; +using LiteCharms.Features.Shop.CartPackages.Entities; +using LiteCharms.Features.Shop.Customers.Entities; +using LiteCharms.Features.Shop.Leads.Entities; +using LiteCharms.Features.Shop.Notifications.Entities; +using LiteCharms.Features.Shop.Orders.Entities; +using LiteCharms.Features.Shop.Products.Entities; +using LiteCharms.Features.Shop.Quotes.Entities; +using LiteCharms.Features.Shop.ShoppingCarts.Entities; -namespace LiteCharms.Infrastructure.Database; +namespace LiteCharms.Features.Shop.Postgres; public class ShopDbContext(DbContextOptions options) : DbContext(options) { diff --git a/LiteCharms.Infrastructure/Database/ShopDbContextFactory.cs b/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs similarity index 93% rename from LiteCharms.Infrastructure/Database/ShopDbContextFactory.cs rename to LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs index 9de3cda..b8437d9 100644 --- a/LiteCharms.Infrastructure/Database/ShopDbContextFactory.cs +++ b/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Infrastructure.Database; +namespace LiteCharms.Features.Shop.Postgres; public class ShopDbContextFactory : IDesignTimeDbContextFactory { diff --git a/LiteCharms.Entities/Product.cs b/LiteCharms.Features/Shop/Products/Entities/Product.cs similarity index 69% rename from LiteCharms.Entities/Product.cs rename to LiteCharms.Features/Shop/Products/Entities/Product.cs index 6b46ff7..5aa0b99 100644 --- a/LiteCharms.Entities/Product.cs +++ b/LiteCharms.Features/Shop/Products/Entities/Product.cs @@ -1,6 +1,4 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; +namespace LiteCharms.Features.Shop.Products.Entities; [EntityTypeConfiguration] public class Product : Models.Product diff --git a/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs new file mode 100644 index 0000000..ee2fb68 --- /dev/null +++ b/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs @@ -0,0 +1,17 @@ +namespace LiteCharms.Features.Shop.Products.Entities; + +public class ProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Product)); + + builder.HasKey(f => f.Id); + builder.Property(f => f.Name).IsRequired(); + builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); + builder.Property(f => f.Description).IsRequired().HasMaxLength(2048); + builder.Property(f => f.ImageUrl).IsRequired(false).HasMaxLength(2048); + builder.Property(f => f.Thumbnails).HasColumnType("jsonb").IsRequired(false); + builder.Property(f => f.Active).HasDefaultValue(true); + } +} diff --git a/LiteCharms.Entities/ProductPrice.cs b/LiteCharms.Features/Shop/Products/Entities/ProductPrice.cs similarity index 69% rename from LiteCharms.Entities/ProductPrice.cs rename to LiteCharms.Features/Shop/Products/Entities/ProductPrice.cs index db492ed..a711cae 100644 --- a/LiteCharms.Entities/ProductPrice.cs +++ b/LiteCharms.Features/Shop/Products/Entities/ProductPrice.cs @@ -1,6 +1,4 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; +namespace LiteCharms.Features.Shop.Products.Entities; [EntityTypeConfiguration] public class ProductPrice : Models.ProductPrice diff --git a/LiteCharms.Entities/Configuration/ProductPriceConfiguration.cs b/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs similarity index 70% rename from LiteCharms.Entities/Configuration/ProductPriceConfiguration.cs rename to LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs index 5e7cfc9..a8daa83 100644 --- a/LiteCharms.Entities/Configuration/ProductPriceConfiguration.cs +++ b/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Entities.Configuration; +namespace LiteCharms.Features.Shop.Products.Entities; public class ProductPriceConfiguration : IEntityTypeConfiguration { @@ -7,16 +7,17 @@ public class ProductPriceConfiguration : IEntityTypeConfiguration builder.ToTable(nameof(ProductPrice)); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); + builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.ProductId).IsRequired(); builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2); builder.Property(f => f.Discount).HasPrecision(18, 2); builder.Property(f => f.Active); builder.HasOne(f => f.Product) - .WithMany(p => p.ProductPrices) - .HasForeignKey(f => f.ProductId) + .WithMany(f => f.ProductPrices) + .HasForeignKey(pp => pp.ProductId) + .IsRequired() .OnDelete(DeleteBehavior.Restrict); } } diff --git a/LiteCharms.Features/Shop/Products/Models/Product.cs b/LiteCharms.Features/Shop/Products/Models/Product.cs new file mode 100644 index 0000000..8fdc778 --- /dev/null +++ b/LiteCharms.Features/Shop/Products/Models/Product.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.Shop.Products.Models; + +public class Product +{ + public Guid Id { get; set; } + + public string? Name { get; set; } + + public string? Summary { get; set; } + + public string? Description { get; set; } + + public string? ImageUrl { get; set; } + + public string[]? Thumbnails { get; set; } + + public bool Active { get; set; } +} diff --git a/LiteCharms.Models/ProductPrice.cs b/LiteCharms.Features/Shop/Products/Models/ProductPrice.cs similarity index 85% rename from LiteCharms.Models/ProductPrice.cs rename to LiteCharms.Features/Shop/Products/Models/ProductPrice.cs index 49931ea..1f44247 100644 --- a/LiteCharms.Models/ProductPrice.cs +++ b/LiteCharms.Features/Shop/Products/Models/ProductPrice.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.Products.Models; public class ProductPrice { diff --git a/LiteCharms.Features/Products/Queries/GetProductPriceQuery.cs b/LiteCharms.Features/Shop/Products/Queries/GetProductPriceQuery.cs similarity index 90% rename from LiteCharms.Features/Products/Queries/GetProductPriceQuery.cs rename to LiteCharms.Features/Shop/Products/Queries/GetProductPriceQuery.cs index 347d759..f118bcc 100644 --- a/LiteCharms.Features/Products/Queries/GetProductPriceQuery.cs +++ b/LiteCharms.Features/Shop/Products/Queries/GetProductPriceQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Products.Models; namespace LiteCharms.Features.Products.Queries; diff --git a/LiteCharms.Features/Products/Queries/GetProductPricesQuery.cs b/LiteCharms.Features/Shop/Products/Queries/GetProductPricesQuery.cs similarity index 90% rename from LiteCharms.Features/Products/Queries/GetProductPricesQuery.cs rename to LiteCharms.Features/Shop/Products/Queries/GetProductPricesQuery.cs index 6e9cf66..5ba4be2 100644 --- a/LiteCharms.Features/Products/Queries/GetProductPricesQuery.cs +++ b/LiteCharms.Features/Shop/Products/Queries/GetProductPricesQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Products.Models; namespace LiteCharms.Features.Products.Queries; diff --git a/LiteCharms.Features/Products/Queries/GetProductQuery.cs b/LiteCharms.Features/Shop/Products/Queries/GetProductQuery.cs similarity index 89% rename from LiteCharms.Features/Products/Queries/GetProductQuery.cs rename to LiteCharms.Features/Shop/Products/Queries/GetProductQuery.cs index 2f5b2a8..cd811ac 100644 --- a/LiteCharms.Features/Products/Queries/GetProductQuery.cs +++ b/LiteCharms.Features/Shop/Products/Queries/GetProductQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Products.Models; namespace LiteCharms.Features.Products.Queries; diff --git a/LiteCharms.Features/Products/Queries/GetProductsQuery.cs b/LiteCharms.Features/Shop/Products/Queries/GetProductsQuery.cs similarity index 89% rename from LiteCharms.Features/Products/Queries/GetProductsQuery.cs rename to LiteCharms.Features/Shop/Products/Queries/GetProductsQuery.cs index 1c7082d..a60670d 100644 --- a/LiteCharms.Features/Products/Queries/GetProductsQuery.cs +++ b/LiteCharms.Features/Shop/Products/Queries/GetProductsQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Products.Models; namespace LiteCharms.Features.Products.Queries; diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPriceQueryHandler.cs similarity index 93% rename from LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs rename to LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPriceQueryHandler.cs index f73bced..c1383de 100644 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs +++ b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPriceQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Products.Models; namespace LiteCharms.Features.Products.Queries.Handlers; diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPricesQueryHandler.cs similarity index 91% rename from LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs rename to LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPricesQueryHandler.cs index c9eac0a..1bc5489 100644 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs +++ b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPricesQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Products.Models; namespace LiteCharms.Features.Products.Queries.Handlers; diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductQueryHandler.cs similarity index 90% rename from LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs rename to LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductQueryHandler.cs index 6cf09c6..d60591e 100644 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs +++ b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Products.Models; namespace LiteCharms.Features.Products.Queries.Handlers; diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductsQueryHandler.cs similarity index 90% rename from LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs rename to LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductsQueryHandler.cs index 3fc0366..0ad9f40 100644 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs +++ b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductsQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Products.Models; namespace LiteCharms.Features.Products.Queries.Handlers; diff --git a/LiteCharms.Features/Quotes/Commands/AssignQuoteToOrderCommand.cs b/LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToOrderCommand.cs similarity index 100% rename from LiteCharms.Features/Quotes/Commands/AssignQuoteToOrderCommand.cs rename to LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToOrderCommand.cs diff --git a/LiteCharms.Features/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs b/LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs similarity index 100% rename from LiteCharms.Features/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs rename to LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs b/LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs similarity index 97% rename from LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs rename to LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs index 3145cd4..266e8d9 100644 --- a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs +++ b/LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Quotes.Commands.Handlers; diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs similarity index 96% rename from LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs rename to LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs index 443c907..a9f6b12 100644 --- a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs +++ b/LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Quotes.Commands.Handlers; diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs b/LiteCharms.Features/Shop/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs similarity index 95% rename from LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs rename to LiteCharms.Features/Shop/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs index a279370..8e80a52 100644 --- a/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs +++ b/LiteCharms.Features/Shop/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Quotes.Commands.Handlers; diff --git a/LiteCharms.Features/Quotes/Commands/UpdateQuoteStatusCommand.cs b/LiteCharms.Features/Shop/Quotes/Commands/UpdateQuoteStatusCommand.cs similarity index 90% rename from LiteCharms.Features/Quotes/Commands/UpdateQuoteStatusCommand.cs rename to LiteCharms.Features/Shop/Quotes/Commands/UpdateQuoteStatusCommand.cs index 901bc46..14c9684 100644 --- a/LiteCharms.Features/Quotes/Commands/UpdateQuoteStatusCommand.cs +++ b/LiteCharms.Features/Shop/Quotes/Commands/UpdateQuoteStatusCommand.cs @@ -1,4 +1,5 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop; +using LiteCharms.Models; namespace LiteCharms.Features.Quotes.Commands; diff --git a/LiteCharms.Entities/Quote.cs b/LiteCharms.Features/Shop/Quotes/Entities/Quote.cs similarity index 54% rename from LiteCharms.Entities/Quote.cs rename to LiteCharms.Features/Shop/Quotes/Entities/Quote.cs index 8bacd89..4ea6dab 100644 --- a/LiteCharms.Entities/Quote.cs +++ b/LiteCharms.Features/Shop/Quotes/Entities/Quote.cs @@ -1,13 +1,15 @@ -using LiteCharms.Entities.Configuration; +using LiteCharms.Features.Shop.Customers.Entities; +using LiteCharms.Features.Shop.Orders.Entities; +using LiteCharms.Features.Shop.ShoppingCarts.Entities; -namespace LiteCharms.Entities; +namespace LiteCharms.Features.Shop.Quotes.Entities; [EntityTypeConfiguration] public class Quote : Models.Quote { public virtual Customer? Customer { get; set; } - public virtual ShoppingCart? ShoppingCart { get; set; } - public virtual Order? Order { get; set; } + + public virtual ShoppingCart? ShoppingCart { get; set; } } diff --git a/LiteCharms.Features/Shop/Quotes/Entities/QuoteConfiguration.cs b/LiteCharms.Features/Shop/Quotes/Entities/QuoteConfiguration.cs new file mode 100644 index 0000000..299044e --- /dev/null +++ b/LiteCharms.Features/Shop/Quotes/Entities/QuoteConfiguration.cs @@ -0,0 +1,33 @@ +namespace LiteCharms.Features.Shop.Quotes.Entities; + +public class QuoteConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Quote)); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.ExpiredAt).IsRequired(false); + builder.Property(f => f.CustomerId).IsRequired(); + builder.Property(f => f.OrderId); + builder.Property(f => f.ShoppingCartId); + builder.Property(f => f.Status).IsRequired().HasConversion(); + builder.Property(f => f.InvoiceUrl).IsRequired(false).HasMaxLength(2048); + builder.Property(f => f.Reason).IsRequired(false); + + builder.HasOne(q => q.Customer) + .WithMany(c => c.Quotes) + .HasForeignKey(q => q.CustomerId) + .IsRequired(); + + builder.HasOne(q => q.Order) + .WithOne(o => o.Quote) + .HasForeignKey(q => q.OrderId); + + builder.HasOne(q => q.ShoppingCart) + .WithOne(o => o.Quote) + .HasForeignKey(q => q.ShoppingCartId); + } +} diff --git a/LiteCharms.Models/Quote.cs b/LiteCharms.Features/Shop/Quotes/Models/Quote.cs similarity index 64% rename from LiteCharms.Models/Quote.cs rename to LiteCharms.Features/Shop/Quotes/Models/Quote.cs index 955d0e7..5422348 100644 --- a/LiteCharms.Models/Quote.cs +++ b/LiteCharms.Features/Shop/Quotes/Models/Quote.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.Quotes.Models; public class Quote { @@ -12,9 +12,13 @@ public class Quote public Guid CustomerId { get; set; } - public Guid ShoppingCartId { get; set; } + public Guid? OrderId { get; set; } + + public Guid? ShoppingCartId { get; set; } public QuoteStatus Status { get; set; } + public string? InvoiceUrl { get; set; } + public string? Reason { get; set; } } diff --git a/LiteCharms.Features/Quotes/Queries/GetCustomerQuotesQuery.cs b/LiteCharms.Features/Shop/Quotes/Queries/GetCustomerQuotesQuery.cs similarity index 90% rename from LiteCharms.Features/Quotes/Queries/GetCustomerQuotesQuery.cs rename to LiteCharms.Features/Shop/Quotes/Queries/GetCustomerQuotesQuery.cs index d531b3a..37adc17 100644 --- a/LiteCharms.Features/Quotes/Queries/GetCustomerQuotesQuery.cs +++ b/LiteCharms.Features/Shop/Quotes/Queries/GetCustomerQuotesQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Quotes.Models; namespace LiteCharms.Features.Quotes.Queries; diff --git a/LiteCharms.Features/Quotes/Queries/GetQuoteQuery.cs b/LiteCharms.Features/Shop/Quotes/Queries/GetQuoteQuery.cs similarity index 89% rename from LiteCharms.Features/Quotes/Queries/GetQuoteQuery.cs rename to LiteCharms.Features/Shop/Quotes/Queries/GetQuoteQuery.cs index 0f566cf..756788e 100644 --- a/LiteCharms.Features/Quotes/Queries/GetQuoteQuery.cs +++ b/LiteCharms.Features/Shop/Quotes/Queries/GetQuoteQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Quotes.Models; namespace LiteCharms.Features.Quotes.Queries; diff --git a/LiteCharms.Features/Quotes/Queries/GetQuotesQuery.cs b/LiteCharms.Features/Shop/Quotes/Queries/GetQuotesQuery.cs similarity index 93% rename from LiteCharms.Features/Quotes/Queries/GetQuotesQuery.cs rename to LiteCharms.Features/Shop/Quotes/Queries/GetQuotesQuery.cs index 20a8d4b..4e7db83 100644 --- a/LiteCharms.Features/Quotes/Queries/GetQuotesQuery.cs +++ b/LiteCharms.Features/Shop/Quotes/Queries/GetQuotesQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.Quotes.Models; namespace LiteCharms.Features.Quotes.Queries; diff --git a/LiteCharms.Features/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs b/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs similarity index 93% rename from LiteCharms.Features/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs rename to LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs index 11c2169..811df36 100644 --- a/LiteCharms.Features/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs +++ b/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs @@ -1,7 +1,7 @@ using LiteCharms.Extensions; using LiteCharms.Features.Quotes.Queries; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Quotes.Models; namespace LiteCharms.Features.Quotes.Queries.Handlers; diff --git a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs b/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs similarity index 91% rename from LiteCharms.Features/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs rename to LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs index 8986161..35c2d39 100644 --- a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs +++ b/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Quotes.Models; namespace LiteCharms.Features.Quotes.Queries.Handlers; diff --git a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuotesHandler.cs b/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuotesHandler.cs similarity index 93% rename from LiteCharms.Features/Quotes/Queries/Handlers/GetQuotesHandler.cs rename to LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuotesHandler.cs index 3d548b0..f7ad77b 100644 --- a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuotesHandler.cs +++ b/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuotesHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Quotes.Models; namespace LiteCharms.Features.Quotes.Queries.Handlers; diff --git a/LiteCharms.Features/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs similarity index 100% rename from LiteCharms.Features/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs diff --git a/LiteCharms.Features/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs similarity index 100% rename from LiteCharms.Features/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs diff --git a/LiteCharms.Features/ShoppingCarts/Commands/CreateShoppingCartCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/CreateShoppingCartCommand.cs similarity index 100% rename from LiteCharms.Features/ShoppingCarts/Commands/CreateShoppingCartCommand.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/CreateShoppingCartCommand.cs diff --git a/LiteCharms.Features/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs similarity index 100% rename from LiteCharms.Features/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs similarity index 97% rename from LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs index dc4a50d..7a2cd2c 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs similarity index 97% rename from LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs index 67e7949..a634375 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs similarity index 96% rename from LiteCharms.Features/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs index 241af60..2d51f2e 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs similarity index 96% rename from LiteCharms.Features/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs index 3f3ce2f..0043a5f 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs similarity index 97% rename from LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs index 6294199..2bd5b3a 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs similarity index 97% rename from LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs index 73255c5..442c944 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs similarity index 96% rename from LiteCharms.Features/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs index 42b362f..3e54242 100644 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs @@ -1,4 +1,4 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; diff --git a/LiteCharms.Features/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs similarity index 100% rename from LiteCharms.Features/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs diff --git a/LiteCharms.Features/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs similarity index 100% rename from LiteCharms.Features/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs diff --git a/LiteCharms.Features/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs similarity index 100% rename from LiteCharms.Features/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs diff --git a/LiteCharms.Entities/ShoppingCart.cs b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCart.cs similarity index 54% rename from LiteCharms.Entities/ShoppingCart.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCart.cs index ead1175..cfb4bd5 100644 --- a/LiteCharms.Entities/ShoppingCart.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCart.cs @@ -1,6 +1,8 @@ -using LiteCharms.Entities.Configuration; +using LiteCharms.Features.Shop.Customers.Entities; +using LiteCharms.Features.Shop.Orders.Entities; +using LiteCharms.Features.Shop.Quotes.Entities; -namespace LiteCharms.Entities; +namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; [EntityTypeConfiguration] public class ShoppingCart : Models.ShoppingCart @@ -13,5 +15,5 @@ public class ShoppingCart : Models.ShoppingCart public virtual ICollection? ShoppingCartItems { get; set; } - public virtual ICollection? Packages { get; set; } + public virtual ICollection? ShoppingCartPackages { get; set; } } diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs new file mode 100644 index 0000000..e45a04b --- /dev/null +++ b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; + +public class ShoppingCartConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(ShoppingCart)); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.CustomerId).IsRequired(); + builder.Property(f => f.OrderId); + + builder.HasOne(s => s.Customer) + .WithMany(c => c.ShoppingCarts) + .HasForeignKey(s => s.CustomerId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(s => s.Order) + .WithOne(o => o.ShoppingCart) + .HasForeignKey(s => s.OrderId) + .OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/LiteCharms.Entities/ShoppingCartItem.cs b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItem.cs similarity index 61% rename from LiteCharms.Entities/ShoppingCartItem.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItem.cs index d5e5634..50afb9d 100644 --- a/LiteCharms.Entities/ShoppingCartItem.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItem.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Entities; +using LiteCharms.Features.Shop.Products.Entities; + +namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; public class ShoppingCartItem : Models.ShoppingCartItem { diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs new file mode 100644 index 0000000..3dbc5f7 --- /dev/null +++ b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs @@ -0,0 +1,30 @@ +using LiteCharms.Features.Shop.Products.Entities; + +namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; + +public class ShoppingCartItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(ShoppingCartItem)); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.Quantity).IsRequired().HasDefaultValue(1); + builder.Property(f => f.ShoppingCartId).IsRequired(); + builder.Property(f => f.ProductPriceId).IsRequired(); + + builder.HasOne() + .WithMany(s => s.ShoppingCartItems) + .HasForeignKey(f => f.ShoppingCartId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne() + .WithMany() + .HasForeignKey(f => f.ProductPriceId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Entities/ShoppingCartPackage.cs b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackage.cs similarity index 69% rename from LiteCharms.Entities/ShoppingCartPackage.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackage.cs index 45a3831..4adf983 100644 --- a/LiteCharms.Entities/ShoppingCartPackage.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackage.cs @@ -1,6 +1,6 @@ -using LiteCharms.Entities.Configuration; +using LiteCharms.Features.Shop.CartPackages.Entities; -namespace LiteCharms.Entities; +namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; [EntityTypeConfiguration] public class ShoppingCartPackage : Models.ShoppingCartPackage diff --git a/LiteCharms.Entities/Configuration/ShoppingCartPackageConfiguration.cs b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs similarity index 54% rename from LiteCharms.Entities/Configuration/ShoppingCartPackageConfiguration.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs index 7d2f296..ee08e91 100644 --- a/LiteCharms.Entities/Configuration/ShoppingCartPackageConfiguration.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Entities.Configuration; +namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration { @@ -7,18 +7,20 @@ public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration f.Id); - builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(f => f.ShoppingCartId).IsRequired(); builder.Property(f => f.PackageId).IsRequired(); - builder.HasOne(f => f.Package) - .WithMany() - .HasForeignKey(f => f.PackageId) - .OnDelete(DeleteBehavior.NoAction); - builder.HasOne(f => f.ShoppingCart) - .WithMany() - .HasForeignKey(f => f.ShoppingCartId) - .OnDelete(DeleteBehavior.NoAction); + .WithMany(s => s.ShoppingCartPackages) + .HasForeignKey(scp => scp.ShoppingCartId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Package) + .WithMany() + .HasForeignKey(scp => scp.PackageId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); } } diff --git a/LiteCharms.Models/ShoppingCart.cs b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCart.cs similarity index 64% rename from LiteCharms.Models/ShoppingCart.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCart.cs index 53e27b9..fe6f633 100644 --- a/LiteCharms.Models/ShoppingCart.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCart.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.ShoppingCarts.Models; public class ShoppingCart { @@ -8,9 +8,7 @@ public class ShoppingCart public DateTimeOffset? UpdatedAt { get; set; } - public Guid? CustomerId { get; set; } + public Guid CustomerId { get; set; } public Guid? OrderId { get; set; } - - public Guid? QuoteId { get; set; } } diff --git a/LiteCharms.Models/ShoppingCartItem.cs b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartItem.cs similarity index 83% rename from LiteCharms.Models/ShoppingCartItem.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartItem.cs index 3530515..8dcc0be 100644 --- a/LiteCharms.Models/ShoppingCartItem.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartItem.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.ShoppingCarts.Models; public class ShoppingCartItem { diff --git a/LiteCharms.Models/ShoppingCartPackage.cs b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartPackage.cs similarity index 77% rename from LiteCharms.Models/ShoppingCartPackage.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartPackage.cs index 673b577..fdc6d7b 100644 --- a/LiteCharms.Models/ShoppingCartPackage.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartPackage.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.Shop.ShoppingCarts.Models; public class ShoppingCartPackage { diff --git a/LiteCharms.Features/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs similarity index 90% rename from LiteCharms.Features/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs index 0d64287..e901ee3 100644 --- a/LiteCharms.Features/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.ShoppingCarts.Models; namespace LiteCharms.Features.ShoppingCarts.Queries; diff --git a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs similarity index 90% rename from LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs index 197d475..e6ddec3 100644 --- a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.ShoppingCarts.Models; namespace LiteCharms.Features.ShoppingCarts.Queries; diff --git a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs similarity index 90% rename from LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs index 2f64359..611b48f 100644 --- a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.ShoppingCarts.Models; namespace LiteCharms.Features.ShoppingCarts.Queries; diff --git a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartQuery.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartQuery.cs similarity index 90% rename from LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartQuery.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartQuery.cs index 07dfc5a..1ef9784 100644 --- a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartQuery.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartQuery.cs @@ -1,4 +1,4 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.ShoppingCarts.Models; namespace LiteCharms.Features.ShoppingCarts.Queries; diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs similarity index 93% rename from LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs index d6bde0f..8554fc6 100644 --- a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs @@ -1,7 +1,7 @@ using LiteCharms.Extensions; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.ShoppingCarts.Models; using LiteCharms.Features.ShoppingCarts.Queries; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs similarity index 93% rename from LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs index 0e7104c..0082f85 100644 --- a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.ShoppingCarts.Models; namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs similarity index 93% rename from LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs index d6727ee..68e17cf 100644 --- a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.ShoppingCarts.Models; namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs similarity index 91% rename from LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs rename to LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs index 6793e22..2cf05a9 100644 --- a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.ShoppingCarts.Models; namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; diff --git a/LiteCharms.Features/Utilities/Hash/Commands/ComputeHashCommand.cs b/LiteCharms.Features/Utilities/Hash/Commands/ComputeHashCommand.cs deleted file mode 100644 index 252a9e7..0000000 --- a/LiteCharms.Features/Utilities/Hash/Commands/ComputeHashCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace LiteCharms.Features.Utilities.Hash.Commands; - -public class ComputeHashCommand : IRequest> -{ - public string? Input { get; set; } - - private ComputeHashCommand(string input) => Input = input; - - public static ComputeHashCommand Create(string input) - { - if(string.IsNullOrWhiteSpace(input)) - throw new ArgumentException("Input is required", nameof(input)); - - return new(input); - } -} diff --git a/LiteCharms.Features/Utilities/Hash/Commands/Handlers/ComputeHashCommandHandler.cs b/LiteCharms.Features/Utilities/Hash/Commands/Handlers/ComputeHashCommandHandler.cs deleted file mode 100644 index 013ab03..0000000 --- a/LiteCharms.Features/Utilities/Hash/Commands/Handlers/ComputeHashCommandHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -using LiteCharms.Features.Utilities.Hash.Commands; - -namespace LiteCharms.Features.Utilities.Hash.Commands.Handlers; - -public class ComputeHashCommandHandler : IRequestHandler> -{ - public async ValueTask> Handle(ComputeHashCommand request, CancellationToken cancellationToken) - { - try - { - var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(request.Input!)); - - return Result.Ok(Convert.ToHexString(bytes)); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj b/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj deleted file mode 100644 index b387b65..0000000 --- a/LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj +++ /dev/null @@ -1,109 +0,0 @@ - - - - net10.0 - enable - enable - 7770ab3b-72ee-4897-8e06-57d6613e050a - True - ..\LiteCharms.snk - - - - - LiteCharms.Infrastructure - 1.0.20 - Khwezi Mngoma - Lite Charms (PTY) Ltd - Infrastructure components for Lite Charms applications. - https://gitea.khongisa.co.za/litecharms/components - https://gitea.khongisa.co.za/litecharms/components.git - git - LICENSE - icon.png - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - - - - - - diff --git a/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs b/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs deleted file mode 100644 index 145ce46..0000000 --- a/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs +++ /dev/null @@ -1,37 +0,0 @@ -using LiteCharms.Infrastructure.ServiceBus.Queues; - -namespace LiteCharms.Infrastructure.ServiceBus.Exchanges; - -public class EmailExchange(EmailQueue messages, ILogger logger, IPublisher mediator) : BackgroundService -{ - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - while (messages.Incoming.TryRead(out var message)) - { - try - { - switch (message.Name) - { - case "SendShopEmailEnquiryEvent": - await mediator.Publish(message, stoppingToken); - break; - case "ProcessEmailNotificationsEvent": - await mediator.Publish(message, stoppingToken); - break; - default: - logger.LogWarning("Unsupported email event {Event}", message.Name); - break; - } - } - catch (Exception ex) - { - logger.LogError(ex, ex.Message); - } - } - - await Task.Delay(1000, stoppingToken); - } - } -} diff --git a/LiteCharms.Infrastructure/ServiceBus/Queues/EmailQueue.cs b/LiteCharms.Infrastructure/ServiceBus/Queues/EmailQueue.cs deleted file mode 100644 index 24b161e..0000000 --- a/LiteCharms.Infrastructure/ServiceBus/Queues/EmailQueue.cs +++ /dev/null @@ -1,5 +0,0 @@ -using LiteCharms.Abstractions; - -namespace LiteCharms.Infrastructure.ServiceBus.Queues; - -public class EmailQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Infrastructure/ServiceBus/Queues/GeneralQueue.cs b/LiteCharms.Infrastructure/ServiceBus/Queues/GeneralQueue.cs deleted file mode 100644 index f7024f6..0000000 --- a/LiteCharms.Infrastructure/ServiceBus/Queues/GeneralQueue.cs +++ /dev/null @@ -1,5 +0,0 @@ -using LiteCharms.Abstractions; - -namespace LiteCharms.Infrastructure.ServiceBus.Queues; - -public class GeneralQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Infrastructure/ServiceBus/Queues/SalesQueue.cs b/LiteCharms.Infrastructure/ServiceBus/Queues/SalesQueue.cs deleted file mode 100644 index 714ac11..0000000 --- a/LiteCharms.Infrastructure/ServiceBus/Queues/SalesQueue.cs +++ /dev/null @@ -1,5 +0,0 @@ -using LiteCharms.Abstractions; - -namespace LiteCharms.Infrastructure.ServiceBus.Queues; - -public class SalesQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Infrastructure/appsettings.json b/LiteCharms.Infrastructure/appsettings.json deleted file mode 100644 index 0db3279..0000000 --- a/LiteCharms.Infrastructure/appsettings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} diff --git a/LiteCharms.Models/LiteCharms.Models.csproj b/LiteCharms.Models/LiteCharms.Models.csproj deleted file mode 100644 index 95e00d1..0000000 --- a/LiteCharms.Models/LiteCharms.Models.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - net10.0 - enable - enable - True - ..\LiteCharms.snk - - - - - LiteCharms.Models - 1.0.20 - Khwezi Mngoma - Lite Charms (PTY) Ltd - Shared models for Lite Charms applications. - https://gitea.khongisa.co.za/litecharms/components - https://gitea.khongisa.co.za/litecharms/components.git - git - LICENSE - utility;dotnet - icon.png - - - - - - - - - - - - diff --git a/LiteCharms.Models/Product.cs b/LiteCharms.Models/Product.cs deleted file mode 100644 index 406452c..0000000 --- a/LiteCharms.Models/Product.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace LiteCharms.Models; - -public class Product -{ - public Guid Id { get; set; } - - public string? Name { get; set; } - - public string? Description { get; set; } - - public bool Active { get; set; } -} diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index 05f4368..21e11b6 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -2,10 +2,6 @@ - - - + - - From 42001998d6850b6eedab555e7bffeae30f499225 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 13 May 2026 20:15:21 +0200 Subject: [PATCH 023/153] Fixed package references and namespaces Refactored mappers --- LiteCharms.Features.Tests/CommonFixture.cs | 2 +- LiteCharms.Features/Extensions/Constants.cs | 5 -- .../Extensions/EntityModeMappers.cs | 72 +++++++++++-------- .../Extensions/HealthChecks.cs | 4 +- LiteCharms.Features/Extensions/Monitoring.cs | 2 +- LiteCharms.Features/Extensions/Postgres.cs | 4 +- LiteCharms.Features/Extensions/Quartz.cs | 6 +- LiteCharms.Features/Extensions/ServiceBus.cs | 10 +-- .../LiteCharms.Features.csproj | 1 + 9 files changed, 56 insertions(+), 50 deletions(-) delete mode 100644 LiteCharms.Features/Extensions/Constants.cs diff --git a/LiteCharms.Features.Tests/CommonFixture.cs b/LiteCharms.Features.Tests/CommonFixture.cs index b237b8f..358aaf2 100644 --- a/LiteCharms.Features.Tests/CommonFixture.cs +++ b/LiteCharms.Features.Tests/CommonFixture.cs @@ -1,4 +1,4 @@ -using LiteCharms.Extensions; +using LiteCharms.Features.Extensions; namespace LiteCharms.Features.Tests; diff --git a/LiteCharms.Features/Extensions/Constants.cs b/LiteCharms.Features/Extensions/Constants.cs deleted file mode 100644 index b9d11e1..0000000 --- a/LiteCharms.Features/Extensions/Constants.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace LiteCharms.Features.Extensions; - -public static class Constants -{ -} diff --git a/LiteCharms.Features/Extensions/EntityModeMappers.cs b/LiteCharms.Features/Extensions/EntityModeMappers.cs index 9a1d97a..baeb69c 100644 --- a/LiteCharms.Features/Extensions/EntityModeMappers.cs +++ b/LiteCharms.Features/Extensions/EntityModeMappers.cs @@ -1,29 +1,36 @@ -using LiteCharms.Models; +using LiteCharms.Features.Shop.CartPackages.Models; +using LiteCharms.Features.Shop.Customers.Models; +using LiteCharms.Features.Shop.Leads.Models; +using LiteCharms.Features.Shop.Notifications.Models; +using LiteCharms.Features.Shop.Orders.Models; +using LiteCharms.Features.Shop.Products.Models; +using LiteCharms.Features.Shop.Quotes.Models; +using LiteCharms.Features.Shop.ShoppingCarts.Models; -namespace LiteCharms.Extensions; +namespace LiteCharms.Features.Extensions; public static class EntityModeMappers { - public static ShoppingCartPackage ToModel(this Entities.ShoppingCartPackage entity) => + public static ShoppingCartPackage ToModel(this Shop.ShoppingCarts.Entities.ShoppingCartPackage entity) => new() { Id = entity.Id, CreatedAt = entity.CreatedAt, PackageId = entity.PackageId, - ShoppingCartId = entity.ShoppingCartId + ShoppingCartId = entity.ShoppingCartId }; - public static PackageItem ToModel(this Entities.PackageItem entity) => + public static PackageItem ToModel(this Shop.CartPackages.Entities.PackageItem entity) => new() { Id = entity.Id, Active = entity.Active, CreatedAt = entity.CreatedAt, PackageId = entity.PackageId, - ProductPriceId = entity.ProductPriceId + ProductPriceId = entity.ProductPriceId }; - public static Package ToModel(this Entities.Package entity) => + public static Package ToModel(this Shop.CartPackages.Entities.Package entity) => new() { Id = entity.Id, @@ -31,10 +38,12 @@ public static class EntityModeMappers Active = entity.Active, Description = entity.Description, Name = entity.Name, - UpdatedAt = entity.UpdatedAt + UpdatedAt = entity.UpdatedAt, + ImageUrl = entity.ImageUrl, + Summary = entity.Summary }; - public static ShoppingCartItem ToModel(this Entities.ShoppingCartItem entity) => + public static ShoppingCartItem ToModel(this Shop.ShoppingCarts.Entities.ShoppingCartItem entity) => new() { Id = entity.Id, @@ -42,21 +51,20 @@ public static class EntityModeMappers UpdatedAt = entity.UpdatedAt, ProductPriceId = entity.ProductPriceId, Quantity = entity.Quantity, - ShoppingCartId = entity.ShoppingCartId + ShoppingCartId = entity.ShoppingCartId }; - public static ShoppingCart ToModel(this Entities.ShoppingCart entity) => + public static ShoppingCart ToModel(this Shop.ShoppingCarts.Entities.ShoppingCart entity) => new() { Id = entity.Id, CreatedAt = entity.CreatedAt, UpdatedAt = entity.UpdatedAt, CustomerId = entity.CustomerId, - OrderId = entity.OrderId, - QuoteId = entity.QuoteId + OrderId = entity.OrderId }; - public static Quote ToModel(this Entities.Quote entity) => + public static Quote ToModel(this Shop.Quotes.Entities.Quote entity) => new() { Id = entity.Id, @@ -66,10 +74,12 @@ public static class EntityModeMappers ExpiredAt = entity.ExpiredAt, Reason = entity.Reason, ShoppingCartId = entity.ShoppingCartId, - Status = entity.Status + Status = entity.Status, + InvoiceUrl = entity.InvoiceUrl, + OrderId = entity.OrderId }; - public static Notification ToModel(this Entities.Notification entity) => + public static Notification ToModel(this Shop.Notifications.Entities.Notification entity) => new() { Id = entity.Id, @@ -93,7 +103,7 @@ public static class EntityModeMappers Errors = entity.Errors }; - public static Customer ToModel(this Entities.Customer entity) => + public static Customer ToModel(this Shop.Customers.Entities.Customer entity) => new() { Id = entity.Id, @@ -118,7 +128,7 @@ public static class EntityModeMappers Whatsapp = entity.Whatsapp }; - public static Lead ToModel(this Entities.Lead entity) => + public static Lead ToModel(this Shop.Leads.Entities.Lead entity) => new() { Id = entity.Id, @@ -136,10 +146,10 @@ public static class EntityModeMappers ClickId = entity.ClickId, TargetId = entity.TargetId, WebClickId = entity.WebClickId, - Status = entity.Status + Status = entity.Status }; - public static Order ToModel(this Entities.Order entity) => + public static Order ToModel(this Shop.Orders.Entities.Order entity) => new() { Id = entity.Id, @@ -147,35 +157,35 @@ public static class EntityModeMappers UpdatedAt = entity.UpdatedAt, CustomerId = entity.CustomerId, Notes = entity.Notes, - RefundId = entity.RefundId, - QuoteId = entity.QuoteId, Status = entity.Status, - ShoppingCartId = entity.ShoppingCartId, - DepositRequired = entity.DepositRequired, Requirements = entity.Requirements, - Terms = entity.Terms + Terms = entity.Terms, + InvoiceUrl = entity.InvoiceUrl }; - public static OrderRefund ToModel(this Entities.OrderRefund entity) => + public static OrderRefund ToModel(this Shop.Orders.Entities.OrderRefund entity) => new() { Id = entity.Id, CreatedAt = entity.CreatedAt, OrderId = entity.OrderId, Reason = entity.Reason, - Amount = entity.Amount + Amount = entity.Amount }; - public static Product ToModel(this Entities.Product entity) => + public static Product ToModel(this Shop.Products.Entities.Product entity) => new() { Id = entity.Id, Name = entity.Name, Description = entity.Description, - Active = entity.Active + Active = entity.Active, + Summary = entity.Summary, + ImageUrl = entity.ImageUrl, + Thumbnails = entity.Thumbnails }; - public static ProductPrice ToModel(this Entities.ProductPrice entity) => + public static ProductPrice ToModel(this Shop.Products.Entities.ProductPrice entity) => new() { Id = entity.Id, @@ -184,6 +194,6 @@ public static class EntityModeMappers Active = entity.Active, CreatedAt = entity.CreatedAt, Discount = entity.Discount, - UpdatedAt = entity.UpdatedAt + UpdatedAt = entity.UpdatedAt }; } diff --git a/LiteCharms.Features/Extensions/HealthChecks.cs b/LiteCharms.Features/Extensions/HealthChecks.cs index 01bb1ae..ebabd8f 100644 --- a/LiteCharms.Features/Extensions/HealthChecks.cs +++ b/LiteCharms.Features/Extensions/HealthChecks.cs @@ -1,6 +1,6 @@ -using LiteCharms.Infrastructure.HealthChecks; +using LiteCharms.Features.HealthChecks; -namespace LiteCharms.Extensions; +namespace LiteCharms.Features.Extensions; public static class HealthChecks { diff --git a/LiteCharms.Features/Extensions/Monitoring.cs b/LiteCharms.Features/Extensions/Monitoring.cs index a5b52da..8e5db29 100644 --- a/LiteCharms.Features/Extensions/Monitoring.cs +++ b/LiteCharms.Features/Extensions/Monitoring.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Extensions; +namespace LiteCharms.Features.Extensions; public static class Monitoring { diff --git a/LiteCharms.Features/Extensions/Postgres.cs b/LiteCharms.Features/Extensions/Postgres.cs index 50c420c..0e70287 100644 --- a/LiteCharms.Features/Extensions/Postgres.cs +++ b/LiteCharms.Features/Extensions/Postgres.cs @@ -1,6 +1,6 @@ -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.Shop.Postgres; -namespace LiteCharms.Extensions; +namespace LiteCharms.Features.Extensions; public static class Postgres { diff --git a/LiteCharms.Features/Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs index a92d462..e22b1ca 100644 --- a/LiteCharms.Features/Extensions/Quartz.cs +++ b/LiteCharms.Features/Extensions/Quartz.cs @@ -1,7 +1,7 @@ -using LiteCharms.Abstractions; -using LiteCharms.Infrastructure.Quartz; +using LiteCharms.Features.Quartz; +using LiteCharms.Features.Quartz.Abstractions; -namespace LiteCharms.Extensions; +namespace LiteCharms.Features.Extensions; public static class Quartz { diff --git a/LiteCharms.Features/Extensions/ServiceBus.cs b/LiteCharms.Features/Extensions/ServiceBus.cs index ab8d7c0..3a57832 100644 --- a/LiteCharms.Features/Extensions/ServiceBus.cs +++ b/LiteCharms.Features/Extensions/ServiceBus.cs @@ -1,9 +1,9 @@ -using LiteCharms.Abstractions; -using LiteCharms.Infrastructure.ServiceBus; -using LiteCharms.Infrastructure.ServiceBus.Exchanges; -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.ServiceBus; +using LiteCharms.Features.ServiceBus.Abstractions; +using LiteCharms.Features.ServiceBus.Exchanges; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Extensions; +namespace LiteCharms.Features.Extensions; public static class ServiceBus { diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 3d6552b..9ad7508 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -130,6 +130,7 @@ + From 134d8429c00e8a3f91bab7676b5ce408ce84c31a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 14 May 2026 01:33:21 +0200 Subject: [PATCH 024/153] Completed refactor --- LiteCharms.Features.Tests/CommonFixture.cs | 1 + .../NotificationsFeatureTests.cs | 32 +- LiteCharms.Features/Email/EmailService.cs | 8 +- .../SendShopEmailEnquiryEventHandler.cs | 35 +- LiteCharms.Features/Email/IEmailService.cs | 15 - .../Setup.cs => Extensions/Email.cs} | 9 +- .../Extensions/EntityModeMappers.cs | 30 +- LiteCharms.Features/Extensions/Shop.cs | 27 ++ LiteCharms.Features/Models/DateRange.cs | 10 + .../Commands/AddPackageItemsCommand.cs | 25 -- .../Commands/CreatePackageCommand.cs | 23 -- .../Commands/DeletePackageCommand.cs | 25 -- .../Commands/DeletePackageItemsCommand.cs | 16 - .../Handlers/AddPackageItemCommandHandler.cs | 38 --- .../Handlers/CreatePackageCommandHandler.cs | 32 -- .../DeletePackageItemCommandHandler.cs | 32 -- .../DeletePackageItemsCommandHandler.cs | 29 -- .../Handlers/UpdatePackageCommandHandler.cs | 33 -- .../UpdatePackageStatusCommandHandler.cs | 29 -- .../Commands/UpdatePackageCommand.cs | 28 -- .../Commands/UpdatePackageStatusCommand.cs | 22 -- .../Shop/CartPackages/PackageService.cs | 243 ++++++++++++++ .../Queries/GetPackageItemsQuery.cs | 18 -- .../CartPackages/Queries/GetPackageQuery.cs | 18 -- .../CartPackages/Queries/GetPackagesQuery.cs | 33 -- .../Handlers/GetPackageItemsQueryHandler.cs | 32 -- .../Handlers/GetPackageQueryHandler.cs | 27 -- .../Handlers/GetPackagesQueryHandler.cs | 35 -- .../Commands/CreateCustomerCommand.cs | 64 ---- .../Handlers/CreateCustomerCommandHandler.cs | 48 --- .../Handlers/UpdateCustomerCommandHandler.cs | 44 --- .../Commands/UpdateCustomerCommand.cs | 70 ---- .../Shop/Customers/CustomerService.cs | 132 ++++++++ .../Shop/Customers/Models/Records.cs | 73 +++++ .../Customers/Queries/GetCustomerQuery.cs | 18 -- .../Customers/Queries/GetCustomersQuery.cs | 30 -- .../Handlers/GetCustomerQueryHandler.cs | 26 -- .../Handlers/GetCustomersQueryHandler.cs | 33 -- .../Shop/Leads/Commands/CreateLeadCommand.cs | 55 ---- .../Handlers/CreateLeadCommandHandler.cs | 47 --- .../Handlers/UpdateLeadCommandHandler.cs | 29 -- .../Shop/Leads/Commands/UpdateLeadCommand.cs | 28 -- LiteCharms.Features/Shop/Leads/LeadService.cs | 116 +++++++ .../Shop/Leads/Models/Records.cs | 28 ++ .../Leads/Queries/GetCustomerLeadsQuery.cs | 30 -- .../Shop/Leads/Queries/GetLeadsQuery.cs | 30 -- .../Handlers/GetCustomerLeadsQueryHandler.cs | 33 -- .../Queries/Handlers/GetLeadsQueryHandler.cs | 33 -- .../Commands/CreateNotificationCommand.cs | 73 ----- .../CreateNotificationCommandHandler.cs | 40 --- .../UpdateNotificationCommandHandler.cs | 35 -- .../Commands/UpdateNotificationCommand.cs | 28 -- .../Entities/NotificationConfiguration.cs | 4 +- .../ProcessEmailNotificationsEventHandler.cs | 64 +++- .../Events/ProcessEmailNotificationsEvent.cs | 2 +- .../Notifications/INotificationService.cs | 9 - .../Shop/Notifications/Models/Notification.cs | 4 +- .../Shop/Notifications/Models/Records.cs | 20 +- .../Shop/Notifications/NotificationService.cs | 114 ++++++- .../Queries/GetNotificationQuery.cs | 18 -- .../Queries/GetNotificationsQuery.cs | 30 -- .../Handlers/GetNotificationQueryHandler.cs | 26 -- .../Handlers/GetNotificationsQueryHandler.cs | 33 -- .../Orders/Commands/CreateOrderCommand.cs | 40 --- .../Handlers/CreateOrderCommandHandler.cs | 46 --- .../UpdateOrderStatusCommandHandler.cs | 29 -- .../Commands/UpdateOrderStatusCommand.cs | 31 -- .../Shop/Orders/Models/Records.cs | 40 +++ .../Shop/Orders/OrderService.cs | 260 +++++++++++++++ .../Orders/Queries/GetCustomerOrdersQuery.cs | 18 -- .../Orders/Queries/GetOrderRefundQuery.cs | 27 -- .../Shop/Orders/Queries/GetOrdersQuery.cs | 30 -- .../Handlers/GetCustomerOrdersQueryHandler.cs | 32 -- .../Handlers/GetOrderRefundQueryHandler.cs | 27 -- .../Queries/Handlers/GetOrdersQueryHandler.cs | 34 -- .../Handlers/RefundCustomerCommandHandler.cs | 39 --- .../UpdateOrderRefundCommandHandler.cs | 31 -- .../Refunds/Commands/RefundCustomerCommand.cs | 38 --- .../Commands/UpdateOrderRefundCommand.cs | 28 -- .../Queries/GetCustomerRefundsQuery.cs | 18 -- .../Orders/Refunds/Queries/GetRefundQuery.cs | 18 -- .../GetCustomerRefundsQueryHandler.cs | 32 -- .../Queries/Handlers/GetRefundQueryHandler.cs | 27 -- .../Shop/Products/Models/Records.cs | 14 + .../Shop/Products/ProductService.cs | 229 ++++++++++++++ .../Products/Queries/GetProductPriceQuery.cs | 18 -- .../Products/Queries/GetProductPricesQuery.cs | 18 -- .../Shop/Products/Queries/GetProductQuery.cs | 18 -- .../Shop/Products/Queries/GetProductsQuery.cs | 18 -- .../Handlers/GetProductPriceQueryHandler.cs | 32 -- .../Handlers/GetProductPricesQueryHandler.cs | 27 -- .../Handlers/GetProductQueryHandler.cs | 26 -- .../Handlers/GetProductsQueryHandler.cs | 27 -- .../Commands/AssignQuoteToOrderCommand.cs | 25 -- .../AssignQuoteToShoppingCartCommand.cs | 25 -- .../AssignQuoteToOrderCommandHandler.cs | 35 -- ...AssignQuoteToShoppingCartCommandHandler.cs | 32 -- .../UpdateQuoteStatusCommandHandler.cs | 29 -- .../Commands/UpdateQuoteStatusCommand.cs | 25 -- .../Quotes/Queries/GetCustomerQuotesQuery.cs | 18 -- .../Shop/Quotes/Queries/GetQuoteQuery.cs | 18 -- .../Shop/Quotes/Queries/GetQuotesQuery.cs | 30 -- .../Handlers/GetCustomerQuotesQueryHandler.cs | 31 -- .../Queries/Handlers/GetQuoteQueryHandler.cs | 26 -- .../Queries/Handlers/GetQuotesHandler.cs | 33 -- .../Shop/Quotes/QuoteService.cs | 154 +++++++++ .../Commands/AddItemToShoppingCartCommand.cs | 30 -- .../AddPackageToShoppingCartCommand.cs | 25 -- .../Commands/CreateShoppingCartCommand.cs | 25 -- .../Commands/EmptyShoppingCartCommand.cs | 16 - .../AddItemToShoppingCartCommandHandler.cs | 40 --- .../AddPackageToShoppingCartCommandHandler.cs | 39 --- .../CreateShoppingCartCommandHandler.cs | 32 -- .../EmptyShoppingCartCommandHandler.cs | 32 -- ...vePackageFromShoppingCartCommandHandler.cs | 35 -- .../RemoveShoppingCartItemCommandHandler.cs | 36 --- .../UpdateShoppingCartItemCommandHandler.cs | 32 -- .../RemovePackageFromShoppingCartCommand.cs | 25 -- .../Commands/RemoveShoppingCartItemCommand.cs | 25 -- .../Commands/UpdateShoppingCartItemCommand.cs | 30 -- .../Queries/GetCustomerShoppingCartsQuery.cs | 18 -- .../Queries/GetShoppingCartItemsQuery.cs | 18 -- .../Queries/GetShoppingCartPackagesQuery.cs | 18 -- .../Queries/GetShoppingCartQuery.cs | 18 -- .../GetCustomerShoppingCartsQueryHandler.cs | 30 -- .../GetShoppingCartItemsQueryHandler.cs | 30 -- .../GetShoppingCartPackagesQueryHandler.cs | 32 -- .../Handlers/GetShoppingCartQueryHandler.cs | 26 -- .../Shop/ShoppingCarts/ShoppingCartService.cs | 298 ++++++++++++++++++ 129 files changed, 1870 insertions(+), 3165 deletions(-) delete mode 100644 LiteCharms.Features/Email/IEmailService.cs rename LiteCharms.Features/{Email/Extensions/Setup.cs => Extensions/Email.cs} (69%) create mode 100644 LiteCharms.Features/Extensions/Shop.cs create mode 100644 LiteCharms.Features/Models/DateRange.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/AddPackageItemsCommand.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/CreatePackageCommand.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageCommand.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageItemsCommand.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageCommand.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageStatusCommand.cs create mode 100644 LiteCharms.Features/Shop/CartPackages/PackageService.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Queries/GetPackageItemsQuery.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Queries/GetPackageQuery.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Queries/GetPackagesQuery.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Customers/Commands/CreateCustomerCommand.cs delete mode 100644 LiteCharms.Features/Shop/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Customers/Commands/UpdateCustomerCommand.cs create mode 100644 LiteCharms.Features/Shop/Customers/CustomerService.cs create mode 100644 LiteCharms.Features/Shop/Customers/Models/Records.cs delete mode 100644 LiteCharms.Features/Shop/Customers/Queries/GetCustomerQuery.cs delete mode 100644 LiteCharms.Features/Shop/Customers/Queries/GetCustomersQuery.cs delete mode 100644 LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomerQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomersQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Leads/Commands/CreateLeadCommand.cs delete mode 100644 LiteCharms.Features/Shop/Leads/Commands/Handlers/CreateLeadCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Leads/Commands/UpdateLeadCommand.cs create mode 100644 LiteCharms.Features/Shop/Leads/LeadService.cs create mode 100644 LiteCharms.Features/Shop/Leads/Models/Records.cs delete mode 100644 LiteCharms.Features/Shop/Leads/Queries/GetCustomerLeadsQuery.cs delete mode 100644 LiteCharms.Features/Shop/Leads/Queries/GetLeadsQuery.cs delete mode 100644 LiteCharms.Features/Shop/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Leads/Queries/Handlers/GetLeadsQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Notifications/Commands/CreateNotificationCommand.cs delete mode 100644 LiteCharms.Features/Shop/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Notifications/Commands/UpdateNotificationCommand.cs delete mode 100644 LiteCharms.Features/Shop/Notifications/INotificationService.cs delete mode 100644 LiteCharms.Features/Shop/Notifications/Queries/GetNotificationQuery.cs delete mode 100644 LiteCharms.Features/Shop/Notifications/Queries/GetNotificationsQuery.cs delete mode 100644 LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Commands/CreateOrderCommand.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Commands/Handlers/CreateOrderCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Commands/UpdateOrderStatusCommand.cs create mode 100644 LiteCharms.Features/Shop/Orders/Models/Records.cs create mode 100644 LiteCharms.Features/Shop/Orders/OrderService.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Queries/GetCustomerOrdersQuery.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Queries/GetOrderRefundQuery.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Queries/GetOrdersQuery.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrdersQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Refunds/Commands/RefundCustomerCommand.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Refunds/Commands/UpdateOrderRefundCommand.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Refunds/Queries/GetCustomerRefundsQuery.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Refunds/Queries/GetRefundQuery.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetRefundQueryHandler.cs create mode 100644 LiteCharms.Features/Shop/Products/Models/Records.cs create mode 100644 LiteCharms.Features/Shop/Products/ProductService.cs delete mode 100644 LiteCharms.Features/Shop/Products/Queries/GetProductPriceQuery.cs delete mode 100644 LiteCharms.Features/Shop/Products/Queries/GetProductPricesQuery.cs delete mode 100644 LiteCharms.Features/Shop/Products/Queries/GetProductQuery.cs delete mode 100644 LiteCharms.Features/Shop/Products/Queries/GetProductsQuery.cs delete mode 100644 LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPriceQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPricesQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductsQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToOrderCommand.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Commands/UpdateQuoteStatusCommand.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Queries/GetCustomerQuotesQuery.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Queries/GetQuoteQuery.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Queries/GetQuotesQuery.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuotesHandler.cs create mode 100644 LiteCharms.Features/Shop/Quotes/QuoteService.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/CreateShoppingCartCommand.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartQuery.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs delete mode 100644 LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs create mode 100644 LiteCharms.Features/Shop/ShoppingCarts/ShoppingCartService.cs diff --git a/LiteCharms.Features.Tests/CommonFixture.cs b/LiteCharms.Features.Tests/CommonFixture.cs index 358aaf2..a73bed2 100644 --- a/LiteCharms.Features.Tests/CommonFixture.cs +++ b/LiteCharms.Features.Tests/CommonFixture.cs @@ -22,6 +22,7 @@ public class CommonFixture : IDisposable Services = new ServiceCollection() .AddMediator() .AddLogging() + .AddShopServices() .AddEmailServiceBus() .AddShopDatabase(Configuration) .AddEmailServices(Configuration) diff --git a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs index 53b8154..ede7fd8 100644 --- a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs +++ b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs @@ -1,19 +1,35 @@ -using LiteCharms.Features.Notifications.Commands; +using LiteCharms.Features.Shop.Notifications; namespace LiteCharms.Features.Tests; -public class NotificationsFeatureTests(CommonFixture fixture) : IClassFixture +public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture { + private readonly NotificationService notificationService = fixture.Services.GetRequiredService(); + [Fact] public async Task CreateNotificationCommand_ShouldSucceed() { - var command = CreateNotification.Create(Models.NotificationDirection.Outgoing, "UnitTest", "khwezi@mngoma.co.za", - "CreateNotificationCommand_ShouldSucceed Test", "Test Message", Models.NotificationPlatforms.Email, Models.Priorities.Medium, - "Khngisa Shop - Test", "shop@litecharms.co.za", Guid.NewGuid().ToString(), Models.CorrelationIdTypes.None, - true, false); + Shop.Notifications.Models.CreateNotification request = new() + { + CorrelationId = Guid.CreateVersion7().ToString(), + CorrelationIdType = Shop.CorrelationIdTypes.None, + Direction = Shop.NotificationDirection.Outgoing, + Platform = Shop.NotificationPlatforms.Email, + Priority = Shop.Priorities.Medium, + Sender = "xUnit Test", + SenderAddress = "khwezi@mngoma.africa", + Recipient = $"{Email.Extensions.Constants.ShopEmailFromName} [Test]", + RecipientAddress = Email.Extensions.Constants.ShopEmailFromAddress, + Subject = "Test Message", + Message = "This is an automation test", + IsHtml = false, + IsInternal = true, + }; - var result = await fixture.Mediator.Send(command); + var createResult = await notificationService.CreateNotificationAsync(request); - Assert.True(result.IsSuccess); + Assert.True(createResult.IsSuccess); + + foreach (var error in createResult.Errors) output.WriteLine(error.Message); } } diff --git a/LiteCharms.Features/Email/EmailService.cs b/LiteCharms.Features/Email/EmailService.cs index 8cb3a82..3e6c103 100644 --- a/LiteCharms.Features/Email/EmailService.cs +++ b/LiteCharms.Features/Email/EmailService.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.Shop; namespace LiteCharms.Features.Email; -public class EmailService(IOptions options) : IEmailService +public class EmailService(IOptions options) : IDisposable { private readonly SmtpSettings settings = options.Value; private readonly SmtpClient client = new(); @@ -14,7 +14,7 @@ public class EmailService(IOptions options) : IEmailService public EmailStatuses Status { get; private set; } = EmailStatuses.Disconnected; - public async Task> SendEmailAsync(Message message, CancellationToken cancellationToken = default) + public async ValueTask> SendEmailAsync(Message message, CancellationToken cancellationToken = default) { using var activity = EmailTelemetry.Source.StartActivity("Email Send"); activity?.SetTag("email.recipient", message.Recipient?.Address); @@ -100,7 +100,7 @@ public class EmailService(IOptions options) : IEmailService } } - public async Task> ConnectAsync(CancellationToken cancellationToken = default) + public async ValueTask> ConnectAsync(CancellationToken cancellationToken = default) { using var activity = EmailTelemetry.Source.StartActivity("Email Connect"); activity?.SetTag("email.smtp.connect", settings.Host); @@ -153,7 +153,7 @@ public class EmailService(IOptions options) : IEmailService } } - public async Task DisconnectAsync(CancellationToken cancellationToken = default) + public async ValueTask DisconnectAsync(CancellationToken cancellationToken = default) { using var activity = EmailTelemetry.Source.StartActivity("Email Disconnect"); activity?.SetTag("email.smtp.disconnect", settings.Host); diff --git a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs index f3e703f..f4173a6 100644 --- a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs +++ b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs @@ -1,22 +1,27 @@ -using LiteCharms.Features.Notifications.Commands; -using LiteCharms.Features.Shop; +using LiteCharms.Features.Shop; +using LiteCharms.Features.Shop.Notifications; using static LiteCharms.Features.Email.Extensions.Constants; namespace LiteCharms.Features.Email.Events.Handlers; -// TODO: Inject the INotificationService -public class SendShopEmailEnquiryEventHandler(ISender mediator) : +public class SendShopEmailEnquiryEventHandler(NotificationService notificationService) : INotificationHandler { - public async ValueTask Handle(SendShopEmailEnquiryEvent notification, CancellationToken cancellationToken) - { - // TODO: Refactor this to use the NotificationService - var command = CreateNotification.Create(NotificationDirection.Outgoing, notification.SenderName!, - notification.SenderAddress!, notification.Subject!, notification.Message!, NotificationPlatforms.Email, - notification.Priority, ShopEmailFromName, ShopEmailFromAddress, Guid.CreateVersion7().ToString(), - CorrelationIdTypes.None, isInternal: true, isHtml: false); - - // TODO: Remove, deprecated - await mediator.Send(command, cancellationToken); - } + public async ValueTask Handle(SendShopEmailEnquiryEvent notification, CancellationToken cancellationToken) => + await notificationService.CreateNotificationAsync(new Shop.Notifications.Models.CreateNotification + { + CorrelationId = notification.CorrelationId, + CorrelationIdType = CorrelationIdTypes.None, + Direction = NotificationDirection.Outgoing, + IsHtml = false, + IsInternal = true, + Message = notification.Message, + Platform = NotificationPlatforms.Email, + Priority = notification.Priority, + Subject = notification.Subject!, + Sender = notification.SenderName!, + SenderAddress = notification.SenderAddress!, + Recipient = ShopEmailFromName, + RecipientAddress = ShopEmailFromAddress + }, cancellationToken); } diff --git a/LiteCharms.Features/Email/IEmailService.cs b/LiteCharms.Features/Email/IEmailService.cs deleted file mode 100644 index bda6a6f..0000000 --- a/LiteCharms.Features/Email/IEmailService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using LiteCharms.Features.Email.Models; -using LiteCharms.Features.Shop; - -namespace LiteCharms.Features.Email; - -public interface IEmailService : IDisposable -{ - EmailStatuses Status { get; } - - Task> SendEmailAsync(Message message, CancellationToken cancellationToken = default); - - Task> ConnectAsync(CancellationToken cancellationToken = default); - - Task DisconnectAsync(CancellationToken cancellationToken = default); -} diff --git a/LiteCharms.Features/Email/Extensions/Setup.cs b/LiteCharms.Features/Extensions/Email.cs similarity index 69% rename from LiteCharms.Features/Email/Extensions/Setup.cs rename to LiteCharms.Features/Extensions/Email.cs index f937dda..ae3756d 100644 --- a/LiteCharms.Features/Email/Extensions/Setup.cs +++ b/LiteCharms.Features/Extensions/Email.cs @@ -1,14 +1,15 @@ -using LiteCharms.Features.Email.Configuration; +using LiteCharms.Features.Email; +using LiteCharms.Features.Email.Configuration; -namespace LiteCharms.Features.Email.Extensions; +namespace LiteCharms.Features.Extensions; -public static class Setup +public static class Email { public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration) { services.Configure(configuration.GetSection("Email")); - services.AddSingleton(); + services.AddSingleton(); services.AddOpenTelemetry() .WithTracing(tracing => tracing.AddSource("LiteCharms.EmailService")) diff --git a/LiteCharms.Features/Extensions/EntityModeMappers.cs b/LiteCharms.Features/Extensions/EntityModeMappers.cs index baeb69c..d73a028 100644 --- a/LiteCharms.Features/Extensions/EntityModeMappers.cs +++ b/LiteCharms.Features/Extensions/EntityModeMappers.cs @@ -11,7 +11,7 @@ namespace LiteCharms.Features.Extensions; public static class EntityModeMappers { - public static ShoppingCartPackage ToModel(this Shop.ShoppingCarts.Entities.ShoppingCartPackage entity) => + public static ShoppingCartPackage ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage entity) => new() { Id = entity.Id, @@ -20,7 +20,7 @@ public static class EntityModeMappers ShoppingCartId = entity.ShoppingCartId }; - public static PackageItem ToModel(this Shop.CartPackages.Entities.PackageItem entity) => + public static PackageItem ToModel(this Features.Shop.CartPackages.Entities.PackageItem entity) => new() { Id = entity.Id, @@ -30,7 +30,7 @@ public static class EntityModeMappers ProductPriceId = entity.ProductPriceId }; - public static Package ToModel(this Shop.CartPackages.Entities.Package entity) => + public static Package ToModel(this Features.Shop.CartPackages.Entities.Package entity) => new() { Id = entity.Id, @@ -43,7 +43,7 @@ public static class EntityModeMappers Summary = entity.Summary }; - public static ShoppingCartItem ToModel(this Shop.ShoppingCarts.Entities.ShoppingCartItem entity) => + public static ShoppingCartItem ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartItem entity) => new() { Id = entity.Id, @@ -54,7 +54,7 @@ public static class EntityModeMappers ShoppingCartId = entity.ShoppingCartId }; - public static ShoppingCart ToModel(this Shop.ShoppingCarts.Entities.ShoppingCart entity) => + public static ShoppingCart ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCart entity) => new() { Id = entity.Id, @@ -64,7 +64,7 @@ public static class EntityModeMappers OrderId = entity.OrderId }; - public static Quote ToModel(this Shop.Quotes.Entities.Quote entity) => + public static Quote ToModel(this Features.Shop.Quotes.Entities.Quote entity) => new() { Id = entity.Id, @@ -79,7 +79,7 @@ public static class EntityModeMappers OrderId = entity.OrderId }; - public static Notification ToModel(this Shop.Notifications.Entities.Notification entity) => + public static Notification ToModel(this Features.Shop.Notifications.Entities.Notification entity) => new() { Id = entity.Id, @@ -89,9 +89,9 @@ public static class EntityModeMappers CorrelationId = entity.CorrelationId, CorrelationIdType = entity.CorrelationIdType, IsInternal = entity.IsInternal, - Sender = entity.Sender, + SenderAddress = entity.SenderAddress, Platform = entity.Platform, - Recipient = entity.Recipient, + RecipientName = entity.RecipientName, Subject = entity.Subject, Processed = entity.Processed, SenderName = entity.SenderName, @@ -103,7 +103,7 @@ public static class EntityModeMappers Errors = entity.Errors }; - public static Customer ToModel(this Shop.Customers.Entities.Customer entity) => + public static Customer ToModel(this Features.Shop.Customers.Entities.Customer entity) => new() { Id = entity.Id, @@ -128,7 +128,7 @@ public static class EntityModeMappers Whatsapp = entity.Whatsapp }; - public static Lead ToModel(this Shop.Leads.Entities.Lead entity) => + public static Lead ToModel(this Features.Shop.Leads.Entities.Lead entity) => new() { Id = entity.Id, @@ -149,7 +149,7 @@ public static class EntityModeMappers Status = entity.Status }; - public static Order ToModel(this Shop.Orders.Entities.Order entity) => + public static Order ToModel(this Features.Shop.Orders.Entities.Order entity) => new() { Id = entity.Id, @@ -163,7 +163,7 @@ public static class EntityModeMappers InvoiceUrl = entity.InvoiceUrl }; - public static OrderRefund ToModel(this Shop.Orders.Entities.OrderRefund entity) => + public static OrderRefund ToModel(this Features.Shop.Orders.Entities.OrderRefund entity) => new() { Id = entity.Id, @@ -173,7 +173,7 @@ public static class EntityModeMappers Amount = entity.Amount }; - public static Product ToModel(this Shop.Products.Entities.Product entity) => + public static Product ToModel(this Features.Shop.Products.Entities.Product entity) => new() { Id = entity.Id, @@ -185,7 +185,7 @@ public static class EntityModeMappers Thumbnails = entity.Thumbnails }; - public static ProductPrice ToModel(this Shop.Products.Entities.ProductPrice entity) => + public static ProductPrice ToModel(this Features.Shop.Products.Entities.ProductPrice entity) => new() { Id = entity.Id, diff --git a/LiteCharms.Features/Extensions/Shop.cs b/LiteCharms.Features/Extensions/Shop.cs new file mode 100644 index 0000000..78502de --- /dev/null +++ b/LiteCharms.Features/Extensions/Shop.cs @@ -0,0 +1,27 @@ +using LiteCharms.Features.Shop.CartPackages; +using LiteCharms.Features.Shop.Customers; +using LiteCharms.Features.Shop.Leads; +using LiteCharms.Features.Shop.Notifications; +using LiteCharms.Features.Shop.Orders; +using LiteCharms.Features.Shop.Products; +using LiteCharms.Features.Shop.Quotes; +using LiteCharms.Features.Shop.ShoppingCarts; + +namespace LiteCharms.Features.Extensions; + +public static class Shop +{ + public static IServiceCollection AddShopServices(this IServiceCollection services) + { + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + return services; + } +} diff --git a/LiteCharms.Features/Models/DateRange.cs b/LiteCharms.Features/Models/DateRange.cs new file mode 100644 index 0000000..c82cba3 --- /dev/null +++ b/LiteCharms.Features/Models/DateRange.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.Models; + +public class DateRange +{ + public DateOnly From { get; set; } + + public DateOnly To { get; set; } + + public int MaxRecords { get; set; } +} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/AddPackageItemsCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/AddPackageItemsCommand.cs deleted file mode 100644 index be87a47..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/AddPackageItemsCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.CartPackages.Commands; - -public class AddPackageItemCommand : IRequest> -{ - public Guid PackageId { get; set; } - - public Guid ProductPriceId { get; set; } - - private AddPackageItemCommand(Guid packageId, Guid productPriceId) - { - PackageId = packageId; - ProductPriceId = productPriceId; - } - - public static AddPackageItemCommand Create(Guid packageId, Guid productPriceId) - { - if (packageId == Guid.Empty) - throw new ArgumentException("Package id is required", nameof(packageId)); - - if (productPriceId == Guid.Empty) - throw new ArgumentException("Product price id is required", nameof(productPriceId)); - - return new(packageId, productPriceId); - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/CreatePackageCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/CreatePackageCommand.cs deleted file mode 100644 index 4a86846..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/CreatePackageCommand.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace LiteCharms.Features.CartPackages.Commands; - -public class CreatePackageCommand : IRequest> -{ - public string? Name { get; set; } - - public string? Description { get; set; } - - private CreatePackageCommand(string? name, string? description) - { - Name = name; - Description = description; - } - - public static CreatePackageCommand Create(string? name, string? description) - { - ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name)); - - ArgumentException.ThrowIfNullOrWhiteSpace(description, nameof(description)); - - return new(name, description); - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageCommand.cs deleted file mode 100644 index 5957dce..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.CartPackages.Commands; - -public class DeletePackageItemCommand : IRequest -{ - public Guid PackageId { get; set; } - - public Guid PackageItemId { get; set; } - - private DeletePackageItemCommand(Guid packageId, Guid packageItemId) - { - PackageId = packageId; - PackageItemId = packageItemId; - } - - public static DeletePackageItemCommand Create(Guid packageId, Guid packageItemId) - { - if (packageId == Guid.Empty) - throw new ArgumentException("Package id is required", nameof(packageId)); - - if (packageItemId == Guid.Empty) - throw new ArgumentException("Product price id is required", nameof(packageItemId)); - - return new(packageId, packageItemId); - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageItemsCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageItemsCommand.cs deleted file mode 100644 index c9aa3e0..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/DeletePackageItemsCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace LiteCharms.Features.CartPackages.Commands; - -public class DeletePackageItemsCommand : IRequest -{ - public Guid PackageId { get; set; } - - private DeletePackageItemsCommand(Guid packageId) => PackageId = packageId; - - public static DeletePackageItemsCommand Create(Guid packageId) - { - if (packageId == Guid.Empty) - throw new ArgumentException("Package ID is required", nameof(packageId)); - - return new(packageId); - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs deleted file mode 100644 index 71c54f7..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/AddPackageItemCommandHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.CartPackages.Commands.Handlers; - -public class AddPackageItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(AddPackageItemCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) - return Result.Fail($"Could not find package by ID {request.PackageId}"); - - if (!await context.ProductPrices.AnyAsync(p => p.Id == request.ProductPriceId && p.Active == true, cancellationToken)) - return Result.Fail($"Could not find an active product price by ID {request.ProductPriceId}"); - - if (await context.PackageItems.AnyAsync(p => p.ProductPriceId == request.ProductPriceId && p.PackageId == request.PackageId, cancellationToken)) - return Result.Fail($"Product price {request.ProductPriceId} is already added to this package {request.PackageId}"); - - var newPackageItem = context.PackageItems.Add(new Entities.PackageItem - { - PackageId = request.PackageId, - ProductPriceId = request.ProductPriceId, - Active = true - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(newPackageItem.Entity.Id) - : Result.Fail($"Failed to add new package item by ID {request.ProductPriceId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs deleted file mode 100644 index ff7847e..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/CreatePackageCommandHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.CartPackages.Commands.Handlers; - -public class CreatePackageCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(CreatePackageCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (await context.Packages.AnyAsync(p => p.Name == request.Name, cancellationToken)) - return Result.Fail($"A package by the same name already exists: {request.Name}"); - - var newPackage = context.Packages.Add(new Entities.Package - { - Name = request.Name, - Description = request.Description, - Active = true - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(newPackage.Entity.Id) - : Result.Fail($"Failed to create a new package by the name: {request.Name}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs deleted file mode 100644 index 7d7284e..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemCommandHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.CartPackages.Commands.Handlers; - -public class DeletePackageItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(DeletePackageItemCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) - return Result.Fail($"Could not find package by ID {request.PackageId}"); - - var item = await context.PackageItems.FirstOrDefaultAsync(p => p.Id == request.PackageItemId && p.PackageId == request.PackageId, cancellationToken); - - if(item is null) - return Result.Fail($"Product item {request.PackageItemId} is already added to this package {request.PackageId}"); - - context.PackageItems.Remove(item); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to delete package item by id {request.PackageItemId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs deleted file mode 100644 index bad0e89..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/DeletePackageItemsCommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.CartPackages.Commands.Handlers; - -public class DeletePackageItemsCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(DeletePackageItemsCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) - return Result.Fail($"Could not find package by ID {request.PackageId}"); - - var items = await context.PackageItems.Where(i => i.PackageId == request.PackageId).ToArrayAsync(cancellationToken); - - context.PackageItems.RemoveRange(items); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to delete package {request.PackageId} items"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs deleted file mode 100644 index 15945ff..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageCommandHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.CartPackages.Commands.Handlers; - -public class UpdatePackageCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdatePackageCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (await context.Packages.AnyAsync(p => p.Name == request.Name, cancellationToken)) - return Result.Fail($"A package by the same name already exists: {request.Name}"); - - var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken); - - if (package is null) - return Result.Fail($"Could not find package by id {request.PackageId}"); - - package.Name = request.Name; - package.Description = request.Description; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to update package with id {request.PackageId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs b/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs deleted file mode 100644 index 16c6482..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/Handlers/UpdatePackageStatusCommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.CartPackages.Commands.Handlers; - -public class UpdatePackageStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdatePackageStatusCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken); - - if (package is null) - return Result.Fail($"Could not find package by id {request.PackageId}"); - - package.Active = request.Active; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to update package with id {request.PackageId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageCommand.cs deleted file mode 100644 index 938ca44..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace LiteCharms.Features.CartPackages.Commands; - -public class UpdatePackageCommand : IRequest -{ - public Guid PackageId { get; set; } - - public string? Name { get; set; } - - public string? Description { get; set; } - - private UpdatePackageCommand(Guid packageId, string? name, string? description) - { - PackageId = packageId; - Name = name; - Description = description; - } - - public static UpdatePackageCommand Create(Guid packageId, string? name, string? description) - { - if (packageId == Guid.Empty) - throw new ArgumentException($"Package ID is required", nameof(packageId)); - - ArgumentNullException.ThrowIfNullOrWhiteSpace(name, nameof(name)); - ArgumentNullException.ThrowIfNullOrWhiteSpace(description, nameof(description)); - - return new(packageId, name, description); - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageStatusCommand.cs b/LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageStatusCommand.cs deleted file mode 100644 index 7be4651..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Commands/UpdatePackageStatusCommand.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace LiteCharms.Features.CartPackages.Commands; - -public class UpdatePackageStatusCommand : IRequest -{ - public Guid PackageId { get; set; } - - public bool Active { get; set; } - - private UpdatePackageStatusCommand(Guid packageId, bool active) - { - PackageId = packageId; - Active = active; - } - - public static UpdatePackageStatusCommand Create(Guid packageId, bool active) - { - if(packageId == Guid.Empty) - throw new ArgumentException($"Package id is required", nameof(packageId)); - - return new(packageId, active); - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/PackageService.cs b/LiteCharms.Features/Shop/CartPackages/PackageService.cs new file mode 100644 index 0000000..1df6c00 --- /dev/null +++ b/LiteCharms.Features/Shop/CartPackages/PackageService.cs @@ -0,0 +1,243 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.Shop.CartPackages.Models; +using LiteCharms.Features.Shop.Postgres; +using static LiteCharms.Features.Extensions.Timezones; + +namespace LiteCharms.Features.Shop.CartPackages; + +public class PackageService(IDbContextFactory contextFactory) +{ + public async ValueTask> AddPackageItemAsync(Guid packageId, Guid productPriceId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == packageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {packageId}"); + + if (!await context.ProductPrices.AnyAsync(p => p.Id == productPriceId && p.Active == true, cancellationToken)) + return Result.Fail($"Could not find an active product price by ID {productPriceId}"); + + if (await context.PackageItems.AnyAsync(p => p.ProductPriceId == productPriceId && p.PackageId == packageId, cancellationToken)) + return Result.Fail($"Product price {productPriceId} is already added to this package {packageId}"); + + var newPackageItem = context.PackageItems.Add(new Entities.PackageItem + { + PackageId = packageId, + ProductPriceId = productPriceId, + Active = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newPackageItem.Entity.Id) + : Result.Fail($"Failed to add new package item by ID {productPriceId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreatePackageAsync(string? name, string? summary, string? description, string? ImageUrl, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Packages.AnyAsync(p => p.Name == name, cancellationToken)) + return Result.Fail($"A package by the same name already exists: {name}"); + + var newPackage = context.Packages.Add(new Entities.Package + { + UpdatedAt = null, + Name = name, + Summary = summary, + Description = description, + ImageUrl = ImageUrl, + Active = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newPackage.Entity.Id) + : Result.Fail($"Failed to create a new package by the name: {name}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeletePackageItemAsync(Guid packageId, Guid packageItemId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == packageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {packageId}"); + + var item = await context.PackageItems.FirstOrDefaultAsync(p => p.Id == packageItemId && p.PackageId == packageId, cancellationToken); + + if (item is null) + return Result.Fail($"Product item {packageItemId} is already added to this package {packageId}"); + + context.PackageItems.Remove(item); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to delete package item by id {packageItemId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeletePackageItemsAsync(Guid packageId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == packageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {packageId}"); + + var items = await context.PackageItems.Where(i => i.PackageId == packageId).ToArrayAsync(cancellationToken); + + context.PackageItems.RemoveRange(items); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to delete package {packageId} items"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPackageAsync(Guid packageId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == packageId, cancellationToken); + + return package is not null + ? Result.Ok(package.ToModel()) + : Result.Fail($"Failed to find package by ID {packageId}"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPackageItemsAsync(Guid packageId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == packageId, cancellationToken)) + return Result.Fail($"Package could not be found with ID {packageId}"); + + var items = await context.PackageItems.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(p => p.PackageId == packageId) + .ToArrayAsync(cancellationToken); + + return items?.Length > 0 + ? Result.Ok(items.Select(i => i.ToModel()).ToArray()) + : Result.Fail($"Could not find package items by package ID {packageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPackagesAsync(Guid packageId, DateRange range, bool active, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var packages = await context.Packages + .AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) + .Where(p => p.Active == active) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return packages?.Length > 0 + ? Result.Ok(packages.Select(o => o.ToModel()).ToArray()) + : Result.Fail(new Error($"No packages found for the specified date range {range.From} - {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> UpdatePackageAsync(Guid packageId, string? name, string? summary, string? description, string? ImageUrl, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Packages.AnyAsync(p => p.Name == name, cancellationToken)) + return Result.Fail($"A package by the same name already exists: {name}"); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == packageId, cancellationToken); + + if (package is null) + return Result.Fail($"Could not find package by id {packageId}"); + + package.Name = name; + package.Summary = summary; + package.Description = description; + package.ImageUrl = ImageUrl; + package.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update package with id {packageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> UpdatePackageStatusAsync(Guid packageId, bool active, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == packageId, cancellationToken); + + if (package is null) + return Result.Fail($"Could not find package by id {packageId}"); + + package.Active = active; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update package with id {packageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/Shop/CartPackages/Queries/GetPackageItemsQuery.cs b/LiteCharms.Features/Shop/CartPackages/Queries/GetPackageItemsQuery.cs deleted file mode 100644 index 3315726..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Queries/GetPackageItemsQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.CartPackages.Models; - -namespace LiteCharms.Features.CartPackages.Queries; - -public class GetPackageItemsQuery : IRequest> -{ - public Guid PackageId { get; set; } - - private GetPackageItemsQuery(Guid packageId) => PackageId = packageId; - - public static GetPackageItemsQuery Create(Guid packageId) - { - if (packageId == Guid.Empty) - throw new ArgumentException("Package ID is required", nameof(packageId)); - - return new(packageId); - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Queries/GetPackageQuery.cs b/LiteCharms.Features/Shop/CartPackages/Queries/GetPackageQuery.cs deleted file mode 100644 index a05264e..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Queries/GetPackageQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.CartPackages.Models; - -namespace LiteCharms.Features.CartPackages.Queries; - -public class GetPackageQuery : IRequest> -{ - public Guid PackageId { get; set; } - - private GetPackageQuery(Guid packageId) => PackageId = packageId; - - public static GetPackageQuery Create(Guid packageId) - { - if(packageId == Guid.Empty) - throw new ArgumentException("Package ID is required", nameof(packageId)); - - return new(packageId); - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Queries/GetPackagesQuery.cs b/LiteCharms.Features/Shop/CartPackages/Queries/GetPackagesQuery.cs deleted file mode 100644 index 036ff09..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Queries/GetPackagesQuery.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Features.Shop.CartPackages.Models; - -namespace LiteCharms.Features.CartPackages.Queries; - -public class GetPackagesQuery : IRequest> -{ - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - public int MaxRecords { get; set; } - - public bool Active { get; set; } - - private GetPackagesQuery(DateOnly from, DateOnly to, int maxRecords = 1000, bool active = true) - { - From = from; - To = to; - MaxRecords = maxRecords; - Active = active; - } - - public static GetPackagesQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000, bool active = true) - { - if (from > to) - throw new ArgumentException("From date cannot be greater than To date."); - - if (maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer."); - - return new(from, to, maxRecords, active); - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs b/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs deleted file mode 100644 index 3b19cdc..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageItemsQueryHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.CartPackages.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.CartPackages.Queries.Handlers; - -public class GetPackageItemsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetPackageItemsQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) - return Result.Fail($"Package could not be found with ID {request.PackageId}"); - - var items = await context.PackageItems.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(p => p.PackageId == request.PackageId) - .ToArrayAsync(cancellationToken); - - return items?.Length > 0 - ? Result.Ok(items.Select(i => i.ToModel()).ToArray()) - : Result.Fail($"Could not find package items by package ID {request.PackageId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs b/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs deleted file mode 100644 index d822584..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackageQueryHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.CartPackages.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.CartPackages.Queries.Handlers; - -public class GetPackageQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetPackageQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == request.PackageId, cancellationToken); - - return package is not null - ? Result.Ok(package.ToModel()) - : Result.Fail($"Failed to find package by ID {request.PackageId}"); - - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs b/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs deleted file mode 100644 index 1ef691f..0000000 --- a/LiteCharms.Features/Shop/CartPackages/Queries/Handlers/GetPackagesQueryHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.CartPackages.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.CartPackages.Queries.Handlers; - -public class GetPackagesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetPackagesQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var packages = await context.Packages - .AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) - .Where(p => p.Active == request.Active) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return packages?.Length > 0 - ? Result.Ok(packages.Select(o => o.ToModel()).ToArray()) - : Result.Fail(new Error($"No packages found for the specified date range {request.From} - {request.To}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Customers/Commands/CreateCustomerCommand.cs b/LiteCharms.Features/Shop/Customers/Commands/CreateCustomerCommand.cs deleted file mode 100644 index f0b3560..0000000 --- a/LiteCharms.Features/Shop/Customers/Commands/CreateCustomerCommand.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace LiteCharms.Features.Customers.Commands; - -public class CreateCustomerCommand : IRequest> -{ - public string? Company { get; set; } - - public string Name { get; set; } - - public string LastName { get; set; } - - public string? Tax { get; set; } - - public string Email { get; set; } - - public string? Discord { get; set; } - - public string? Slack { get; set; } - - public string? LinkedIn { get; set; } - - public string? Whatsapp { get; set; } - - public string? Website { get; set; } - - public string? Phone { get; set; } - - public string? Address { get; set; } - - public string? City { get; set; } - - public string? Region { get; set; } - - public string? Country { get; set; } - - public string? PostalCode { get; set; } - - private CreateCustomerCommand(string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode) - { - Name = name; - LastName = lastName; - Company = company; - Tax = tax; - Email = email; - Discord = discord; - Slack = slack; - LinkedIn = linkedIn; - Whatsapp = whatsapp; - Website = website; - Phone = phone; - Address = address; - City = city; - Region = region; - Country = country; - PostalCode = postalCode; - } - - public static CreateCustomerCommand Create(string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode) - { - if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(lastName) && string.IsNullOrWhiteSpace(email)) - throw new ArgumentException("At the following fields must be provided: Name, LastName, Email"); - - return new(name, lastName, company, tax, email, discord, slack, linkedIn, whatsapp, website, phone, address, city, region, country, postalCode); - } -} diff --git a/LiteCharms.Features/Shop/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs b/LiteCharms.Features/Shop/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs deleted file mode 100644 index bf1a5d6..0000000 --- a/LiteCharms.Features/Shop/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs +++ /dev/null @@ -1,48 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Customers.Commands.Handlers; - -public class CreateCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(CreateCustomerCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var customerEmail = request.Email.ToLower().Trim(); - - if (await context.Customers.AnyAsync(c => c.Email == customerEmail, cancellationToken)) - return Result.Fail(new Error($"A customer with the email {customerEmail} already exists")); - - var newCustomer = context.Customers.Add(new Entities.Customer - { - Company = request.Company, - Name = request.Name, - LastName = request.LastName, - Tax = request.Tax, - Email = customerEmail, - Discord = request.Discord, - Slack = request.Slack, - LinkedIn = request.LinkedIn, - Whatsapp = request.Whatsapp, - Website = request.Website, - Phone = request.Phone, - Address = request.Address, - City = request.City, - Region = request.Region, - Country = request.Country, - PostalCode = request.PostalCode, - Active = true, - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(newCustomer.Entity.Id) - : Result.Fail(new Error($"Failed to create customer {customerEmail}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs b/LiteCharms.Features/Shop/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs deleted file mode 100644 index 031f591..0000000 --- a/LiteCharms.Features/Shop/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Customers.Commands.Handlers; - -public class UpdateCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateCustomerCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == request.CustomerId, cancellationToken); - - if (customer is null) - return Result.Fail(new Error($"Customer with ID {request.CustomerId} not found.")); - - customer.Name = request.Name; - customer.LastName = request.LastName; - customer.Email = request.Email; - customer.Company = request.Company; - customer.Address = request.Address; - customer.City = request.City; - customer.Region = request.Region; - customer.Country = request.Country; - customer.PostalCode = request.PostalCode; - customer.Phone = request.Phone; - customer.Tax = request.Tax; - customer.City = request.City; - customer.Discord = request.Discord; - customer.Slack = request.Slack; - customer.LinkedIn = request.LinkedIn; - customer.Whatsapp = request.Whatsapp; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error($"Failed to update the customer {request.CustomerId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} \ No newline at end of file diff --git a/LiteCharms.Features/Shop/Customers/Commands/UpdateCustomerCommand.cs b/LiteCharms.Features/Shop/Customers/Commands/UpdateCustomerCommand.cs deleted file mode 100644 index ac4f0cd..0000000 --- a/LiteCharms.Features/Shop/Customers/Commands/UpdateCustomerCommand.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace LiteCharms.Features.Customers.Commands; - -public class UpdateCustomerCommand : IRequest -{ - public Guid CustomerId { get; set; } - - public string? Company { get; set; } - - public string? Name { get; set; } - - public string? LastName { get; set; } - - public string? Tax { get; set; } - - public string? Email { get; set; } - - public string? Discord { get; set; } - - public string? Slack { get; set; } - - public string? LinkedIn { get; set; } - - public string? Whatsapp { get; set; } - - public string? Website { get; set; } - - public string? Phone { get; set; } - - public string? Address { get; set; } - - public string? City { get; set; } - - public string? Region { get; set; } - - public string? Country { get; set; } - - public string? PostalCode { get; set; } - - private UpdateCustomerCommand(Guid customerId, string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode) - { - CustomerId = customerId; - Name = name; - LastName = lastName; - Company = company; - Tax = tax; - Email = email; - Discord = discord; - Slack = slack; - LinkedIn = linkedIn; - Whatsapp = whatsapp; - Website = website; - Phone = phone; - Address = address; - City = city; - Region = region; - Country = country; - PostalCode = postalCode; - } - - public static UpdateCustomerCommand Create(Guid customerId, string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode) - { - if (customerId == Guid.Empty) - throw new ArgumentException("Customer ID is required.", nameof(customerId)); - - if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(lastName) && string.IsNullOrWhiteSpace(email)) - throw new ArgumentException("At the following fields must be provided: Name, LastName, Email"); - - return new(customerId, name, lastName, company, tax, email, discord, slack, linkedIn, whatsapp, website, phone, address, city, region, country, postalCode); - } -} diff --git a/LiteCharms.Features/Shop/Customers/CustomerService.cs b/LiteCharms.Features/Shop/Customers/CustomerService.cs new file mode 100644 index 0000000..531ecfe --- /dev/null +++ b/LiteCharms.Features/Shop/Customers/CustomerService.cs @@ -0,0 +1,132 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.Shop.Customers.Models; +using LiteCharms.Features.Shop.Postgres; + +namespace LiteCharms.Features.Shop.Customers; + +public class CustomerService(IDbContextFactory contextFactory) +{ + public async ValueTask> CreateCustomerAsync(CreateCustomer request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customerEmail = request.Email.ToLower().Trim(); + + if (await context.Customers.AnyAsync(c => c.Email == customerEmail, cancellationToken)) + return Result.Fail(new Error($"A customer with the email {customerEmail} already exists")); + + var newCustomer = context.Customers.Add(new Entities.Customer + { + Company = request.Company, + Name = request.Name, + LastName = request.LastName, + Tax = request.Tax, + Email = customerEmail, + Discord = request.Discord, + Slack = request.Slack, + LinkedIn = request.LinkedIn, + Whatsapp = request.Whatsapp, + Website = request.Website, + Phone = request.Phone, + Address = request.Address, + City = request.City, + Region = request.Region, + Country = request.Country, + PostalCode = request.PostalCode, + Active = true, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newCustomer.Entity.Id) + : Result.Fail(new Error($"Failed to create customer {customerEmail}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAsync(Guid customerId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); + + return customer is not null + ? Result.Ok(customer.ToModel()) + : Result.Fail($"Customer not found with id {customerId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomersAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customers = await context.Customers.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(c => c.CreatedAt >= fromDate && c.CreatedAt <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return customers?.Length > 0 + ? Result.Ok(customers.Select(c => c.ToModel()).ToArray()) + : Result.Fail(new Error("No customers found in the specified date range.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerAsync(UpdateCustomer request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == request.CustomerId, cancellationToken); + + if (customer is null) + return Result.Fail(new Error($"Customer with ID {request.CustomerId} not found.")); + + customer.Name = request.Name; + customer.LastName = request.LastName; + customer.Email = request.Email; + customer.Company = request.Company; + customer.Address = request.Address; + customer.City = request.City; + customer.Region = request.Region; + customer.Country = request.Country; + customer.PostalCode = request.PostalCode; + customer.Phone = request.Phone; + customer.Tax = request.Tax; + customer.City = request.City; + customer.Discord = request.Discord; + customer.Slack = request.Slack; + customer.LinkedIn = request.LinkedIn; + customer.Whatsapp = request.Whatsapp; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update the customer {request.CustomerId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/Shop/Customers/Models/Records.cs b/LiteCharms.Features/Shop/Customers/Models/Records.cs new file mode 100644 index 0000000..0b4c788 --- /dev/null +++ b/LiteCharms.Features/Shop/Customers/Models/Records.cs @@ -0,0 +1,73 @@ +namespace LiteCharms.Features.Shop.Customers.Models; + +public record CreateCustomer +{ + public string? Company { get; set; } + + public required string Name { get; set; } + + public required string LastName { get; set; } + + public string? Tax { get; set; } + + public required string Email { get; set; } + + public string? Discord { get; set; } + + public string? Slack { get; set; } + + public string? LinkedIn { get; set; } + + public string? Whatsapp { get; set; } + + public string? Website { get; set; } + + public string? Phone { get; set; } + + public string? Address { get; set; } + + public string? City { get; set; } + + public string? Region { get; set; } + + public string? Country { get; set; } + + public string? PostalCode { get; set; } +} + +public record UpdateCustomer +{ + public required Guid CustomerId { get; set; } + + public string? Company { get; set; } + + public string? Name { get; set; } + + public string? LastName { get; set; } + + public string? Tax { get; set; } + + public string? Email { get; set; } + + public string? Discord { get; set; } + + public string? Slack { get; set; } + + public string? LinkedIn { get; set; } + + public string? Whatsapp { get; set; } + + public string? Website { get; set; } + + public string? Phone { get; set; } + + public string? Address { get; set; } + + public string? City { get; set; } + + public string? Region { get; set; } + + public string? Country { get; set; } + + public string? PostalCode { get; set; } +} \ No newline at end of file diff --git a/LiteCharms.Features/Shop/Customers/Queries/GetCustomerQuery.cs b/LiteCharms.Features/Shop/Customers/Queries/GetCustomerQuery.cs deleted file mode 100644 index c6ee4b0..0000000 --- a/LiteCharms.Features/Shop/Customers/Queries/GetCustomerQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.Customers.Models; - -namespace LiteCharms.Features.Customers.Queries; - -public class GetCustomerQuery : IRequest> -{ - public Guid CustomerId { get; set; } - - private GetCustomerQuery(Guid customerId) => CustomerId = customerId; - - public static GetCustomerQuery Create(Guid customerId) - { - if(customerId == Guid.Empty) - throw new ArgumentException("Customer ID is required.", nameof(customerId)); - - return new(customerId); - } -} diff --git a/LiteCharms.Features/Shop/Customers/Queries/GetCustomersQuery.cs b/LiteCharms.Features/Shop/Customers/Queries/GetCustomersQuery.cs deleted file mode 100644 index 9c830b4..0000000 --- a/LiteCharms.Features/Shop/Customers/Queries/GetCustomersQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Features.Shop.Customers.Models; - -namespace LiteCharms.Features.Customers.Queries; - -public class GetCustomersQuery : IRequest> -{ - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - public int MaxRecords { get; set; } - - private GetCustomersQuery(DateOnly from, DateOnly to, int maxRecords = 1000) - { - From = from; - To = to; - MaxRecords = maxRecords; - } - - public static GetCustomersQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000) - { - if (from > to) - throw new ArgumentException("From date cannot be greater than To date."); - - if(maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer."); - - return new(from, to, maxRecords); - } -} diff --git a/LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomerQueryHandler.cs b/LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomerQueryHandler.cs deleted file mode 100644 index cbe478d..0000000 --- a/LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomerQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Customers.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Customers.Queries.Handlers; - -public class GetCustomerQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == request.CustomerId, cancellationToken); - - return customer is not null - ? Result.Ok(customer.ToModel()) - : Result.Fail($"Customer not found with id {request.CustomerId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomersQueryHandler.cs b/LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomersQueryHandler.cs deleted file mode 100644 index b9d1e99..0000000 --- a/LiteCharms.Features/Shop/Customers/Queries/Handlers/GetCustomersQueryHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Customers.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Customers.Queries.Handlers; - -public class GetCustomersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomersQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var customers = await context.Customers.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(c => c.CreatedAt >= fromDate && c.CreatedAt <= toDate) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return customers?.Length > 0 - ? Result.Ok(customers.Select(c => c.ToModel()).ToArray()) - : Result.Fail(new Error("No customers found in the specified date range.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Leads/Commands/CreateLeadCommand.cs b/LiteCharms.Features/Shop/Leads/Commands/CreateLeadCommand.cs deleted file mode 100644 index 5b120df..0000000 --- a/LiteCharms.Features/Shop/Leads/Commands/CreateLeadCommand.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace LiteCharms.Features.Leads.Commands; - -public class CreateLeadCommand : IRequest> -{ - public Guid? CustomerId { get; set; } - - public string? Source { get; set; } - - public string? ClickId { get; set; } - - public string? WebClickId { get; set; } - - public string? AppClickId { get; set; } - - public long? CampaignId { get; set; } - - public long? AdGroupId { get; set; } - - public long? AdName { get; set; } - - public long? TargetId { get; set; } - - public long? FeedItemId { get; set; } - - public string? ClickLocation { get; set; } - - public string? AttribusionHash { get; set; } - - private CreateLeadCommand(Guid? customerId, string source, string clickId, string webClickId, string appClickId, long? campaignId, long? adGroupId, long? adName, long? targetId, long? feedItemId, string? clickLocation, string? attribusionHash) - { - CustomerId = customerId; - Source = source; - ClickId = clickId; - WebClickId = webClickId; - AppClickId = appClickId; - CampaignId = campaignId; - AdGroupId = adGroupId; - AdName = adName; - TargetId = targetId; - FeedItemId = feedItemId; - ClickLocation = clickLocation; - AttribusionHash = attribusionHash; - } - - public static CreateLeadCommand Create(Guid? customerId, string source, string clickId, string webClickId, string appClickId, long? campaignId, long? adGroupId, long? adName, long? targetId, long? feedItemId, string? clickLocation, string? attribusionHash) - { - if(string.IsNullOrWhiteSpace(source)) - throw new ArgumentNullException("Lead source is required to create a lead.", nameof(source)); - - if (string.IsNullOrWhiteSpace(clickId) || string.IsNullOrWhiteSpace(appClickId) || string.IsNullOrWhiteSpace(webClickId)) - throw new ArgumentException("ClickId, App ClickId and Web ClickId are required to create a lead."); - - return new(customerId, source, clickId, webClickId, appClickId, campaignId, adGroupId, adName, targetId, feedItemId, clickLocation, attribusionHash); - } -} diff --git a/LiteCharms.Features/Shop/Leads/Commands/Handlers/CreateLeadCommandHandler.cs b/LiteCharms.Features/Shop/Leads/Commands/Handlers/CreateLeadCommandHandler.cs deleted file mode 100644 index a51d31a..0000000 --- a/LiteCharms.Features/Shop/Leads/Commands/Handlers/CreateLeadCommandHandler.cs +++ /dev/null @@ -1,47 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Utilities.Hash.Commands; - -namespace LiteCharms.Features.Leads.Commands.Handlers; - -public class CreateLeadCommandHandler(IDbContextFactory contextFactory, ISender mediator) : IRequestHandler> -{ - public async ValueTask> Handle(CreateLeadCommand request, CancellationToken cancellationToken) - { - try - { - var hashCommand = ComputeHashCommand.Create($"{request.ClickId}{request.AppClickId}{request.WebClickId}"); - var hashResult = await mediator.Send(hashCommand, cancellationToken); - - if(hashResult.IsFailed) - return Result.Fail(new Error($"Failed to compute hash for lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}") - .CausedBy(hashResult.Errors)); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var newLead = context.Leads.Add(new Entities.Lead - { - WebClickId = request.WebClickId, - AppClickId = request.AppClickId, - Source = request.Source, - ClickId = request.ClickId, - AdGroupId = request.AdGroupId, - AdName = request.AdName, - CampaignId = request.CampaignId, - ClickLocation = request.ClickLocation, - CustomerId = request.CustomerId, - FeedItemId = request.FeedItemId, - Status = Models.LeadStatus.New, - TargetId = request.TargetId, - AttributionHash = hashResult.Value - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(newLead.Entity.Id) - : Result.Fail(new Error($"Failed to create lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs b/LiteCharms.Features/Shop/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs deleted file mode 100644 index 67f14fc..0000000 --- a/LiteCharms.Features/Shop/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Leads.Commands.Handlers; - -public class UpdateLeadCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateLeadCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var lead = await context.Leads.FirstOrDefaultAsync(l => l.Id == request.LeadId, cancellationToken); - - if (lead is null) - return Result.Fail(new Error($"Lead with ID {request.LeadId} not found.")); - - lead.Status = request.Status; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error($"Failed to update the lead {request.LeadId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Leads/Commands/UpdateLeadCommand.cs b/LiteCharms.Features/Shop/Leads/Commands/UpdateLeadCommand.cs deleted file mode 100644 index ef31b01..0000000 --- a/LiteCharms.Features/Shop/Leads/Commands/UpdateLeadCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -using LiteCharms.Features.Shop; -using LiteCharms.Models; - -namespace LiteCharms.Features.Leads.Commands; - -public class UpdateLeadCommand : IRequest -{ - public Guid LeadId { get; set; } - - public LeadStatus Status { get; set; } - - private UpdateLeadCommand(Guid leadId, LeadStatus status) - { - LeadId = leadId; - Status = status; - } - - public static UpdateLeadCommand Create(Guid leadId, LeadStatus status) - { - if (leadId == Guid.Empty) - throw new ArgumentException("Lead ID cannot be empty.", nameof(leadId)); - - if (!Enum.IsDefined(typeof(LeadStatus), status)) - throw new ArgumentException("Invalid lead status.", nameof(status)); - - return new(leadId, status); - } -} diff --git a/LiteCharms.Features/Shop/Leads/LeadService.cs b/LiteCharms.Features/Shop/Leads/LeadService.cs new file mode 100644 index 0000000..f8b4637 --- /dev/null +++ b/LiteCharms.Features/Shop/Leads/LeadService.cs @@ -0,0 +1,116 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.Shop.Leads.Models; +using LiteCharms.Features.Shop.Postgres; +using static LiteCharms.Features.Extensions.Hash; + +namespace LiteCharms.Features.Shop.Leads; + +public class LeadService(IDbContextFactory contextFactory) +{ + public async ValueTask> CreateLeadAsync(CreateLead request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var newLead = context.Leads.Add(new Entities.Lead + { + WebClickId = request.WebClickId, + AppClickId = request.AppClickId, + Source = request.Source, + ClickId = request.ClickId, + AdGroupId = request.AdGroupId, + AdName = request.AdName, + CampaignId = request.CampaignId, + ClickLocation = request.ClickLocation, + CustomerId = request.CustomerId, + FeedItemId = request.FeedItemId, + Status = LeadStatus.New, + TargetId = request.TargetId, + AttributionHash = GenerateSha256HashString.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}") + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newLead.Entity.Id) + : Result.Fail(new Error($"Failed to create lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerLeadsAsync(Guid customerId, DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var leads = await context.Leads.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(lead => lead.CustomerId == customerId) + .Where(lead => lead.CreatedAt.Date >= fromDate && lead.CreatedAt.Date <= toDate) + .ToArrayAsync(cancellationToken); + + return leads?.Length > 0 + ? Result.Ok(leads.Select(l => l.ToModel()).ToArray()) + : Result.Fail(new Error($"No customer {customerId} leads found for the specified date range {range.From} to {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetLeadsAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var leads = await context.Leads.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(l => l.CreatedAt.Date >= fromDate && l.CreatedAt.Date <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return leads?.Length > 0 + ? Result.Ok(leads.Select(l => l.ToModel()).ToArray()) + : Result.Fail(new Error($"No leads found for the specified date range {range.From} to {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateLeadAsync(Guid leadId, LeadStatus status, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var lead = await context.Leads.FirstOrDefaultAsync(l => l.Id == leadId, cancellationToken); + + if (lead is null) + return Result.Fail(new Error($"Lead with ID {leadId} not found.")); + + lead.Status = status; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update the lead {leadId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/Shop/Leads/Models/Records.cs b/LiteCharms.Features/Shop/Leads/Models/Records.cs new file mode 100644 index 0000000..9037942 --- /dev/null +++ b/LiteCharms.Features/Shop/Leads/Models/Records.cs @@ -0,0 +1,28 @@ +namespace LiteCharms.Features.Shop.Leads.Models; + +public record CreateLead +{ + public Guid? CustomerId { get; set; } + + public required string Source { get; set; } + + public required string ClickId { get; set; } + + public required string WebClickId { get; set; } + + public required string AppClickId { get; set; } + + public long? CampaignId { get; set; } + + public long? AdGroupId { get; set; } + + public long? AdName { get; set; } + + public long? TargetId { get; set; } + + public long? FeedItemId { get; set; } + + public string? ClickLocation { get; set; } + + public string? AttribusionHash { get; set; } +} diff --git a/LiteCharms.Features/Shop/Leads/Queries/GetCustomerLeadsQuery.cs b/LiteCharms.Features/Shop/Leads/Queries/GetCustomerLeadsQuery.cs deleted file mode 100644 index de53634..0000000 --- a/LiteCharms.Features/Shop/Leads/Queries/GetCustomerLeadsQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Features.Shop.Leads.Models; - -namespace LiteCharms.Features.Leads.Queries; - -public class GetCustomerLeadsQuery : IRequest> -{ - public Guid CustomerId { get; } - - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - private GetCustomerLeadsQuery(Guid customerId, DateOnly from, DateOnly to) - { - CustomerId = customerId; - From = from; - To = to; - } - - public static GetCustomerLeadsQuery Create(Guid customerId, DateOnly from, DateOnly to) - { - if(customerId == Guid.Empty) - throw new ArgumentException("Customer ID cannot be empty.", nameof(customerId)); - - if(from > to) - throw new ArgumentException("The 'From' date cannot be later than the 'To' date."); - - return new(customerId, from, to); - } -} diff --git a/LiteCharms.Features/Shop/Leads/Queries/GetLeadsQuery.cs b/LiteCharms.Features/Shop/Leads/Queries/GetLeadsQuery.cs deleted file mode 100644 index d280e65..0000000 --- a/LiteCharms.Features/Shop/Leads/Queries/GetLeadsQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Features.Shop.Leads.Models; - -namespace LiteCharms.Features.Leads.Queries; - -public class GetLeadsQuery : IRequest> -{ - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - public int MaxRecords { get; set; } - - private GetLeadsQuery(DateOnly from, DateOnly to, int maxRecords = 1000) - { - From = from; - To = to; - MaxRecords = maxRecords; - } - - public static GetLeadsQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000) - { - if (from > to) - throw new ArgumentException("From date cannot be greater than To date."); - - if(maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer."); - - return new(from, to, maxRecords); - } -} diff --git a/LiteCharms.Features/Shop/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs b/LiteCharms.Features/Shop/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs deleted file mode 100644 index dd9ba15..0000000 --- a/LiteCharms.Features/Shop/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Leads.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Leads.Queries.Handlers; - -public class GetCustomerLeadsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerLeadsQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var leads = await context.Leads.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(lead => lead.CustomerId == request.CustomerId) - .Where(lead => lead.CreatedAt.Date >= fromDate && lead.CreatedAt.Date <= toDate) - .ToArrayAsync(cancellationToken); - - return leads?.Length > 0 - ? Result.Ok(leads.Select(l => l.ToModel()).ToArray()) - : Result.Fail(new Error($"No customer {request.CustomerId} leads found for the specified date range {request.From} to {request.To}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Leads/Queries/Handlers/GetLeadsQueryHandler.cs b/LiteCharms.Features/Shop/Leads/Queries/Handlers/GetLeadsQueryHandler.cs deleted file mode 100644 index 417ff5c..0000000 --- a/LiteCharms.Features/Shop/Leads/Queries/Handlers/GetLeadsQueryHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Leads.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Leads.Queries.Handlers; - -public class GetLeadsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetLeadsQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var leads = await context.Leads.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(l => l.CreatedAt.Date >= fromDate && l.CreatedAt.Date <= toDate) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return leads?.Length > 0 - ? Result.Ok(leads.Select(l => l.ToModel()).ToArray()) - : Result.Fail(new Error($"No leads found for the specified date range {request.From} to {request.To}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Notifications/Commands/CreateNotificationCommand.cs b/LiteCharms.Features/Shop/Notifications/Commands/CreateNotificationCommand.cs deleted file mode 100644 index 59e69d6..0000000 --- a/LiteCharms.Features/Shop/Notifications/Commands/CreateNotificationCommand.cs +++ /dev/null @@ -1,73 +0,0 @@ -using LiteCharms.Features.Shop; -using LiteCharms.Models; - -namespace LiteCharms.Features.Notifications.Commands; - -public class CreateNotification : IRequest> -{ - public NotificationDirection Direction { get; set; } - - public string? Sender { get; set; } - - public string? SenderAddress { get; set; } - - public string? Subject { get; set; } - - public string? Message { get; set; } - - public NotificationPlatforms Platform { get; set; } - - public Priorities Priority { get; set; } - - public string? Recipient { get; set; } - - public string? RecipientAddress { get; set; } - - public string? CorrelationId { get; set; } - - public CorrelationIdTypes CorrelationIdType { get; set; } - - public bool IsInternal { get; set; } - - public bool IsHtml { get; set; } - - private CreateNotification(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false) - { - Direction = direction; - Sender = sender; - SenderAddress = senderAddress; - Subject = subject; - Message = message; - Platform = platform; - Priority = priority; - Recipient = recipient; - RecipientAddress = recipientAddress; - CorrelationId = correlationId; - CorrelationIdType = correlationIdType; - IsInternal = isInternal; - IsHtml = isHtml; - } - - public static CreateNotification Create(NotificationDirection direction, string sender, string senderAddress, string subject, string message, NotificationPlatforms platform, Priorities priority, string recipient, string recipientAddress, string correlationId, CorrelationIdTypes correlationIdType, bool isInternal, bool isHtml = false) - { - if (string.IsNullOrWhiteSpace(sender)) - throw new ArgumentException("Sender name is required.", nameof(sender)); - - if (string.IsNullOrWhiteSpace(subject)) - throw new ArgumentException("Subject is required.", nameof(subject)); - - if (string.IsNullOrWhiteSpace(message)) - throw new ArgumentException("Message is required.", nameof(message)); - - if (string.IsNullOrWhiteSpace(recipient)) - throw new ArgumentException("Recipient name is required.", nameof(recipient)); - - if (string.IsNullOrWhiteSpace(recipientAddress)) - throw new ArgumentException("Recipient address is required.", nameof(recipientAddress)); - - if (string.IsNullOrWhiteSpace(correlationId)) - throw new ArgumentException("CorrelationId is required.", nameof(correlationId)); - - return new(direction, sender, senderAddress, subject, message, platform, priority, recipient, recipientAddress, correlationId, correlationIdType, isInternal, isHtml); - } -} diff --git a/LiteCharms.Features/Shop/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs b/LiteCharms.Features/Shop/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs deleted file mode 100644 index facd9a3..0000000 --- a/LiteCharms.Features/Shop/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Notifications.Commands.Handlers; - -public class CreateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(CreateNotification request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var newNotification = context.Notifications.Add(new Entities.Notification - { - Direction = request.Direction, - SenderName = request.Sender, - Sender = request.SenderAddress, - Recipient = request.Recipient, - RecipientAddress = request.RecipientAddress, - Subject = request.Subject, - Message = request.Message, - Platform = request.Platform, - Priority = request.Priority, - CorrelationId = request.CorrelationId, - CorrelationIdType = request.CorrelationIdType, - IsInternal = request.IsInternal, - IsHtml = request.IsHtml, - Processed = false - }); - - return newNotification is not null - ? Result.Ok(newNotification.Entity.Id) - : Result.Fail(new Error("Failed to create notification")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs b/LiteCharms.Features/Shop/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs deleted file mode 100644 index 6d32acc..0000000 --- a/LiteCharms.Features/Shop/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Notifications.Commands.Handlers; - -public class UpdateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateNotificationCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var notification = await context.Notifications.FirstOrDefaultAsync(n => n.Id == request.NotificationId, cancellationToken); - - if(notification is null) - return Result.Fail(new Error($"Notification with id {request.NotificationId} not found.")); - - notification.Processed = request.Processed; - - if (request.HasError) - { - notification.HasError = request.HasError; - notification.Errors = request.Errors; - } - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error($"Failed to update notification with id {request.NotificationId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Notifications/Commands/UpdateNotificationCommand.cs b/LiteCharms.Features/Shop/Notifications/Commands/UpdateNotificationCommand.cs deleted file mode 100644 index 950442d..0000000 --- a/LiteCharms.Features/Shop/Notifications/Commands/UpdateNotificationCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace LiteCharms.Features.Notifications.Commands; - -public class UpdateNotificationCommand : IRequest -{ - public Guid NotificationId { get; set; } - - public bool Processed { get; set; } - - public bool HasError { get; set; } - - public string[]? Errors { get; set; } - - private UpdateNotificationCommand(Guid notificationId, bool processed, bool hasError = false, string[]? errors = null) - { - NotificationId = notificationId; - Processed = processed; - HasError = hasError; - Errors = errors; - } - - public static UpdateNotificationCommand Create(Guid notificationId, bool processed, bool hasError = false, string[]? errors = null) - { - if(notificationId == Guid.Empty) - throw new ArgumentException("Notification ID cannot be empty.", nameof(notificationId)); - - return new(notificationId, processed); - } -} diff --git a/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs b/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs index 37b7659..8a00b68 100644 --- a/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs +++ b/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs @@ -13,10 +13,10 @@ public class NotificationConfiguration : IEntityTypeConfiguration builder.Property(f => f.Platform).IsRequired().HasConversion(); builder.Property(f => f.Priority).IsRequired().HasConversion(); builder.Property(f => f.CorrelationIdType).IsRequired().HasConversion(); - builder.Property(f => f.Sender).IsRequired(); + builder.Property(f => f.SenderAddress).IsRequired(); builder.Property(f => f.Subject).IsRequired(); builder.Property(f => f.Message).IsRequired(); - builder.Property(f => f.Recipient).IsRequired(); + builder.Property(f => f.RecipientName).IsRequired(); builder.Property(f => f.RecipientAddress).IsRequired(); builder.Property(f => f.CorrelationId).IsRequired(); builder.Property(f => f.IsHtml).HasDefaultValue(false); diff --git a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs index 332ec86..6a11fda 100644 --- a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs +++ b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs @@ -1,13 +1,15 @@ -using LiteCharms.Features.Email.Commands; +using LiteCharms.Features.Email; using LiteCharms.Features.Shop.Notifications.Entities; using LiteCharms.Features.Shop.Postgres; -using static LiteCharms.Features.ServiceBus.Constants; +using static LiteCharms.Features.Extensions.Timezones; -namespace LiteCharms.Features.Notifications.Events.Handlers; +namespace LiteCharms.Features.Shop.Notifications.Events.Handlers; -public class ProcessEmailNotificationsEventHandler(IDbContextFactory contextFactory, ILogger logger, ISender mediator) : - INotificationHandler +public class ProcessEmailNotificationsEventHandler(IDbContextFactory contextFactory, ILogger logger, + EmailService emailService) : INotificationHandler { + private bool dropBatch = false; + public async ValueTask Handle(ProcessEmailNotificationsEvent message, CancellationToken cancellationToken) { try @@ -17,14 +19,16 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory o.Priority) .ThenBy(o => o.CreatedAt) - .Where(n => n.CorrelationIdType == Models.CorrelationIdTypes.Email) - .Where(n => n.Direction == Models.NotificationDirection.Outgoing) + .Where(n => n.CorrelationIdType == CorrelationIdTypes.Email) + .Where(n => n.Direction == NotificationDirection.Outgoing) .Take(message.MaxRecords) .ToListAsync(cancellationToken); foreach (var notification in notifications) { - var sendResult = await SendEmailAsync(notification, cancellationToken); + if (dropBatch || cancellationToken.IsCancellationRequested) break; + + var sendResult = await SendEmailAsync(notification,emailService, cancellationToken); if(sendResult.IsFailed) { @@ -40,6 +44,7 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory SendEmailAsync(Notification notification, CancellationToken cancellationToken = default) + private async Task SendEmailAsync(Notification notification, EmailService service, CancellationToken cancellationToken = default) { try { - var request = SendEmailCommand.Create(notification.Sender!, notification.SenderName!, ShopEmailFromAddress, - ShopEmailFromName, notification.Subject!, notification.Message!); + using Email.Models.Message message = CreateMessage(notification); - var result = await mediator.Send(request, cancellationToken); + var sendResult = await service.SendEmailAsync(message, cancellationToken); - return result.IsFailed - ? Result.Fail(result.Errors) + if (sendResult.IsFailed) + { + if (emailService.Status != EmailStatuses.Success && emailService.Status != EmailStatuses.Connected) dropBatch = true; + + return Result.Fail(sendResult.Errors); + } + + return sendResult.IsFailed + ? Result.Fail(sendResult.Errors) : Result.Ok(); } catch (Exception ex) @@ -68,4 +79,29 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory + new() + { + Sender = new Email.Models.Party + { + Name = notification.SenderName, + Address = notification.SenderAddress + }, + Recipient = new Email.Models.Party + { + Name = notification.RecipientName, + Address = notification.RecipientAddress + }, + Subject = notification.Subject, + Body = new Email.Models.Body + { + Properties = new Email.Models.BodyProperties + { + HasAttachments = false, + IsHtml = notification.IsHtml + }, + Message = notification.Message + } + }; } diff --git a/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs b/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs index bbc4da4..d496ab9 100644 --- a/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs +++ b/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs @@ -1,6 +1,6 @@ using LiteCharms.Features.Abstractions; -namespace LiteCharms.Features.Notifications.Events; +namespace LiteCharms.Features.Shop.Notifications.Events; public class ProcessEmailNotificationsEvent : EventBase, IEvent { diff --git a/LiteCharms.Features/Shop/Notifications/INotificationService.cs b/LiteCharms.Features/Shop/Notifications/INotificationService.cs deleted file mode 100644 index d036de3..0000000 --- a/LiteCharms.Features/Shop/Notifications/INotificationService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using LiteCharms.Features.Shop.Notifications.Models; - -namespace LiteCharms.Features.Shop.Notifications; - -public interface INotificationService -{ - Task> CreateNotificationAsync(CreateNotification request, CancellationToken cancellationToken = default); - Task UpdateNotificationAsync(UpdateNotification request, CancellationToken cancellationToken = default); -} diff --git a/LiteCharms.Features/Shop/Notifications/Models/Notification.cs b/LiteCharms.Features/Shop/Notifications/Models/Notification.cs index cba815c..bb9b77c 100644 --- a/LiteCharms.Features/Shop/Notifications/Models/Notification.cs +++ b/LiteCharms.Features/Shop/Notifications/Models/Notification.cs @@ -16,7 +16,7 @@ public class Notification public CorrelationIdTypes CorrelationIdType { get; set; } - public string? Sender { get; set; } + public string? SenderAddress { get; set; } public string? SenderName { get; set; } @@ -24,7 +24,7 @@ public class Notification public string? Message { get; set; } - public string? Recipient { get; set; } + public string? RecipientName { get; set; } public string? RecipientAddress { get; set; } diff --git a/LiteCharms.Features/Shop/Notifications/Models/Records.cs b/LiteCharms.Features/Shop/Notifications/Models/Records.cs index 2f0f949..6d41f26 100644 --- a/LiteCharms.Features/Shop/Notifications/Models/Records.cs +++ b/LiteCharms.Features/Shop/Notifications/Models/Records.cs @@ -2,23 +2,23 @@ public record CreateNotification { - public NotificationDirection Direction { get; set; } + public required NotificationDirection Direction { get; set; } - public string? Sender { get; set; } + public required string Sender { get; set; } - public string? SenderAddress { get; set; } + public required string SenderAddress { get; set; } - public string? Subject { get; set; } + public required string Subject { get; set; } public string? Message { get; set; } - public NotificationPlatforms Platform { get; set; } + public required NotificationPlatforms Platform { get; set; } - public Priorities Priority { get; set; } + public required Priorities Priority { get; set; } - public string? Recipient { get; set; } + public required string Recipient { get; set; } - public string? RecipientAddress { get; set; } + public required string RecipientAddress { get; set; } public string? CorrelationId { get; set; } @@ -31,9 +31,9 @@ public record CreateNotification public class UpdateNotification { - public Guid NotificationId { get; set; } + public required Guid NotificationId { get; set; } - public bool Processed { get; set; } + public required bool Processed { get; set; } public bool HasError { get; set; } diff --git a/LiteCharms.Features/Shop/Notifications/NotificationService.cs b/LiteCharms.Features/Shop/Notifications/NotificationService.cs index 02ee707..84e7565 100644 --- a/LiteCharms.Features/Shop/Notifications/NotificationService.cs +++ b/LiteCharms.Features/Shop/Notifications/NotificationService.cs @@ -1,5 +1,115 @@ -namespace LiteCharms.Features.Shop.Notifications; +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.Shop.Notifications.Models; +using LiteCharms.Features.Shop.Postgres; -public class NotificationService : INotificationService +namespace LiteCharms.Features.Shop.Notifications; + +public class NotificationService(IDbContextFactory contextFactory) { + public async ValueTask> CreateNotificationAsync(CreateNotification request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var newNotification = context.Notifications.Add(new Entities.Notification + { + Direction = request.Direction, + SenderName = request.Sender, + SenderAddress = request.SenderAddress, + RecipientName = request.Recipient, + RecipientAddress = request.RecipientAddress, + Subject = request.Subject, + Message = request.Message, + Platform = request.Platform, + Priority = request.Priority, + CorrelationId = request.CorrelationId, + CorrelationIdType = request.CorrelationIdType, + IsInternal = request.IsInternal, + IsHtml = request.IsHtml, + Processed = false + }); + + return newNotification is not null + ? Result.Ok(newNotification.Entity.Id) + : Result.Fail(new Error("Failed to create notification")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetNotificationAsync(Guid notificationId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var notification = await context.Notifications.FirstOrDefaultAsync(n => n.Id == notificationId, cancellationToken); + + return notification is not null + ? Result.Ok(notification.ToModel()) + : Result.Fail(new Error($"Notification with id {notificationId} not found")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetNotificationsAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var notifications = await context.Notifications.AsNoTracking() + .Where(n => n.CreatedAt >= fromDate && n.CreatedAt <= toDate) + .OrderByDescending(n => n.CreatedAt) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return notifications?.Length > 0 + ? Result.Ok(notifications.Select(n => n.ToModel()).ToArray()) + : Result.Fail(new Error($"No notifications found for the specified date range {range.From} to {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateNotificationAsync(UpdateNotification request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var notification = await context.Notifications.FirstOrDefaultAsync(n => n.Id == request.NotificationId, cancellationToken); + + if (notification is null) + return Result.Fail(new Error($"Notification with id {request.NotificationId} not found.")); + + notification.Processed = request.Processed; + + if (request.HasError) + { + notification.HasError = request.HasError; + notification.Errors = request.Errors; + } + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update notification with id {request.NotificationId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } } diff --git a/LiteCharms.Features/Shop/Notifications/Queries/GetNotificationQuery.cs b/LiteCharms.Features/Shop/Notifications/Queries/GetNotificationQuery.cs deleted file mode 100644 index 6c599fb..0000000 --- a/LiteCharms.Features/Shop/Notifications/Queries/GetNotificationQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.Notifications.Models; - -namespace LiteCharms.Features.Notifications.Queries; - -public class GetNotificationQuery : IRequest> -{ - public Guid NotificationId { get; set; } - - private GetNotificationQuery(Guid notificationId) => NotificationId = notificationId; - - public static GetNotificationQuery Create(Guid notificationId) - { - if (notificationId == Guid.Empty) - throw new ArgumentException("Notification ID is required.", nameof(notificationId)); - - return new(notificationId); - } -} diff --git a/LiteCharms.Features/Shop/Notifications/Queries/GetNotificationsQuery.cs b/LiteCharms.Features/Shop/Notifications/Queries/GetNotificationsQuery.cs deleted file mode 100644 index 6a10a4e..0000000 --- a/LiteCharms.Features/Shop/Notifications/Queries/GetNotificationsQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Features.Shop.Notifications.Models; - -namespace LiteCharms.Features.Notifications.Queries; - -public class GetNotificationsQuery : IRequest> -{ - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - public int MaxRecords { get; set; } - - private GetNotificationsQuery(DateOnly from, DateOnly to, int maxRecords = 1000) - { - From = from; - To = to; - MaxRecords = maxRecords; - } - - public static GetNotificationsQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000) - { - if (from > to) - throw new ArgumentException("From date cannot be greater than To date."); - - if(maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer.", nameof(maxRecords)); - - return new(from, to, maxRecords); - } -} diff --git a/LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs b/LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs deleted file mode 100644 index 11d5553..0000000 --- a/LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Notifications.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Notifications.Queries.Handlers; - -public class GetNotificationQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetNotificationQuery request, CancellationToken cancellationToken) - { - try - { - await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var notification = await context.Notifications.FirstOrDefaultAsync(n => n.Id == request.NotificationId, cancellationToken); - - return notification is not null - ? Result.Ok(notification.ToModel()) - : Result.Fail(new Error($"Notification with id {request.NotificationId} not found")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs b/LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs deleted file mode 100644 index 17e6094..0000000 --- a/LiteCharms.Features/Shop/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Notifications.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Notifications.Queries.Handlers; - -public class GetNotificationsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetNotificationsQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var notifications = await context.Notifications.AsNoTracking() - .Where(n => n.CreatedAt >= fromDate && n.CreatedAt <= toDate) - .OrderByDescending(n => n.CreatedAt) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return notifications?.Length > 0 - ? Result.Ok(notifications.Select(n => n.ToModel()).ToArray()) - : Result.Fail(new Error($"No notifications found for the specified date range {request.From} to {request.To}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Orders/Commands/CreateOrderCommand.cs b/LiteCharms.Features/Shop/Orders/Commands/CreateOrderCommand.cs deleted file mode 100644 index 8baf7e1..0000000 --- a/LiteCharms.Features/Shop/Orders/Commands/CreateOrderCommand.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace LiteCharms.Features.Orders.Commands; - -public class CreateOrderCommand : IRequest> -{ - public Guid CustomerId { get; set; } - - public Guid ShoppingCartId { get; set; } - - public Guid? QuoteId { get; set; } - - public string[]? Requirements { get; set; } - - public string[]? Notes { get; set; } - - public string[]? Terms { get; set; } - - public bool DepositRequired { get; set; } - - private CreateOrderCommand(Guid customerId, Guid shoppingCartId, bool depositRequired, Guid? quoteId = null, string[]? requirements = null, string[]? notes = null, string[]? terms = null) - { - CustomerId = customerId; - ShoppingCartId = shoppingCartId; - DepositRequired = depositRequired; - QuoteId = quoteId; - Requirements = requirements; - Notes = notes; - Terms = terms; - } - - public static CreateOrderCommand Create(Guid customerId, Guid shoppingCartId, bool depositRequired, Guid? quoteId = null, string[]? requirements = null, string[]? notes = null, string[]? terms = null) - { - if (customerId == Guid.Empty) - throw new ArgumentException("CustomerId is required.", nameof(customerId)); - - if (shoppingCartId == Guid.Empty) - throw new ArgumentException("ShoppingCartId is required.", nameof(shoppingCartId)); - - return new(customerId, shoppingCartId, depositRequired, quoteId, requirements, notes, terms); - } -} diff --git a/LiteCharms.Features/Shop/Orders/Commands/Handlers/CreateOrderCommandHandler.cs b/LiteCharms.Features/Shop/Orders/Commands/Handlers/CreateOrderCommandHandler.cs deleted file mode 100644 index 95a599a..0000000 --- a/LiteCharms.Features/Shop/Orders/Commands/Handlers/CreateOrderCommandHandler.cs +++ /dev/null @@ -1,46 +0,0 @@ -using LiteCharms.Features.Shop; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Models; - -namespace LiteCharms.Features.Orders.Commands.Handlers; - -public class CreateOrderCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(CreateOrderCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if(!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer {request.CustomerId} does not exist.")); - - if(!await context.ShoppingCarts.AnyAsync(sc => sc.Id == request.ShoppingCartId, cancellationToken)) - return Result.Fail(new Error($"Shopping cart {request.ShoppingCartId} does not exist.")); - - if(request.QuoteId.HasValue && !await context.Quotes.AnyAsync(q => q.Id == request.QuoteId.Value, cancellationToken)) - return Result.Fail(new Error($"Quote {request.QuoteId.Value} does not exist.")); - - var newOrder = context.Orders.Add(new Entities.Order - { - CreatedAt = DateTime.UtcNow, - Status = OrderStatus.Pending, - CustomerId = request.CustomerId, - QuoteId = request.QuoteId, - ShoppingCartId = request.ShoppingCartId, - DepositRequired = request.DepositRequired, - Requirements = request.Requirements, - Notes = request.Notes, - Terms = request.Terms - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(newOrder.Entity.Id) - : Result.Fail(new Error($"Failed to create customer {request.CustomerId} order using shopping cart {request.ShoppingCartId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs b/LiteCharms.Features/Shop/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs deleted file mode 100644 index 2c0052f..0000000 --- a/LiteCharms.Features/Shop/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Orders.Commands.Handlers; - -public class UpdateOrderStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateOrderStatusCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken); - - if (order is null) - return Result.Fail(new Error($"Order {request.OrderId} not found")); - - order.Status = request.Status; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error($"Failed to update order {request.OrderId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Orders/Commands/UpdateOrderStatusCommand.cs b/LiteCharms.Features/Shop/Orders/Commands/UpdateOrderStatusCommand.cs deleted file mode 100644 index eefc734..0000000 --- a/LiteCharms.Features/Shop/Orders/Commands/UpdateOrderStatusCommand.cs +++ /dev/null @@ -1,31 +0,0 @@ -using LiteCharms.Features.Shop; -using LiteCharms.Models; - -namespace LiteCharms.Features.Orders.Commands; - -public class UpdateOrderStatusCommand : IRequest -{ - public Guid OrderId { get; set; } - - public OrderStatus Status { get; set; } - - public string? Note { get; set; } - - private UpdateOrderStatusCommand(Guid orderId, OrderStatus status, string? note) - { - OrderId = orderId; - Status = status; - Note = note; - } - - public static UpdateOrderStatusCommand Create(Guid orderId, OrderStatus status, string? note) - { - if (orderId == Guid.Empty) - throw new ArgumentException("OrderId is required.", nameof(orderId)); - - if (!Enum.IsDefined(typeof(OrderStatus), status)) - throw new ArgumentException("Invalid order status.", nameof(status)); - - return new(orderId, status, note); - } -} diff --git a/LiteCharms.Features/Shop/Orders/Models/Records.cs b/LiteCharms.Features/Shop/Orders/Models/Records.cs new file mode 100644 index 0000000..21dde34 --- /dev/null +++ b/LiteCharms.Features/Shop/Orders/Models/Records.cs @@ -0,0 +1,40 @@ +namespace LiteCharms.Features.Shop.Orders.Models; + +public record CreateOrder +{ + public required Guid CustomerId { get; set; } + + public required Guid ShoppingCartId { get; set; } + + public Guid? QuoteId { get; set; } + + public string[]? Requirements { get; set; } + + public string[]? Notes { get; set; } + + public string[]? Terms { get; set; } +} + +public record UpdateOrder +{ + public required Guid OrderId { get; set; } + + public required OrderStatus Status { get; set; } + + public string? InvoiceUrl { get; set; } + + public string[]? Notes { get; set; } + + public string[]? Requirements { get; set; } +} + +public record RefundCustomer +{ + public required Guid OrderId { get; set; } + + public required Guid CustomerId { get; set; } + + public required string Reason { get; set; } + + public required decimal Amount { get; set; } +} \ No newline at end of file diff --git a/LiteCharms.Features/Shop/Orders/OrderService.cs b/LiteCharms.Features/Shop/Orders/OrderService.cs new file mode 100644 index 0000000..705a099 --- /dev/null +++ b/LiteCharms.Features/Shop/Orders/OrderService.cs @@ -0,0 +1,260 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.Shop.Orders.Models; +using LiteCharms.Features.Shop.Postgres; +using static LiteCharms.Features.Extensions.Timezones; + +namespace LiteCharms.Features.Shop.Orders; + +public class OrderService(IDbContextFactory contextFactory) +{ + public async ValueTask> CreateOrderAsync(CreateOrder request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) + return Result.Fail(new Error($"Customer {request.CustomerId} does not exist.")); + + if (!await context.ShoppingCarts.AnyAsync(sc => sc.Id == request.ShoppingCartId, cancellationToken)) + return Result.Fail(new Error($"Shopping cart {request.ShoppingCartId} does not exist.")); + + if (request.QuoteId.HasValue && !await context.Quotes.AnyAsync(q => q.Id == request.QuoteId.Value, cancellationToken)) + return Result.Fail(new Error($"Quote {request.QuoteId.Value} does not exist.")); + + var newOrder = context.Orders.Add(new Entities.Order + { + Status = OrderStatus.Pending, + CustomerId = request.CustomerId, + Requirements = request.Requirements, + Notes = request.Notes, + Terms = request.Terms + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newOrder.Entity.Id) + : Result.Fail(new Error($"Failed to create customer {request.CustomerId} order using shopping cart {request.ShoppingCartId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerOrderRefundsAsync(Guid customerId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with Id {customerId} does not exist.")); + + var refunds = await context.OrderRefunds.AsNoTracking().AsSplitQuery() + .OrderByDescending(o => o.CreatedAt) + .Where(r => r.Order!.CustomerId == customerId).ToArrayAsync(cancellationToken); + + return refunds?.Length > 0 + ? Result.Ok(refunds.Select(r => r.ToModel()).ToArray()) + : Result.Fail(new Error($"No refunds found for customer with Id {customerId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerOrdersAsync(Guid customerId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AsNoTracking().AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with Id {customerId} does not exist.")); + + var orders = await context.Orders.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(o => o.CustomerId == customerId) + .ToArrayAsync(cancellationToken); + + return orders?.Length > 0 + ? Result.Ok(orders.Select(o => o.ToModel()).ToArray()) + : Result.Fail(new Error($"No orders found for customer with Id {customerId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrderRefundAsync(Guid orderId, Guid orderRefundId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var refund = await context.OrderRefunds.AsNoTracking() + .FirstOrDefaultAsync(r => r.OrderId == orderId && r.Id == orderRefundId, cancellationToken); + + return refund is not null + ? Result.Ok(refund.ToModel()) + : Result.Fail(new Error($"Refund {orderRefundId} not found for the given OrderId: {orderId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrderRefundAsync(Guid orderRefundId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var refund = await context.OrderRefunds.AsNoTracking().FirstOrDefaultAsync(r => r.Id == orderRefundId, cancellationToken); + + return refund is not null + ? Result.Ok(refund.ToModel()) + : Result.Fail($"Order refund could not be found with id {orderRefundId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrderRefundsAsync(Guid orderId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var refunds = await context.OrderRefunds.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(r => r.OrderId == orderId) + .ToArrayAsync(cancellationToken); + + return refunds?.Length > 0 + ? Result.Ok(refunds.Select(r => r.ToModel()).ToArray()) + : Result.Fail($"Order refunds could not be found with order id {orderId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrdersAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var orders = await context.Orders + .AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return orders?.Length > 0 + ? Result.Ok(orders.Select(o => o.ToModel()).ToArray()) + : Result.Fail(new Error($"No orders found for the specified date range {range.From} - {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> RefundCustomerAsync(RefundCustomer request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) + return Result.Fail(new Error($"Order with Id: {request.OrderId} does not exist")); + + if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) + return Result.Fail(new Error($"Customer with Id: {request.CustomerId} does not exist")); + + if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId && o.CustomerId == request.CustomerId, cancellationToken)) + return Result.Fail(new Error($"Order with Id: {request.OrderId} does not belong to Customer with Id: {request.CustomerId}")); + + var refund = context.OrderRefunds.Add(new Entities.OrderRefund + { + OrderId = request.OrderId, + Reason = request.Reason, + Amount = request.Amount + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(refund.Entity.Id) + : Result.Fail(new Error($"Failed to create refund for OrderId: {request.OrderId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateOrderRefundAsync(Guid orderRefundId, string reason, decimal amount, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var refund = await context.OrderRefunds.FirstOrDefaultAsync(r => r.Id == orderRefundId, cancellationToken); + + if (refund is null) + return Result.Fail($"Order refund not found with id {orderRefundId}"); + + refund.Reason = reason; + refund.Amount = amount; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update order refund {orderRefundId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateOrderStatusAsync(UpdateOrder request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken); + + if (order is null) + return Result.Fail(new Error($"Order {request.OrderId} not found")); + + order.Status = request.Status; + order.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + + if(!string.IsNullOrWhiteSpace(request.InvoiceUrl)) order.InvoiceUrl = request.InvoiceUrl; + + if(request.Requirements?.Length > 0) order.Requirements = request.Requirements; + if(request.Notes?.Length > 0) order.Notes = request.Notes; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update order {request.OrderId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/Shop/Orders/Queries/GetCustomerOrdersQuery.cs b/LiteCharms.Features/Shop/Orders/Queries/GetCustomerOrdersQuery.cs deleted file mode 100644 index f74f5c4..0000000 --- a/LiteCharms.Features/Shop/Orders/Queries/GetCustomerOrdersQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.Orders.Models; - -namespace LiteCharms.Features.Orders.Queries; - -public class GetCustomerOrdersQuery : IRequest> -{ - public Guid CustomerId { get; } - - private GetCustomerOrdersQuery(Guid customerId) => CustomerId = customerId; - - public static GetCustomerOrdersQuery Create(Guid customerId) - { - if (customerId == Guid.Empty) - throw new ArgumentException("CustomerId is required.", nameof(customerId)); - - return new(customerId); - } -} diff --git a/LiteCharms.Features/Shop/Orders/Queries/GetOrderRefundQuery.cs b/LiteCharms.Features/Shop/Orders/Queries/GetOrderRefundQuery.cs deleted file mode 100644 index 01295cf..0000000 --- a/LiteCharms.Features/Shop/Orders/Queries/GetOrderRefundQuery.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Features.Shop.Orders.Models; - -namespace LiteCharms.Features.Orders.Queries; - -public class GetOrderRefundQuery : IRequest> -{ - public Guid OrderId { get; set; } - - public Guid OrderRefundId { get; set; } - - private GetOrderRefundQuery(Guid orderId, Guid orderRefundId) - { - OrderId = orderId; - OrderRefundId = orderRefundId; - } - - public static GetOrderRefundQuery Create(Guid orderId, Guid orderRefundId) - { - if (orderId == Guid.Empty) - throw new ArgumentException("OrderId is required.", nameof(orderId)); - - if (orderRefundId == Guid.Empty) - throw new ArgumentException("OrderRefundId is required.", nameof(orderRefundId)); - - return new(orderId, orderRefundId); - } -} diff --git a/LiteCharms.Features/Shop/Orders/Queries/GetOrdersQuery.cs b/LiteCharms.Features/Shop/Orders/Queries/GetOrdersQuery.cs deleted file mode 100644 index 0fc45d8..0000000 --- a/LiteCharms.Features/Shop/Orders/Queries/GetOrdersQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Features.Shop.Orders.Models; - -namespace LiteCharms.Features.Orders.Queries; - -public class GetOrdersQuery : IRequest> -{ - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - public int MaxRecords { get; set; } - - private GetOrdersQuery(DateOnly from, DateOnly to, int maxRecords = 1000) - { - From = from; - To = to; - MaxRecords = maxRecords; - } - - public static GetOrdersQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000) - { - if (from > to) - throw new ArgumentException("From date cannot be greater than To date."); - - if(maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer."); - - return new(from, to, maxRecords); - } -} \ No newline at end of file diff --git a/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs b/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs deleted file mode 100644 index a2f65d9..0000000 --- a/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Orders.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Orders.Queries.Handlers; - -public class GetCustomerOrdersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerOrdersQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if(!await context.Customers.AsNoTracking().AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer with Id {request.CustomerId} does not exist.")); - - var orders = await context.Orders.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(o => o.CustomerId == request.CustomerId) - .ToArrayAsync(cancellationToken); - - return orders?.Length > 0 - ? Result.Ok(orders.Select(o => o.ToModel()).ToArray()) - : Result.Fail(new Error($"No orders found for customer with Id {request.CustomerId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs b/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs deleted file mode 100644 index 3493b11..0000000 --- a/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Orders.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Orders.Queries.Handlers; - -public class GetOrderRefundQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetOrderRefundQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var refund = await context.OrderRefunds.AsNoTracking() - .FirstOrDefaultAsync(r => r.OrderId == request.OrderId && r.Id == request.OrderRefundId, cancellationToken); - - return refund is not null - ? Result.Ok(refund.ToModel()) - : Result.Fail(new Error($"Refund {request.OrderRefundId} not found for the given OrderId: {request.OrderId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrdersQueryHandler.cs b/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrdersQueryHandler.cs deleted file mode 100644 index 898f3fc..0000000 --- a/LiteCharms.Features/Shop/Orders/Queries/Handlers/GetOrdersQueryHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Orders.Models; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Orders.Queries.Handlers; - -public class GetOrdersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetOrdersQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var orders = await context.Orders - .AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return orders?.Length > 0 - ? Result.Ok(orders.Select(o => o.ToModel()).ToArray()) - : Result.Fail(new Error($"No orders found for the specified date range {request.From} - {request.To}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs b/LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs deleted file mode 100644 index 9013f10..0000000 --- a/LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using LiteCharms.Features.Shop.Orders.Refunds.Commands; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Shop.Orders.Refunds.Commands.Handlers; - -public class RefundCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(RefundCustomerCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if(!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) - return Result.Fail(new Error($"Order with Id: {request.OrderId} does not exist")); - - if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer with Id: {request.CustomerId} does not exist")); - - if(!await context.Orders.AnyAsync(o => o.Id == request.OrderId && o.CustomerId == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Order with Id: {request.OrderId} does not belong to Customer with Id: {request.CustomerId}")); - - var refund = context.OrderRefunds.Add(new Entities.OrderRefund - { - OrderId = request.OrderId, - Reason = request.Reason, - Amount = request.Amount - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(refund.Entity.Id) - : Result.Fail(new Error($"Failed to create refund for OrderId: {request.OrderId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs b/LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs deleted file mode 100644 index ac88a49..0000000 --- a/LiteCharms.Features/Shop/Orders/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using LiteCharms.Features.Shop.Orders.Refunds.Commands; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Shop.Orders.Refunds.Commands.Handlers; - -public class UpdateOrderRefundCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateOrderRefundCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var refund = await context.OrderRefunds.FirstOrDefaultAsync(r => r.Id == request.OrderRefundId, cancellationToken); - - if (refund is null) - return Result.Fail($"Order refund not found with id {request.OrderRefundId}"); - - refund.Reason = request.Reason; - refund.Amount = request.Amount; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to update order refund {request.OrderRefundId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Orders/Refunds/Commands/RefundCustomerCommand.cs b/LiteCharms.Features/Shop/Orders/Refunds/Commands/RefundCustomerCommand.cs deleted file mode 100644 index ac5c75b..0000000 --- a/LiteCharms.Features/Shop/Orders/Refunds/Commands/RefundCustomerCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace LiteCharms.Features.Shop.Orders.Refunds.Commands; - -public class RefundCustomerCommand : IRequest> -{ - public Guid OrderId { get; set; } - - public Guid CustomerId { get; set; } - - public string? Reason { get; set; } - - public decimal Amount { get; set; } - - private RefundCustomerCommand(Guid orderId, Guid customerId, string? reason, decimal amount) - { - OrderId = orderId; - CustomerId = customerId; - Reason = reason; - Amount = amount; - CustomerId = customerId; - } - - public static RefundCustomerCommand Create(Guid orderId, Guid customerId, string? reason, decimal amount) - { - if (orderId == Guid.Empty) - throw new ArgumentException("OrderId is required", nameof(orderId)); - - if (customerId == Guid.Empty) - throw new ArgumentException("CustomerId is required", nameof(customerId)); - - if (amount <= 0) - throw new ArgumentException("Amount must be greater than zero", nameof(amount)); - - if (string.IsNullOrWhiteSpace(reason)) - throw new ArgumentException("Reason is required", nameof(reason)); - - return new(orderId, customerId, reason, amount); - } -} diff --git a/LiteCharms.Features/Shop/Orders/Refunds/Commands/UpdateOrderRefundCommand.cs b/LiteCharms.Features/Shop/Orders/Refunds/Commands/UpdateOrderRefundCommand.cs deleted file mode 100644 index 9f69b2f..0000000 --- a/LiteCharms.Features/Shop/Orders/Refunds/Commands/UpdateOrderRefundCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace LiteCharms.Features.Shop.Orders.Refunds.Commands; - -public class UpdateOrderRefundCommand : IRequest -{ - public Guid OrderRefundId { get; set; } - - public string? Reason { get; set; } - - public decimal Amount { get; set; } - - private UpdateOrderRefundCommand(Guid orderRefundId, string? reason, decimal amount) - { - OrderRefundId = orderRefundId; - Reason = reason; - Amount = amount; - } - - public static UpdateOrderRefundCommand Create(Guid orderRefundId, string? reason, decimal amount) - { - if (orderRefundId == Guid.Empty) - throw new ArgumentException("Order refund id is required.", nameof(orderRefundId)); - - if (string.IsNullOrWhiteSpace(reason)) - throw new ArgumentException("Refund update reason is required"); - - return new(orderRefundId, reason, amount); - } -} diff --git a/LiteCharms.Features/Shop/Orders/Refunds/Queries/GetCustomerRefundsQuery.cs b/LiteCharms.Features/Shop/Orders/Refunds/Queries/GetCustomerRefundsQuery.cs deleted file mode 100644 index 56d19f6..0000000 --- a/LiteCharms.Features/Shop/Orders/Refunds/Queries/GetCustomerRefundsQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.Orders.Models; - -namespace LiteCharms.Features.Shop.Orders.Refunds.Queries; - -public class GetCustomerRefundsQuery : IRequest> -{ - public Guid CustomerId { get; set; } - - private GetCustomerRefundsQuery(Guid customerId) => CustomerId = customerId; - - public static GetCustomerRefundsQuery Create(Guid customerId) - { - if (customerId == Guid.Empty) - throw new ArgumentException("CustomerId is required.", nameof(customerId)); - - return new(customerId); - } -} diff --git a/LiteCharms.Features/Shop/Orders/Refunds/Queries/GetRefundQuery.cs b/LiteCharms.Features/Shop/Orders/Refunds/Queries/GetRefundQuery.cs deleted file mode 100644 index ddeaa7b..0000000 --- a/LiteCharms.Features/Shop/Orders/Refunds/Queries/GetRefundQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.Orders.Models; - -namespace LiteCharms.Features.Shop.Orders.Refunds.Queries; - -public class GetRefundQuery : IRequest> -{ - public Guid OrderRefundId { get; set; } - - private GetRefundQuery(Guid orderRefundId) => OrderRefundId = orderRefundId; - - public static GetRefundQuery Create(Guid orderRefundId) - { - if(orderRefundId == Guid.Empty) - throw new ArgumentException("Customer ID is required.", nameof(orderRefundId)); - - return new(orderRefundId); - } -} diff --git a/LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs b/LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs deleted file mode 100644 index 8439f88..0000000 --- a/LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Orders.Models; -using LiteCharms.Features.Shop.Orders.Refunds.Queries; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Shop.Orders.Refunds.Queries.Handlers; - -public class GetCustomerRefundsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerRefundsQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if(!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer with Id {request.CustomerId} does not exist.")); - - var refunds = await context.OrderRefunds.AsNoTracking().AsSplitQuery() - .OrderByDescending(o => o.CreatedAt) - .Where(r => r.Order!.CustomerId == request.CustomerId).ToArrayAsync(cancellationToken); - - return refunds?.Length > 0 - ? Result.Ok(refunds.Select(r => r.ToModel()).ToArray()) - : Result.Fail(new Error($"No refunds found for customer with Id {request.CustomerId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetRefundQueryHandler.cs b/LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetRefundQueryHandler.cs deleted file mode 100644 index a363945..0000000 --- a/LiteCharms.Features/Shop/Orders/Refunds/Queries/Handlers/GetRefundQueryHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Orders.Models; -using LiteCharms.Features.Shop.Orders.Refunds.Queries; -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Shop.Orders.Refunds.Queries.Handlers; - -public class GetRefundQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetRefundQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var refund = await context.OrderRefunds.AsNoTracking().FirstOrDefaultAsync(r => r.Id == request.OrderRefundId, cancellationToken); - - return refund is not null - ? Result.Ok(refund.ToModel()) - : Result.Fail($"Order refund could not be found with id {request.OrderRefundId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Products/Models/Records.cs b/LiteCharms.Features/Shop/Products/Models/Records.cs new file mode 100644 index 0000000..b5579b8 --- /dev/null +++ b/LiteCharms.Features/Shop/Products/Models/Records.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.Shop.Products.Models; + +public record CreateProduct +{ + public required string Name { get; set; } + + public required string Summary { get; set; } + + public required string Description { get; set; } + + public required string ImageUrl { get; set; } + + public string[]? Thumbnails { get; set; } +} diff --git a/LiteCharms.Features/Shop/Products/ProductService.cs b/LiteCharms.Features/Shop/Products/ProductService.cs new file mode 100644 index 0000000..4b9b294 --- /dev/null +++ b/LiteCharms.Features/Shop/Products/ProductService.cs @@ -0,0 +1,229 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Products.Models; +using static LiteCharms.Features.Extensions.Timezones; + +namespace LiteCharms.Features.Shop.Products; + +public class ProductService(IDbContextFactory contextFactory) +{ + public async ValueTask ChangeProductPriceStatusAsync(Guid productPriceId, bool active, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var price = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + + if (price is null) + return Result.Fail($"Could not find product price with ID {productPriceId}"); + + price.Active = active; + price.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to change product price by ID {productPriceId}"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask ChangeProductStatusAsync(Guid productId, bool active, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + + if (product is null) + return Result.Fail($"Could not find product with ID {productId}"); + + product.Active = active; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to change product status by ID {productId}"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateProductAsync(CreateProduct request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken)) + return Result.Fail($"A product by the same name '{request.Name}' already exists"); + + var newProduct = context.Products.Add(new Entities.Product + { + Name = request.Name, + Summary = request.Summary, + Description = request.Description, + ImageUrl = request.ImageUrl, + Thumbnails = request.Thumbnails + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newProduct.Entity.Id) + : Result.Fail($"Failed to create new product '{request.Name}'"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateProductPriceAsync(Guid productId, decimal price, decimal discount = 0, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var newProductPrice = context.ProductPrices.Add(new Entities.ProductPrice + { + Price = price, + Discount = discount, + ProductId = productId + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newProductPrice.Entity.Id) + : Result.Fail($"Failed to create new product price for product id {productId}"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductAsync(Guid productId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + + return product is not null + ? Result.Ok(product.ToModel()) + : Result.Fail(new Error($"Product with ID {productId} not found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductPriceAsync(Guid productPriceId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Products.AnyAsync(p => p.Id == productPriceId, cancellationToken)) + return Result.Fail(new Error($"Product {productPriceId} not found.")); + + var productPrice = await context.ProductPrices.AsNoTracking() + .OrderByDescending(pp => pp.CreatedAt) + .FirstOrDefaultAsync(pp => pp.Id == productPriceId, cancellationToken); + + return productPrice is not null + ? Result.Ok(productPrice.ToModel()) + : Result.Fail(new Error($"Product price {productPriceId} not found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductPricesAsync(int maxRecords = 1000, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var products = await context.ProductPrices.AsNoTracking() + .OrderByDescending(o => o.Id) + .Take(maxRecords) + .ToArrayAsync(cancellationToken); + + return Result.Ok(products.Select(p => p.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductsAsync(int maxRecords = 1000, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var products = await context.Products.AsNoTracking() + .OrderByDescending(o => o.Id) + .Take(maxRecords) + .ToArrayAsync(cancellationToken); + + return Result.Ok(products.Select(p => p.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> ReplaceProductPriceAsync(Guid productPriceId, decimal price, decimal discount = 0, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var existingPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + + if (existingPrice is null) + return Result.Fail($"Could not find product price with ID {productPriceId}"); + + existingPrice.Active = false; + existingPrice.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + + if (!(await context.SaveChangesAsync(cancellationToken) > 0)) + return Result.Fail($"Failed to deactivate existing price of ID {productPriceId}, try again later"); + + var result = await CreateProductPriceAsync(existingPrice.ProductId, price, discount, cancellationToken); + + if(result.IsFailed) + { + var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + + existingPrice.Active = true; + existingPrice.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Fail("Reverted to old price, creation of new price failed") + : Result.Fail($"Failed to reactivate price of ID {productPriceId} after new price creation failed"); + } + + return Result.Ok(result.Value); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/Shop/Products/Queries/GetProductPriceQuery.cs b/LiteCharms.Features/Shop/Products/Queries/GetProductPriceQuery.cs deleted file mode 100644 index f118bcc..0000000 --- a/LiteCharms.Features/Shop/Products/Queries/GetProductPriceQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.Products.Models; - -namespace LiteCharms.Features.Products.Queries; - -public class GetProductPriceQuery : IRequest> -{ - public Guid ProductId { get; set; } - - private GetProductPriceQuery(Guid productId) => ProductId = productId; - - public static GetProductPriceQuery Create(Guid productId) - { - if (productId == Guid.Empty) - throw new ArgumentException("ProductId is required.", nameof(productId)); - - return new(productId); - } -} diff --git a/LiteCharms.Features/Shop/Products/Queries/GetProductPricesQuery.cs b/LiteCharms.Features/Shop/Products/Queries/GetProductPricesQuery.cs deleted file mode 100644 index 5ba4be2..0000000 --- a/LiteCharms.Features/Shop/Products/Queries/GetProductPricesQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.Products.Models; - -namespace LiteCharms.Features.Products.Queries; - -public class GetProductPricesQuery : IRequest> -{ - public int MaxRecords { get; set; } - - private GetProductPricesQuery(int maxRecords = 1000) => MaxRecords = maxRecords; - - public static GetProductPricesQuery Create(int maxRecords = 1000) - { - if (maxRecords <= 0) - throw new ArgumentOutOfRangeException(nameof(maxRecords), "MaxRecords must be greater than zero."); - - return new(maxRecords); - } -} diff --git a/LiteCharms.Features/Shop/Products/Queries/GetProductQuery.cs b/LiteCharms.Features/Shop/Products/Queries/GetProductQuery.cs deleted file mode 100644 index cd811ac..0000000 --- a/LiteCharms.Features/Shop/Products/Queries/GetProductQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.Products.Models; - -namespace LiteCharms.Features.Products.Queries; - -public class GetProductQuery : IRequest> -{ - public Guid ProductId { get; set; } - - private GetProductQuery(Guid productId) => ProductId = productId; - - public static GetProductQuery Create(Guid productId) - { - if(productId == Guid.Empty) - throw new ArgumentException("Product ID is required.", nameof(productId)); - - return new(productId); - } -} diff --git a/LiteCharms.Features/Shop/Products/Queries/GetProductsQuery.cs b/LiteCharms.Features/Shop/Products/Queries/GetProductsQuery.cs deleted file mode 100644 index a60670d..0000000 --- a/LiteCharms.Features/Shop/Products/Queries/GetProductsQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.Products.Models; - -namespace LiteCharms.Features.Products.Queries; - -public class GetProductsQuery : IRequest> -{ - public int MaxRecords { get; set; } - - private GetProductsQuery(int maxRecords = 1000) => MaxRecords = maxRecords; - - public static GetProductsQuery Create(int maxRecords = 1000) - { - if (maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer."); - - return new(maxRecords); - } -} diff --git a/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPriceQueryHandler.cs b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPriceQueryHandler.cs deleted file mode 100644 index c1383de..0000000 --- a/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPriceQueryHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.Products.Models; - -namespace LiteCharms.Features.Products.Queries.Handlers; - -public class GetProductPriceQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetProductPriceQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if(!await context.Products.AnyAsync(p => p.Id == request.ProductId, cancellationToken)) - return Result.Fail(new Error($"Product {request.ProductId} not found.")); - - var productPrice = await context.ProductPrices.AsNoTracking() - .Where(pp => pp.ProductId == request.ProductId && pp.Active) - .OrderByDescending(pp => pp.CreatedAt) - .FirstOrDefaultAsync(cancellationToken); - - return productPrice is not null - ? Result.Ok(productPrice.ToModel()) - : Result.Fail(new Error($"Product price {request.ProductId} not found.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPricesQueryHandler.cs b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPricesQueryHandler.cs deleted file mode 100644 index 1bc5489..0000000 --- a/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductPricesQueryHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.Products.Models; - -namespace LiteCharms.Features.Products.Queries.Handlers; - -public class GetProductPricesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetProductPricesQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var products = await context.ProductPrices.AsNoTracking() - .OrderByDescending(o => o.Id) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return Result.Ok(products.Select(p => p.ToModel()).ToArray()); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductQueryHandler.cs b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductQueryHandler.cs deleted file mode 100644 index d60591e..0000000 --- a/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.Products.Models; - -namespace LiteCharms.Features.Products.Queries.Handlers; - -public class GetProductQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetProductQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var product = await context.Products.FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken); - - return product is not null - ? Result.Ok(product.ToModel()) - : Result.Fail(new Error($"Product with ID {request.ProductId} not found.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductsQueryHandler.cs b/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductsQueryHandler.cs deleted file mode 100644 index 0ad9f40..0000000 --- a/LiteCharms.Features/Shop/Products/Queries/Handlers/GetProductsQueryHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.Products.Models; - -namespace LiteCharms.Features.Products.Queries.Handlers; - -public class GetProductsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetProductsQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var products = await context.Products.AsNoTracking() - .OrderByDescending(o => o.Id) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return Result.Ok(products.Select(p => p.ToModel()).ToArray()); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToOrderCommand.cs b/LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToOrderCommand.cs deleted file mode 100644 index 498aa4a..0000000 --- a/LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToOrderCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.Quotes.Commands; - -public class AssignQuoteToOrderCommand : IRequest -{ - public Guid OrderId { get; set; } - - public Guid QuoteId { get; set; } - - private AssignQuoteToOrderCommand(Guid orderId, Guid quoteId) - { - OrderId = orderId; - QuoteId = quoteId; - } - - public static AssignQuoteToOrderCommand Create(Guid orderId, Guid quoteId) - { - if(orderId == Guid.Empty) - throw new ArgumentException("Order ID is required.", nameof(orderId)); - - if(quoteId == Guid.Empty) - throw new ArgumentException("Quote ID is required.", nameof(quoteId)); - - return new AssignQuoteToOrderCommand(orderId, quoteId); - } -} diff --git a/LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs b/LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs deleted file mode 100644 index 61c0902..0000000 --- a/LiteCharms.Features/Shop/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.Quotes.Commands; - -public class AssignQuoteToShoppingCartCommand : IRequest -{ - public Guid QuoteId { get; set; } - - public Guid ShoppingCartId { get; set; } - - private AssignQuoteToShoppingCartCommand(Guid quoteId, Guid shoppingCartId) - { - QuoteId = quoteId; - ShoppingCartId = shoppingCartId; - } - - public static AssignQuoteToShoppingCartCommand Create(Guid quoteId, Guid shoppingCartId) - { - if(quoteId == Guid.Empty) - throw new ArgumentException("QuoteId cannot be empty.", nameof(quoteId)); - - if (shoppingCartId == Guid.Empty) - throw new ArgumentException("ShoppingCartId cannot be empty.", nameof(shoppingCartId)); - - return new(quoteId, shoppingCartId); - } -} diff --git a/LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs b/LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs deleted file mode 100644 index 266e8d9..0000000 --- a/LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Quotes.Commands.Handlers; - -public class AssignQuoteToOrderCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(AssignQuoteToOrderCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken); - - if (order is null) - return Result.Fail(new Error($"Order with id {request.OrderId} not found")); - - if(!await context.Quotes.AnyAsync(q => q.Id == request.OrderId, cancellationToken)) - return Result.Fail(new Error($"Quote with id {request.QuoteId} not found")); - - if(order.QuoteId == request.QuoteId) - return Result.Fail(new Error($"Quote with id {request.QuoteId} is already assigned to order with id {request.OrderId}")); - - order.QuoteId = request.QuoteId; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error($"Failed to assign quote with id {request.QuoteId} to order with id {request.OrderId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs deleted file mode 100644 index a9f6b12..0000000 --- a/LiteCharms.Features/Shop/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Quotes.Commands.Handlers; - -public class AssignQuoteToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(AssignQuoteToShoppingCartCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var shoppingCart = await context.Orders.FirstOrDefaultAsync(o => o.Id == request.ShoppingCartId, cancellationToken); - - if (shoppingCart is null) - return Result.Fail(new Error($"ShoppingCart with id {request.ShoppingCartId} not found")); - - if(!await context.Quotes.AnyAsync(q => q.Id == request.QuoteId, cancellationToken)) - return Result.Fail(new Error($"Quote with id {request.QuoteId} not found")); - - shoppingCart.QuoteId = request.QuoteId; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error("Failed to assign quote to shopping cart")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs b/LiteCharms.Features/Shop/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs deleted file mode 100644 index 8e80a52..0000000 --- a/LiteCharms.Features/Shop/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.Quotes.Commands.Handlers; - -public class UpdateQuoteStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateQuoteStatusCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var quote = await context.Quotes.FirstOrDefaultAsync(q => q.Id == request.QuoteId, cancellationToken); - - if (quote is null) - return Result.Fail(new Error("Quote not found.")); - - quote.Status = request.Status; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error("Failed to update quote status.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Quotes/Commands/UpdateQuoteStatusCommand.cs b/LiteCharms.Features/Shop/Quotes/Commands/UpdateQuoteStatusCommand.cs deleted file mode 100644 index 14c9684..0000000 --- a/LiteCharms.Features/Shop/Quotes/Commands/UpdateQuoteStatusCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -using LiteCharms.Features.Shop; -using LiteCharms.Models; - -namespace LiteCharms.Features.Quotes.Commands; - -public class UpdateQuoteStatusCommand : IRequest -{ - public Guid QuoteId { get; set; } - - public QuoteStatus Status { get; set; } - - private UpdateQuoteStatusCommand(Guid quoteId, QuoteStatus status) - { - QuoteId = quoteId; - Status = status; - } - - public static UpdateQuoteStatusCommand Create(Guid quoteId, QuoteStatus status) - { - if(quoteId == Guid.Empty) - throw new ArgumentException("Quote ID cannot be empty.", nameof(quoteId)); - - return new(quoteId, status); - } -} diff --git a/LiteCharms.Features/Shop/Quotes/Queries/GetCustomerQuotesQuery.cs b/LiteCharms.Features/Shop/Quotes/Queries/GetCustomerQuotesQuery.cs deleted file mode 100644 index 37adc17..0000000 --- a/LiteCharms.Features/Shop/Quotes/Queries/GetCustomerQuotesQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.Quotes.Models; - -namespace LiteCharms.Features.Quotes.Queries; - -public class GetCustomerQuotesQuery : IRequest> -{ - public Guid CustomerId { get; set; } - - private GetCustomerQuotesQuery(Guid customerId) => CustomerId = customerId; - - public static GetCustomerQuotesQuery Create(Guid customerId) - { - if (customerId == Guid.Empty) - throw new ArgumentException("CustomerId is required."); - - return new(customerId); - } -} \ No newline at end of file diff --git a/LiteCharms.Features/Shop/Quotes/Queries/GetQuoteQuery.cs b/LiteCharms.Features/Shop/Quotes/Queries/GetQuoteQuery.cs deleted file mode 100644 index 756788e..0000000 --- a/LiteCharms.Features/Shop/Quotes/Queries/GetQuoteQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.Quotes.Models; - -namespace LiteCharms.Features.Quotes.Queries; - -public class GetQuoteQuery : IRequest> -{ - public Guid QuoteId { get; set; } - - private GetQuoteQuery(Guid quoteId) => QuoteId = quoteId; - - public static GetQuoteQuery Create(Guid quoteId) - { - if(quoteId == Guid.Empty) - throw new ArgumentException("Quote ID is required.", nameof(quoteId)); - - return new(quoteId); - } -} diff --git a/LiteCharms.Features/Shop/Quotes/Queries/GetQuotesQuery.cs b/LiteCharms.Features/Shop/Quotes/Queries/GetQuotesQuery.cs deleted file mode 100644 index 4e7db83..0000000 --- a/LiteCharms.Features/Shop/Quotes/Queries/GetQuotesQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Features.Shop.Quotes.Models; - -namespace LiteCharms.Features.Quotes.Queries; - -public class GetQuotesQuery : IRequest> -{ - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - public int MaxRecords { get; set; } - - private GetQuotesQuery(DateOnly from, DateOnly to, int maxRecords = 1000) - { - From = from; - To = to; - MaxRecords = maxRecords; - } - - public static GetQuotesQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000) - { - if (from > to) - throw new ArgumentException("From date cannot be greater than To date."); - - if (maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer."); - - return new(from, to, maxRecords); - } -} diff --git a/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs b/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs deleted file mode 100644 index 811df36..0000000 --- a/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Quotes.Queries; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.Quotes.Models; - -namespace LiteCharms.Features.Quotes.Queries.Handlers; - -public class GetCustomerQuotesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerQuotesQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer with Id {request.CustomerId} does not exist.")); - - var quotes = await context.Quotes.AsNoTracking() - .Where(q => q.CustomerId == request.CustomerId).ToArrayAsync(cancellationToken); - - return quotes?.Length > 0 - ? Result.Ok(quotes.Select(q => q.ToModel()).ToArray()) - : Result.Fail(new Error($"No quotes found for customer with Id {request.CustomerId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs b/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs deleted file mode 100644 index 35c2d39..0000000 --- a/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.Quotes.Models; - -namespace LiteCharms.Features.Quotes.Queries.Handlers; - -public class GetQuoteQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetQuoteQuery request, CancellationToken cancellationToken) - { - try - { - await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var quote = await context.Quotes.AsNoTracking().FirstOrDefaultAsync(q => q.Id == request.QuoteId, cancellationToken); - - return quote is not null - ? Result.Ok(quote.ToModel()) - : Result.Fail(new Error($"Quote with ID {request.QuoteId} not found.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuotesHandler.cs b/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuotesHandler.cs deleted file mode 100644 index f7ad77b..0000000 --- a/LiteCharms.Features/Shop/Quotes/Queries/Handlers/GetQuotesHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.Quotes.Models; - -namespace LiteCharms.Features.Quotes.Queries.Handlers; - -public class GetQuotesHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetQuotesQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var quotes = await context.Quotes.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return quotes?.Length > 0 - ? Result.Ok(quotes.Select(o => o.ToModel()).ToArray()) - : Result.Fail(new Error($"No quotes found for the specified date range {request.From} - {request.To}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/Quotes/QuoteService.cs b/LiteCharms.Features/Shop/Quotes/QuoteService.cs new file mode 100644 index 0000000..7e726c1 --- /dev/null +++ b/LiteCharms.Features/Shop/Quotes/QuoteService.cs @@ -0,0 +1,154 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Quotes.Models; +using static LiteCharms.Features.Extensions.Timezones; + +namespace LiteCharms.Features.Shop.Quotes; + +public class QuoteService(IDbContextFactory contextFactory) +{ + public async ValueTask AssignQuoteToOrderAsync(Guid quoteId, Guid orderId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var quote = await context.Quotes.FirstOrDefaultAsync(o => o.Id == quoteId, cancellationToken); + + if (quote is null) + return Result.Fail(new Error($"Quote with id {orderId} not found")); + + if (!await context.Orders.AnyAsync(q => q.Id == orderId, cancellationToken)) + return Result.Fail(new Error($"Order with id {quoteId} not found")); + + if (quote.OrderId == orderId) + return Result.Fail(new Error($"Quote with id {quoteId} is already assigned to order with id {orderId}")); + + quote.OrderId = orderId; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to assign quote with id {quoteId} to order with id {orderId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask AssignQuoteToShoppingCartAsync(Guid quoteId, Guid shoppingCartId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var quote = await context.Quotes.FirstOrDefaultAsync(o => o.Id == quoteId, cancellationToken); + + if (quote is null) + return Result.Fail(new Error($"Quote with id {quoteId} not found")); + + if (!await context.ShoppingCarts.AnyAsync(q => q.Id == shoppingCartId, cancellationToken)) + return Result.Fail(new Error($"Shopping Cart with id {shoppingCartId} not found")); + + quote.ShoppingCartId = shoppingCartId; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to assign quote to shopping cart")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerQuotesAsync(Guid customerId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with Id {customerId} does not exist.")); + + var quotes = await context.Quotes.AsNoTracking() + .Where(q => q.CustomerId == customerId).ToArrayAsync(cancellationToken); + + return quotes?.Length > 0 + ? Result.Ok(quotes.Select(q => q.ToModel()).ToArray()) + : Result.Fail(new Error($"No quotes found for customer with Id {customerId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetQuoteAsync(Guid quoteId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var quote = await context.Quotes.AsNoTracking().FirstOrDefaultAsync(q => q.Id == quoteId, cancellationToken); + + return quote is not null + ? Result.Ok(quote.ToModel()) + : Result.Fail(new Error($"Quote with ID {quoteId} not found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetQuotesAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var quotes = await context.Quotes.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return quotes?.Length > 0 + ? Result.Ok(quotes.Select(o => o.ToModel()).ToArray()) + : Result.Fail(new Error($"No quotes found for the specified date range {range.From} - {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateQuoteStatusAsync(Guid quoteId, QuoteStatus status, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var quote = await context.Quotes.FirstOrDefaultAsync(q => q.Id == quoteId, cancellationToken); + + if (quote is null) + return Result.Fail(new Error("Quote not found.")); + + quote.Status = status; + quote.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to update quote status.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs deleted file mode 100644 index 009fccd..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class AddItemToShoppingCartCommand : IRequest -{ - public Guid ShoppingCartId { get; set; } - - public Guid ProductPriceId { get; set; } - - public int Quantity { get; set; } - - private AddItemToShoppingCartCommand(Guid shoppingCartId, Guid productPriceId, int quantity = 1) - { - ShoppingCartId = shoppingCartId; - ProductPriceId = productPriceId; - Quantity = quantity; - } - - public static AddItemToShoppingCartCommand Create(Guid shoppingCartId, Guid productPriceId, int quantity = 1) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); - - if (productPriceId == Guid.Empty) - throw new ArgumentException($"Product item required", nameof(productPriceId)); - - if(quantity <= 0) throw new ArgumentException($"Quantity must be at least 1", nameof(quantity)); - - return new(shoppingCartId, productPriceId, quantity); - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs deleted file mode 100644 index 81e8bac..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/AddPackageToShoppingCartCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class AddPackageToShoppingCartCommand : IRequest -{ - public Guid ShoppingCartId { get; set; } - - public Guid PackageId { get; set; } - - private AddPackageToShoppingCartCommand(Guid shoppingCartId, Guid packageId) - { - ShoppingCartId = shoppingCartId; - PackageId = packageId; - } - - public static AddPackageToShoppingCartCommand Create(Guid shoppingCartId, Guid packageId) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); - - if (packageId == Guid.Empty) - throw new ArgumentException($"Package ID is required", nameof(packageId)); - - return new(shoppingCartId, packageId); - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/CreateShoppingCartCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/CreateShoppingCartCommand.cs deleted file mode 100644 index fdc7b0e..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/CreateShoppingCartCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class CreateShoppingCartCommand : IRequest> -{ - public Guid? CustomerId { get; set; } - - public Guid? OrderId { get; set; } - - public Guid? QuoteId { get; set; } - - private CreateShoppingCartCommand(Guid customerId, Guid? orderId = null, Guid? quoteId = null) - { - CustomerId = customerId; - OrderId = orderId; - QuoteId = quoteId; - } - - public static CreateShoppingCartCommand Create(Guid customerId, Guid? orderId = null, Guid? quoteId = null) - { - if (customerId == Guid.Empty) - throw new ArgumentException($"Customer ID is required", nameof(customerId)); - - return new(customerId, orderId, quoteId); - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs deleted file mode 100644 index f535ca4..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class EmptyShoppingCartCommand : IRequest -{ - public Guid ShoppingCartId { get; set; } - - private EmptyShoppingCartCommand(Guid shoppingCartId) => ShoppingCartId = shoppingCartId; - - public static EmptyShoppingCartCommand Create(Guid shoppingCartId) - { - if(shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart ID is required.", nameof(shoppingCartId)); - - return new(shoppingCartId); - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs deleted file mode 100644 index 7a2cd2c..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class AddItemToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(AddItemToShoppingCartCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ProductPrices.AnyAsync(c => c.Id == request.ProductPriceId, cancellationToken)) - return Result.Fail($"Product item could not be found with id {request.ProductPriceId}"); - - var cart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == request.ShoppingCartId, cancellationToken); - - if (cart is null) - return Result.Fail($"Shopping cart could not be found with id {request.ShoppingCartId}"); - - if (cart.ShoppingCartItems?.Any(i => i.ProductPriceId == request.ProductPriceId) == true) - return Result.Fail($"Item already in shopping cart with id {request.ShoppingCartId}"); - - context.ShoppingCartItems.Add(new Entities.ShoppingCartItem - { - ShoppingCartId = request.ShoppingCartId, - ProductPriceId = request.ProductPriceId, - Quantity = request.Quantity - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to add cart item with id {request.ProductPriceId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs deleted file mode 100644 index a634375..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/AddPackageToShoppingCartCommandHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class AddPackageToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(AddPackageToShoppingCartCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Packages.AnyAsync(p => p.Id == request.PackageId, cancellationToken)) - return Result.Fail($"Package cold not be found by ID {request.PackageId}"); - - var shoppingCart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == request.ShoppingCartId, cancellationToken); - - if (shoppingCart is null) - return Result.Fail($"Shopping cart could not be found by ID {request.ShoppingCartId}"); - - if (!await context.ShoppingCartPackages.AnyAsync(cp => cp.ShoppingCartId == request.ShoppingCartId, cancellationToken)) - return Result.Fail($"Package {request.PackageId} is already in the cart"); - - var newShoppingCartPackage = context.ShoppingCartPackages.Add(new Entities.ShoppingCartPackage - { - ShoppingCartId = request.ShoppingCartId, - PackageId = request.PackageId - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Could not add package of id {request.PackageId} to shopping cart {request.ShoppingCartId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs deleted file mode 100644 index 2d51f2e..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class CreateShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(CreateShoppingCartCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail($"Customer could not be found with id {request.CustomerId}"); - - var cart = context.ShoppingCarts.Add(new Entities.ShoppingCart - { - CustomerId = request.CustomerId, - OrderId = request.OrderId, - QuoteId = request.QuoteId - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(cart.Entity.Id) - : Result.Fail($"Failed to create shopping cart for customer id {request.CustomerId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs deleted file mode 100644 index 0043a5f..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class EmptyShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(EmptyShoppingCartCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ShoppingCarts.AnyAsync(c => c.Id == request.ShoppingCartId, cancellationToken)) - return Result.Fail($"Shopping could not be found with id {request.ShoppingCartId}"); - - if (await context.ShoppingCartItems.CountAsync(i => i.ShoppingCartId == request.ShoppingCartId, cancellationToken) == 0) - return Result.Ok(); - - var cartItems = await context.ShoppingCartItems.Where(i => i.ShoppingCartId == request.ShoppingCartId).ToListAsync(cancellationToken); - - context.RemoveRange(cartItems); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Could not empty cart with id {request.ShoppingCartId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs deleted file mode 100644 index 2bd5b3a..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemovePackageFromShoppingCartCommandHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class RemovePackageFromShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(RemovePackageFromShoppingCartCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ShoppingCarts.AnyAsync(c => c.Id == request.ShoppingCartId, cancellationToken)) - return Result.Fail($"Shopping cart could not be found by ID {request.ShoppingCartId}"); - - if (!await context.ShoppingCartPackages.AnyAsync(p => p.Id == request.ShoppingCartPackageId, cancellationToken)) - return Result.Fail($"Shopping cart package {request.ShoppingCartPackageId} is not in the shopping cart {request.ShoppingCartId}"); - - var shoppingCartPackage = await context.ShoppingCartPackages.FirstOrDefaultAsync(cp => cp.Id == request.ShoppingCartPackageId, cancellationToken); - - if (shoppingCartPackage is null) - return Result.Ok(); - - context.ShoppingCartPackages.Remove(shoppingCartPackage!); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Could remove package of id {request.ShoppingCartPackageId} from shopping cart {request.ShoppingCartId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs deleted file mode 100644 index 442c944..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs +++ /dev/null @@ -1,36 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class RemoveShoppingCartItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(RemoveShoppingCartItemCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ProductPrices.AnyAsync(c => c.Id == request.ShoppingCartItemId, cancellationToken)) - return Result.Fail($"Product item could not be found with id {request.ShoppingCartItemId}"); - - var cart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == request.ShoppingCartId, cancellationToken); - - if (cart is null) - return Result.Fail($"Shopping cart item could not be found with id {request.ShoppingCartId}"); - - var item = await context.ShoppingCartItems.FirstOrDefaultAsync(i => i.Id == request.ShoppingCartItemId, cancellationToken); - - if (item is null) return Result.Ok(); - - context.ShoppingCartItems.Remove(item); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to remove shopping cart item with id {request.ShoppingCartItemId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs deleted file mode 100644 index 3e54242..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Features.Shop.Postgres; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class UpdateShoppingCartItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateShoppingCartItemCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ShoppingCarts.AnyAsync(c => c.Id == request.ShoppingCartId, cancellationToken)) - return Result.Fail($"Shopping could not be found with id {request.ShoppingCartId}"); - - var item = await context.ShoppingCartItems.FirstOrDefaultAsync(i => i.ShoppingCartId == request.ShoppingCartId, cancellationToken); - - if(item is null) - return Result.Fail($"Shopping cart item could not be found with id {request.ShoppingCartItemId}"); - - item.Quantity = request.Quantity; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to update cart item quntity"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs deleted file mode 100644 index 6aa8f25..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/RemovePackageFromShoppingCartCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class RemovePackageFromShoppingCartCommand : IRequest -{ - public Guid ShoppingCartId { get; set; } - - public Guid ShoppingCartPackageId { get; set; } - - private RemovePackageFromShoppingCartCommand(Guid shoppingCartId, Guid shoppingCartPackageId) - { - ShoppingCartId = shoppingCartId; - ShoppingCartPackageId = shoppingCartPackageId; - } - - public static RemovePackageFromShoppingCartCommand Create(Guid shoppingCartId, Guid shoppingCartPackageId) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); - - if (shoppingCartPackageId == Guid.Empty) - throw new ArgumentException($"Shopping cart Package ID is required", nameof(shoppingCartPackageId)); - - return new(shoppingCartId, shoppingCartPackageId); - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs deleted file mode 100644 index 26706d8..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class RemoveShoppingCartItemCommand : IRequest -{ - public Guid ShoppingCartId { get; set; } - - public Guid ShoppingCartItemId { get; set; } - - private RemoveShoppingCartItemCommand(Guid shoppingCartId, Guid shoppingCartItemId) - { - ShoppingCartId = shoppingCartId; - ShoppingCartItemId = shoppingCartItemId; - } - - public static RemoveShoppingCartItemCommand Create(Guid shoppingCartId, Guid shoppingCartItemId) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); - - if (shoppingCartItemId == Guid.Empty) - throw new ArgumentException($"Shopping cart item required", nameof(shoppingCartItemId)); - - return new(shoppingCartId, shoppingCartItemId); - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs b/LiteCharms.Features/Shop/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs deleted file mode 100644 index d7cebe0..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class UpdateShoppingCartItemCommand : IRequest -{ - public Guid ShoppingCartId { get; set; } - - public Guid ShoppingCartItemId { get; set; } - - public int Quantity { get; set; } - - private UpdateShoppingCartItemCommand(Guid shoppingCartId, Guid shoppingCartItemId, int quantity = 1) - { - ShoppingCartId = shoppingCartId; - ShoppingCartItemId = shoppingCartItemId; - Quantity = quantity; - } - - public static UpdateShoppingCartItemCommand Create(Guid shoppingCartId, Guid shoppingCartItemId, int quantity = 1) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); - - if (shoppingCartItemId == Guid.Empty) - throw new ArgumentException($"Shopping cart item is required", nameof(shoppingCartItemId)); - - if (quantity <= 0) throw new ArgumentException($"Quantity must be at least 1", nameof(quantity)); - - return new(shoppingCartId, shoppingCartItemId, quantity); - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs deleted file mode 100644 index e901ee3..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.ShoppingCarts.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries; - -public class GetCustomerShoppingCartsQuery : IRequest> -{ - public Guid CustomerId { get; set; } - - private GetCustomerShoppingCartsQuery(Guid customerId) => CustomerId = customerId; - - public static GetCustomerShoppingCartsQuery Create(Guid customerId) - { - if(customerId == Guid.Empty) - throw new ArgumentException("Customer ID is required.", nameof(customerId)); - - return new(customerId); - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs deleted file mode 100644 index e6ddec3..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.ShoppingCarts.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries; - -public class GetShoppingCartItemsQuery : IRequest> -{ - public Guid ShoppingCartId { get; set; } - - private GetShoppingCartItemsQuery(Guid shoppingCartId) => ShoppingCartId = shoppingCartId; - - public static GetShoppingCartItemsQuery Create(Guid shoppingCartId) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException("Shopping cart id is required", nameof(shoppingCartId)); - - return new(shoppingCartId); - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs deleted file mode 100644 index 611b48f..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartPackagesQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.ShoppingCarts.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries; - -public class GetShoppingCartPackagesQuery : IRequest> -{ - public Guid ShoppingCartId { get; set; } - - private GetShoppingCartPackagesQuery(Guid shoppingCartId) => ShoppingCartId = shoppingCartId; - - public static GetShoppingCartPackagesQuery Create(Guid shoppingCartId) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException("Shopping cart id is required", nameof(shoppingCartId)); - - return new(shoppingCartId); - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartQuery.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartQuery.cs deleted file mode 100644 index 1ef9784..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Queries/GetShoppingCartQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Features.Shop.ShoppingCarts.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries; - -public class GetShoppingCartQuery : IRequest> -{ - public Guid ShoppingCartId { get; set; } - - private GetShoppingCartQuery(Guid shoppingCartId) => ShoppingCartId = shoppingCartId; - - public static GetShoppingCartQuery Create(Guid shoppingCartId) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart id is required", nameof(shoppingCartId)); - - return new(shoppingCartId); - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs deleted file mode 100644 index 8554fc6..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.ShoppingCarts.Models; -using LiteCharms.Features.ShoppingCarts.Queries; - -namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; - -public class GetCustomerShoppingCartsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerShoppingCartsQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer with Id {request.CustomerId} does not exist.")); - - var shoppingCarts = await context.ShoppingCarts.Where(sc => sc.CustomerId == request.CustomerId).ToArrayAsync(cancellationToken); - - return shoppingCarts?.Length > 0 - ? Result.Ok(shoppingCarts.Select(c => c.ToModel()).ToArray()) - : Result.Fail(new Error($"No shopping carts found for customer with Id {request.CustomerId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs deleted file mode 100644 index 0082f85..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.ShoppingCarts.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; - -public class GetShoppingCartItemsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetShoppingCartItemsQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ShoppingCarts.AnyAsync(i => i.Id == request.ShoppingCartId, cancellationToken)) - return Result.Fail($"Shopping cart could not be found with id {request.ShoppingCartId}"); - - var items = await context.ShoppingCartItems.AsNoTracking() - .Where(i => i.ShoppingCartId == request.ShoppingCartId).ToArrayAsync(cancellationToken); - - return items?.Length > 0 - ? Result.Ok(items.Select(i => i.ToModel()).ToArray()) - : Result.Fail($"Failed to retrieve shopping cart items with id {request.ShoppingCartId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs deleted file mode 100644 index 68e17cf..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartPackagesQueryHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.ShoppingCarts.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; - -public class GetShoppingCartPackagesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetShoppingCartPackagesQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ShoppingCarts.AnyAsync(c => c.Id == request.ShoppingCartId, cancellationToken)) - return Result.Fail($"Shopping cart could not be found by ID {request.ShoppingCartId}"); - - var packages = await context.ShoppingCartPackages.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(cp => cp.ShoppingCartId == request.ShoppingCartId) - .ToArrayAsync(cancellationToken); - - return packages?.Length > 0 - ? Result.Ok(packages.Select(p => p.ToModel()).ToArray()) - : Result.Fail($"Could not find packaged in shopping cart by ID {request.ShoppingCartId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs b/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs deleted file mode 100644 index 2cf05a9..0000000 --- a/LiteCharms.Features/Shop/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.ShoppingCarts.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; - -public class GetShoppingCartQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetShoppingCartQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var cart = await context.ShoppingCarts.AsNoTracking().FirstOrDefaultAsync(c => c.Id == request.ShoppingCartId, cancellationToken); - - return cart is not null - ? Result.Ok(cart.ToModel()) - : Result.Fail($"Failed to find shopping cart with id {request.ShoppingCartId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Shop/ShoppingCarts/ShoppingCartService.cs b/LiteCharms.Features/Shop/ShoppingCarts/ShoppingCartService.cs new file mode 100644 index 0000000..b4fdd2f --- /dev/null +++ b/LiteCharms.Features/Shop/ShoppingCarts/ShoppingCartService.cs @@ -0,0 +1,298 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.ShoppingCarts.Models; +using static LiteCharms.Features.Extensions.Timezones; + +namespace LiteCharms.Features.Shop.ShoppingCarts; + +public class ShoppingCartService(IDbContextFactory contextFactory) +{ + public async ValueTask AddItemToShoppingCartAsync(Guid shoppingCartId, Guid productPriceId, int quantity, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ProductPrices.AnyAsync(c => c.Id == productPriceId, cancellationToken)) + return Result.Fail($"Product item could not be found with id {productPriceId}"); + + var cart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == shoppingCartId, cancellationToken); + + if (cart is null) + return Result.Fail($"Shopping cart could not be found with id {shoppingCartId}"); + + if (cart.ShoppingCartItems?.Any(i => i.ProductPriceId == productPriceId) == true) + return Result.Fail($"Item already in shopping cart with id {shoppingCartId}"); + + context.ShoppingCartItems.Add(new Entities.ShoppingCartItem + { + ShoppingCartId = shoppingCartId, + ProductPriceId = productPriceId, + Quantity = quantity + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to add cart item with id {productPriceId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask AddPackageToShoppingCartAsync(Guid shoppingCartId, Guid packageId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == packageId, cancellationToken)) + return Result.Fail($"Package cold not be found by ID {packageId}"); + + var shoppingCart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == shoppingCartId, cancellationToken); + + if (shoppingCart is null) + return Result.Fail($"Shopping cart could not be found by ID {shoppingCartId}"); + + if (!await context.ShoppingCartPackages.AnyAsync(cp => cp.ShoppingCartId == shoppingCartId, cancellationToken)) + return Result.Fail($"Package {packageId} is already in the cart"); + + var newShoppingCartPackage = context.ShoppingCartPackages.Add(new Entities.ShoppingCartPackage + { + ShoppingCartId = shoppingCartId, + PackageId = packageId + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Could not add package of id {packageId} to shopping cart {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateShoppingCartAsync(Guid customerId, Guid orderId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail($"Customer could not be found with id {customerId}"); + + var cart = context.ShoppingCarts.Add(new Entities.ShoppingCart + { + CustomerId = customerId, + OrderId = orderId + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(cart.Entity.Id) + : Result.Fail($"Failed to create shopping cart for customer id {customerId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask EmptyShoppingCartAsync(Guid shoppingCartId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == shoppingCartId, cancellationToken)) + return Result.Fail($"Shopping could not be found with id {shoppingCartId}"); + + if (await context.ShoppingCartItems.CountAsync(i => i.ShoppingCartId == shoppingCartId, cancellationToken) == 0) + return Result.Ok(); + + var cartItems = await context.ShoppingCartItems.Where(i => i.ShoppingCartId == shoppingCartId).ToListAsync(cancellationToken); + + context.RemoveRange(cartItems); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Could not empty cart with id {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerShoppingCartsAsync(Guid customerId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with Id {customerId} does not exist.")); + + var shoppingCarts = await context.ShoppingCarts.Where(sc => sc.CustomerId == customerId).ToArrayAsync(cancellationToken); + + return shoppingCarts?.Length > 0 + ? Result.Ok(shoppingCarts.Select(c => c.ToModel()).ToArray()) + : Result.Fail(new Error($"No shopping carts found for customer with Id {customerId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShoppingCartAsync(Guid shoppingCartId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var cart = await context.ShoppingCarts.AsNoTracking().FirstOrDefaultAsync(c => c.Id == shoppingCartId, cancellationToken); + + return cart is not null + ? Result.Ok(cart.ToModel()) + : Result.Fail($"Failed to find shopping cart with id {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShoppingCartItemsAsync(Guid shoppingCartId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(i => i.Id == shoppingCartId, cancellationToken)) + return Result.Fail($"Shopping cart could not be found with id {shoppingCartId}"); + + var items = await context.ShoppingCartItems.AsNoTracking() + .Where(i => i.ShoppingCartId == shoppingCartId).ToArrayAsync(cancellationToken); + + return items?.Length > 0 + ? Result.Ok(items.Select(i => i.ToModel()).ToArray()) + : Result.Fail($"Failed to retrieve shopping cart items with id {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShoppingCartPackagesAsync(Guid shoppingCartId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == shoppingCartId, cancellationToken)) + return Result.Fail($"Shopping cart could not be found by ID {shoppingCartId}"); + + var packages = await context.ShoppingCartPackages.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(cp => cp.ShoppingCartId == shoppingCartId) + .ToArrayAsync(cancellationToken); + + return packages?.Length > 0 + ? Result.Ok(packages.Select(p => p.ToModel()).ToArray()) + : Result.Fail($"Could not find packaged in shopping cart by ID {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask RemovePackageFromShoppingCartAsync(Guid shoppingCartId, Guid shoppingCartPackageId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == shoppingCartId, cancellationToken)) + return Result.Fail($"Shopping cart could not be found by ID {shoppingCartId}"); + + if (!await context.ShoppingCartPackages.AnyAsync(p => p.Id == shoppingCartPackageId, cancellationToken)) + return Result.Fail($"Shopping cart package {shoppingCartPackageId} is not in the shopping cart {shoppingCartId}"); + + var shoppingCartPackage = await context.ShoppingCartPackages.FirstOrDefaultAsync(cp => cp.Id == shoppingCartPackageId, cancellationToken); + + if (shoppingCartPackage is null) + return Result.Ok(); + + context.ShoppingCartPackages.Remove(shoppingCartPackage!); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Could remove package of id {shoppingCartPackageId} from shopping cart {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask RemoveShoppingCartItemAsync(Guid shoppingCartId, Guid shoppingCartItemId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ProductPrices.AnyAsync(c => c.Id == shoppingCartItemId, cancellationToken)) + return Result.Fail($"Product item could not be found with id {shoppingCartItemId}"); + + var cart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == shoppingCartId, cancellationToken); + + if (cart is null) + return Result.Fail($"Shopping cart item could not be found with id {shoppingCartId}"); + + var item = await context.ShoppingCartItems.FirstOrDefaultAsync(i => i.Id == shoppingCartItemId, cancellationToken); + + if (item is null) return Result.Ok(); + + context.ShoppingCartItems.Remove(item); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to remove shopping cart item with id {shoppingCartItemId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateShoppingCartItemAsync(Guid shoppingCartId, Guid shoppingCartItemId, int quantity, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == shoppingCartId, cancellationToken)) + return Result.Fail($"Shopping could not be found with id {shoppingCartId}"); + + var item = await context.ShoppingCartItems.FirstOrDefaultAsync(i => i.ShoppingCartId == shoppingCartId, cancellationToken); + + if (item is null) + return Result.Fail($"Shopping cart item could not be found with id {shoppingCartItemId}"); + + item.Quantity = quantity; + item.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update cart item quntity"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} From 2610275bef3cab138f32e7cd37b1e6baea71aa98 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 14 May 2026 02:46:07 +0200 Subject: [PATCH 025/153] Stable run on Notification creation --- LiteCharms.Features/Extensions/Timezones.cs | 2 +- .../LiteCharms.Features.csproj | 9 + .../Entities/PackageConfirguration.cs | 2 +- .../Shop/CartPackages/Models/Package.cs | 4 +- .../Entities/CustomerConfiguration.cs | 4 +- .../Shop/Customers/Models/Customer.cs | 4 +- .../Shop/Leads/Entities/LeadConfiguration.cs | 11 +- LiteCharms.Features/Shop/Leads/Models/Lead.cs | 4 +- .../Entities/NotificationConfiguration.cs | 4 +- .../Shop/Notifications/Models/Notification.cs | 4 +- .../Shop/Notifications/NotificationService.cs | 2 +- .../Orders/Entities/OrderConfiguration.cs | 4 +- .../Shop/Orders/Entities/OrderRefund.cs | 1 + .../Entities/OrderRefundConfiguration.cs | 2 +- .../Shop/Orders/Models/Order.cs | 4 +- .../Shop/Orders/Models/OrderRefund.cs | 2 +- ...514004002_UsedStringTableNames.Designer.cs | 871 ++++++++++++++++ .../20260514004002_UsedStringTableNames.cs | 936 ++++++++++++++++++ .../Migrations/ShopDbContextModelSnapshot.cs | 458 +++++---- .../Shop/Postgres/ShopDbContext.cs | 30 + .../Shop/Products/Entities/Product.cs | 1 + .../Products/Entities/ProductConfiguration.cs | 2 +- .../Entities/ProductPriceConfiguration.cs | 2 +- .../Shop/Products/Models/ProductPrice.cs | 4 +- .../Quotes/Entities/QuoteConfiguration.cs | 6 +- .../Shop/Quotes/Models/Quote.cs | 6 +- .../Entities/ShoppingCartConfiguration.cs | 4 +- .../Entities/ShoppingCartItem.cs | 1 + .../Entities/ShoppingCartItemConfiguration.cs | 4 +- .../ShoppingCartPackageConfiguration.cs | 2 +- .../Shop/ShoppingCarts/Models/ShoppingCart.cs | 4 +- .../ShoppingCarts/Models/ShoppingCartItem.cs | 4 +- .../Models/ShoppingCartPackage.cs | 2 +- LiteCharms.Features/appsettings.json | 22 + 34 files changed, 2199 insertions(+), 223 deletions(-) create mode 100644 LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs create mode 100644 LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs create mode 100644 LiteCharms.Features/appsettings.json diff --git a/LiteCharms.Features/Extensions/Timezones.cs b/LiteCharms.Features/Extensions/Timezones.cs index bbddd5d..3976240 100644 --- a/LiteCharms.Features/Extensions/Timezones.cs +++ b/LiteCharms.Features/Extensions/Timezones.cs @@ -23,5 +23,5 @@ public static class Timezones return DateTimeOffset.Parse(localised!); } - public static DateTimeOffset UtcNow(this TimeZoneInfo timezone) => ToDateTimeWithTimeZone(DateTime.Now, timezone); + public static DateTime UtcNow(this TimeZoneInfo timezone) => ToDateTimeWithTimeZone(DateTime.Now, timezone).UtcDateTime; } diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 9ad7508..a9aedad 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -21,6 +21,7 @@ LICENSE utility;dotnet icon.png + 8a78916e-c86b-4f4b-9f4e-d8e7769b5d23 @@ -143,5 +144,13 @@ + + + PreserveNewest + + + + + diff --git a/LiteCharms.Features/Shop/CartPackages/Entities/PackageConfirguration.cs b/LiteCharms.Features/Shop/CartPackages/Entities/PackageConfirguration.cs index e6ff029..cfc89ff 100644 --- a/LiteCharms.Features/Shop/CartPackages/Entities/PackageConfirguration.cs +++ b/LiteCharms.Features/Shop/CartPackages/Entities/PackageConfirguration.cs @@ -8,7 +8,7 @@ public class PackageConfirguration : IEntityTypeConfiguration builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); - builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); builder.Property(f => f.Name).IsRequired(); builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); builder.Property(f => f.Description).IsRequired().HasMaxLength(2048); diff --git a/LiteCharms.Features/Shop/CartPackages/Models/Package.cs b/LiteCharms.Features/Shop/CartPackages/Models/Package.cs index fcc560b..9d2e8b0 100644 --- a/LiteCharms.Features/Shop/CartPackages/Models/Package.cs +++ b/LiteCharms.Features/Shop/CartPackages/Models/Package.cs @@ -4,9 +4,9 @@ public class Package { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public string? Name { get; set; } diff --git a/LiteCharms.Features/Shop/Customers/Entities/CustomerConfiguration.cs b/LiteCharms.Features/Shop/Customers/Entities/CustomerConfiguration.cs index 335c1b0..f5ce014 100644 --- a/LiteCharms.Features/Shop/Customers/Entities/CustomerConfiguration.cs +++ b/LiteCharms.Features/Shop/Customers/Entities/CustomerConfiguration.cs @@ -4,11 +4,11 @@ public class CustomerConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Customer)); + builder.ToTable("Customers"); builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); - builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); builder.Property(f => f.Company); builder.Property(f => f.Name).IsRequired(); builder.Property(f => f.LastName).IsRequired(); diff --git a/LiteCharms.Features/Shop/Customers/Models/Customer.cs b/LiteCharms.Features/Shop/Customers/Models/Customer.cs index 14387f6..d4cec8c 100644 --- a/LiteCharms.Features/Shop/Customers/Models/Customer.cs +++ b/LiteCharms.Features/Shop/Customers/Models/Customer.cs @@ -4,9 +4,9 @@ public class Customer { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public string? Company { get; set; } diff --git a/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs b/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs index 482677c..39a0a20 100644 --- a/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs +++ b/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs @@ -4,12 +4,12 @@ public class LeadConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Lead)); + builder.ToTable("Leads"); builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); - builder.Property(f => f.UpdatedAt).IsRequired(false); - builder.Property(f => f.CustomerId).IsRequired(false); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); + builder.Property(f => f.CustomerId); builder.Property(f => f.Source); builder.Property(f => f.ClickId); builder.Property(f => f.WebClickId); @@ -22,5 +22,10 @@ public class LeadConfiguration : IEntityTypeConfiguration builder.Property(f => f.ClickLocation); builder.Property(f => f.Status).IsRequired(); builder.Property(f => f.AttributionHash).IsRequired(true); + + builder.HasOne(f => f.Customer) + .WithMany() + .HasForeignKey(f => f.CustomerId) + .OnDelete(DeleteBehavior.Restrict); } } diff --git a/LiteCharms.Features/Shop/Leads/Models/Lead.cs b/LiteCharms.Features/Shop/Leads/Models/Lead.cs index 0cb9fa1..adfb1ba 100644 --- a/LiteCharms.Features/Shop/Leads/Models/Lead.cs +++ b/LiteCharms.Features/Shop/Leads/Models/Lead.cs @@ -4,9 +4,9 @@ public class Lead { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public Guid? CustomerId { get; set; } diff --git a/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs b/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs index 8a00b68..7139d0d 100644 --- a/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs +++ b/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs @@ -4,11 +4,11 @@ public class NotificationConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Notification)); + builder.ToTable("Notification"); builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); - builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); builder.Property(f => f.Direction).IsRequired().HasConversion(); builder.Property(f => f.Platform).IsRequired().HasConversion(); builder.Property(f => f.Priority).IsRequired().HasConversion(); diff --git a/LiteCharms.Features/Shop/Notifications/Models/Notification.cs b/LiteCharms.Features/Shop/Notifications/Models/Notification.cs index bb9b77c..fd085a1 100644 --- a/LiteCharms.Features/Shop/Notifications/Models/Notification.cs +++ b/LiteCharms.Features/Shop/Notifications/Models/Notification.cs @@ -4,9 +4,9 @@ public class Notification { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public NotificationDirection Direction { get; set; } diff --git a/LiteCharms.Features/Shop/Notifications/NotificationService.cs b/LiteCharms.Features/Shop/Notifications/NotificationService.cs index 84e7565..59493e4 100644 --- a/LiteCharms.Features/Shop/Notifications/NotificationService.cs +++ b/LiteCharms.Features/Shop/Notifications/NotificationService.cs @@ -31,7 +31,7 @@ public class NotificationService(IDbContextFactory contextFactory Processed = false }); - return newNotification is not null + return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(newNotification.Entity.Id) : Result.Fail(new Error("Failed to create notification")); } diff --git a/LiteCharms.Features/Shop/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features/Shop/Orders/Entities/OrderConfiguration.cs index d79fb0e..848aaed 100644 --- a/LiteCharms.Features/Shop/Orders/Entities/OrderConfiguration.cs +++ b/LiteCharms.Features/Shop/Orders/Entities/OrderConfiguration.cs @@ -4,11 +4,11 @@ public class OrderConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Order)); + builder.ToTable("Orders"); builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); - builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); builder.Property(f => f.CustomerId).IsRequired(); builder.Property(f => f.Status).HasConversion().IsRequired(); builder.Property(f => f.Requirements).HasColumnType("jsonb").IsRequired(false); diff --git a/LiteCharms.Features/Shop/Orders/Entities/OrderRefund.cs b/LiteCharms.Features/Shop/Orders/Entities/OrderRefund.cs index 33591d7..8f7e462 100644 --- a/LiteCharms.Features/Shop/Orders/Entities/OrderRefund.cs +++ b/LiteCharms.Features/Shop/Orders/Entities/OrderRefund.cs @@ -3,5 +3,6 @@ [EntityTypeConfiguration] public class OrderRefund : Models.OrderRefund { + public virtual Order? Order { get; set; } } diff --git a/LiteCharms.Features/Shop/Orders/Entities/OrderRefundConfiguration.cs b/LiteCharms.Features/Shop/Orders/Entities/OrderRefundConfiguration.cs index f353123..b33c2ee 100644 --- a/LiteCharms.Features/Shop/Orders/Entities/OrderRefundConfiguration.cs +++ b/LiteCharms.Features/Shop/Orders/Entities/OrderRefundConfiguration.cs @@ -4,7 +4,7 @@ public class OrderRefundConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(OrderRefund)); + builder.ToTable("OrderRefunds"); builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); diff --git a/LiteCharms.Features/Shop/Orders/Models/Order.cs b/LiteCharms.Features/Shop/Orders/Models/Order.cs index d093993..30bb75d 100644 --- a/LiteCharms.Features/Shop/Orders/Models/Order.cs +++ b/LiteCharms.Features/Shop/Orders/Models/Order.cs @@ -4,9 +4,9 @@ public class Order { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public Guid CustomerId { get; set; } diff --git a/LiteCharms.Features/Shop/Orders/Models/OrderRefund.cs b/LiteCharms.Features/Shop/Orders/Models/OrderRefund.cs index 34668a7..1463afd 100644 --- a/LiteCharms.Features/Shop/Orders/Models/OrderRefund.cs +++ b/LiteCharms.Features/Shop/Orders/Models/OrderRefund.cs @@ -4,7 +4,7 @@ public class OrderRefund { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } public Guid OrderId { get; set; } diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs b/LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs new file mode 100644 index 0000000..b0185a2 --- /dev/null +++ b/LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs @@ -0,0 +1,871 @@ +// +using System; +using LiteCharms.Features.Shop.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.Shop.Postgres.Migrations +{ + [DbContext(typeof(ShopDbContext))] + [Migration("20260514004002_UsedStringTableNames")] + partial class UsedStringTableNames + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("PackageItem", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discord") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LinkedIn") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Region") + .HasColumnType("text"); + + b.Property("Slack") + .HasColumnType("text"); + + b.Property("Tax") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.Property("Whatsapp") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discord") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("LinkedIn") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Region") + .HasColumnType("text"); + + b.Property("Slack") + .HasColumnType("text"); + + b.Property("Tax") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.Property("Whatsapp") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdGroupId") + .HasColumnType("bigint"); + + b.Property("AdName") + .HasColumnType("bigint"); + + b.Property("AppClickId") + .HasColumnType("text"); + + b.Property("AttributionHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("CampaignId") + .HasColumnType("bigint"); + + b.Property("ClickId") + .HasColumnType("text"); + + b.Property("ClickLocation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("CustomerId1") + .HasColumnType("uuid"); + + b.Property("FeedItemId") + .HasColumnType("bigint"); + + b.Property("Source") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TargetId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebClickId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("CustomerId1"); + + b.ToTable("Leads", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationIdType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsInternal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Processed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); + + b.Property("Status") + .HasColumnType("integer"); + + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderRefunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("Thumbnails") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductPrices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShoppingCartId") + .IsUnique(); + + b.ToTable("Quotes", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("ShoppingCarts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId1") + .HasColumnType("uuid"); + + b.Property("Quantity") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId1") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductPriceId"); + + b.HasIndex("ProductPriceId1"); + + b.HasIndex("ShoppingCartId"); + + b.HasIndex("ShoppingCartId1"); + + b.ToTable("ShoppingCartItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartPackages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany("PackageItems") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Models.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", null) + .WithMany("Leads") + .HasForeignKey("CustomerId1"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product") + .WithMany("ProductPrices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Quotes") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId"); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId"); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("ShoppingCarts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("ShoppingCart") + .HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Customer"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", null) + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId1"); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", null) + .WithMany("ShoppingCartItems") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany() + .HasForeignKey("ShoppingCartId1"); + + b.Navigation("ProductPrice"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartPackages") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Navigation("PackageItems"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Navigation("Leads"); + + b.Navigation("Orders"); + + b.Navigation("Quotes"); + + b.Navigation("ShoppingCarts"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Navigation("Quote"); + + b.Navigation("Refunds"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Navigation("ProductPrices"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Navigation("Quote"); + + b.Navigation("ShoppingCartItems"); + + b.Navigation("ShoppingCartPackages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs b/LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs new file mode 100644 index 0000000..234f04a --- /dev/null +++ b/LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs @@ -0,0 +1,936 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.Shop.Postgres.Migrations +{ + /// + public partial class UsedStringTableNames : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_PackageItem_ProductPrice_ProductPriceId", + table: "PackageItem"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ProductPrice_ProductPriceId", + table: "ShoppingCartItems"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ShoppingCart_ShoppingCartId", + table: "ShoppingCartItems"); + + migrationBuilder.DropTable( + name: "Lead"); + + migrationBuilder.DropTable( + name: "OrderRefund"); + + migrationBuilder.DropTable( + name: "ProductPrice"); + + migrationBuilder.DropTable( + name: "Quote"); + + migrationBuilder.DropTable( + name: "ShoppingCartPackage"); + + migrationBuilder.DropTable( + name: "Product"); + + migrationBuilder.DropTable( + name: "ShoppingCart"); + + migrationBuilder.DropTable( + name: "Order"); + + migrationBuilder.RenameColumn( + name: "Sender", + table: "Notification", + newName: "SenderAddress"); + + migrationBuilder.RenameColumn( + name: "Recipient", + table: "Notification", + newName: "RecipientName"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ShoppingCartItems", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "ShoppingCartItems", + type: "integer", + nullable: false, + defaultValue: 1, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ShoppingCartItems", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone"); + + migrationBuilder.AddColumn( + name: "ProductPriceId1", + table: "ShoppingCartItems", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "ShoppingCartId1", + table: "ShoppingCartItems", + type: "uuid", + nullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customer", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "Customer", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Email", + table: "Customer", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Customer", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "Active", + table: "Customer", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: true); + + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Company = table.Column(type: "text", nullable: true), + Name = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + Tax = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: false), + Discord = table.Column(type: "text", nullable: true), + Slack = table.Column(type: "text", nullable: true), + LinkedIn = table.Column(type: "text", nullable: true), + Whatsapp = table.Column(type: "text", nullable: true), + Website = table.Column(type: "text", nullable: true), + Phone = table.Column(type: "text", nullable: true), + Address = table.Column(type: "text", nullable: true), + City = table.Column(type: "text", nullable: true), + Region = table.Column(type: "text", nullable: true), + Country = table.Column(type: "text", nullable: true), + PostalCode = table.Column(type: "text", nullable: true), + Active = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Description = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Thumbnails = table.Column(type: "jsonb", nullable: true), + Active = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Leads", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId1 = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: true), + Source = table.Column(type: "text", nullable: true), + ClickId = table.Column(type: "text", nullable: true), + WebClickId = table.Column(type: "text", nullable: true), + AppClickId = table.Column(type: "text", nullable: true), + CampaignId = table.Column(type: "bigint", nullable: true), + AdGroupId = table.Column(type: "bigint", nullable: true), + AdName = table.Column(type: "bigint", nullable: true), + TargetId = table.Column(type: "bigint", nullable: true), + FeedItemId = table.Column(type: "bigint", nullable: true), + ClickLocation = table.Column(type: "text", nullable: true), + AttributionHash = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Leads", x => x.Id); + table.ForeignKey( + name: "FK_Leads_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Leads_Customers_CustomerId1", + column: x => x.CustomerId1, + principalTable: "Customers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Requirements = table.Column(type: "jsonb", nullable: true), + Notes = table.Column(type: "jsonb", nullable: true), + Terms = table.Column(type: "jsonb", nullable: true), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + table.ForeignKey( + name: "FK_Orders_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ProductPrices", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + ProductId = table.Column(type: "uuid", nullable: false), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Discount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductPrices", x => x.Id); + table.ForeignKey( + name: "FK_ProductPrices_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "OrderRefunds", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + OrderId = table.Column(type: "uuid", nullable: false), + Reason = table.Column(type: "text", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderRefunds", x => x.Id); + table.ForeignKey( + name: "FK_OrderRefunds_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCarts", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCarts", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCarts_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingCarts_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "Quotes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true), + ShoppingCartId = table.Column(type: "uuid", nullable: true), + Status = table.Column(type: "integer", nullable: false), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Reason = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Quotes", x => x.Id); + table.ForeignKey( + name: "FK_Quotes_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Quotes_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Quotes_ShoppingCarts_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCarts", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCartPackages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + PackageId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCartPackages", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCartPackages_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ShoppingCartPackages_ShoppingCarts_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCarts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ProductPriceId1", + table: "ShoppingCartItems", + column: "ProductPriceId1"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ShoppingCartId1", + table: "ShoppingCartItems", + column: "ShoppingCartId1"); + + migrationBuilder.CreateIndex( + name: "IX_Leads_CustomerId", + table: "Leads", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Leads_CustomerId1", + table: "Leads", + column: "CustomerId1"); + + migrationBuilder.CreateIndex( + name: "IX_OrderRefunds_OrderId", + table: "OrderRefunds", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_CustomerId", + table: "Orders", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductPrices_ProductId", + table: "ProductPrices", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Quotes_CustomerId", + table: "Quotes", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Quotes_OrderId", + table: "Quotes", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Quotes_ShoppingCartId", + table: "Quotes", + column: "ShoppingCartId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackages_PackageId", + table: "ShoppingCartPackages", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackages_ShoppingCartId", + table: "ShoppingCartPackages", + column: "ShoppingCartId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCarts_CustomerId", + table: "ShoppingCarts", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCarts_OrderId", + table: "ShoppingCarts", + column: "OrderId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_PackageItem_ProductPrices_ProductPriceId", + table: "PackageItem", + column: "ProductPriceId", + principalTable: "ProductPrices", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId", + table: "ShoppingCartItems", + column: "ProductPriceId", + principalTable: "ProductPrices", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1", + table: "ShoppingCartItems", + column: "ProductPriceId1", + principalTable: "ProductPrices", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId", + table: "ShoppingCartItems", + column: "ShoppingCartId", + principalTable: "ShoppingCarts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1", + table: "ShoppingCartItems", + column: "ShoppingCartId1", + principalTable: "ShoppingCarts", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_PackageItem_ProductPrices_ProductPriceId", + table: "PackageItem"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId", + table: "ShoppingCartItems"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId", + table: "ShoppingCartItems"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropTable( + name: "Leads"); + + migrationBuilder.DropTable( + name: "OrderRefunds"); + + migrationBuilder.DropTable( + name: "ProductPrices"); + + migrationBuilder.DropTable( + name: "Quotes"); + + migrationBuilder.DropTable( + name: "ShoppingCartPackages"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "ShoppingCarts"); + + migrationBuilder.DropTable( + name: "Orders"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropIndex( + name: "IX_ShoppingCartItems_ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropIndex( + name: "IX_ShoppingCartItems_ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropColumn( + name: "ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropColumn( + name: "ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.RenameColumn( + name: "SenderAddress", + table: "Notification", + newName: "Sender"); + + migrationBuilder.RenameColumn( + name: "RecipientName", + table: "Notification", + newName: "Recipient"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ShoppingCartItems", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "ShoppingCartItems", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldDefaultValue: 1); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ShoppingCartItems", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customer", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "Customer", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Email", + table: "Customer", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Customer", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Active", + table: "Customer", + type: "boolean", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.CreateTable( + name: "Lead", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: true), + AdGroupId = table.Column(type: "bigint", nullable: true), + AdName = table.Column(type: "bigint", nullable: true), + AppClickId = table.Column(type: "text", nullable: true), + AttributionHash = table.Column(type: "text", nullable: false), + CampaignId = table.Column(type: "bigint", nullable: true), + ClickId = table.Column(type: "text", nullable: true), + ClickLocation = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + FeedItemId = table.Column(type: "bigint", nullable: true), + Source = table.Column(type: "text", nullable: true), + Status = table.Column(type: "integer", nullable: false), + TargetId = table.Column(type: "bigint", nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + WebClickId = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Lead", x => x.Id); + table.ForeignKey( + name: "FK_Lead_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Order", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Notes = table.Column(type: "jsonb", nullable: true), + Requirements = table.Column(type: "jsonb", nullable: true), + Status = table.Column(type: "integer", nullable: false), + Terms = table.Column(type: "jsonb", nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Order", x => x.Id); + table.ForeignKey( + name: "FK_Order_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Product", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Active = table.Column(type: "boolean", nullable: false, defaultValue: true), + Description = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Name = table.Column(type: "text", nullable: false), + Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Thumbnails = table.Column(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Product", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OrderRefund", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Reason = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderRefund", x => x.Id); + table.ForeignKey( + name: "FK_OrderRefund_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCart", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCart", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCart_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingCart_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ProductPrice", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + Active = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Discount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductPrice", x => x.Id); + table.ForeignKey( + name: "FK_ProductPrice_Product_ProductId", + column: x => x.ProductId, + principalTable: "Product", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Quote", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true), + ShoppingCartId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Reason = table.Column(type: "text", nullable: true), + Status = table.Column(type: "integer", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Quote", x => x.Id); + table.ForeignKey( + name: "FK_Quote_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Quote_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Quote_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCartPackage", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PackageId = table.Column(type: "uuid", nullable: false), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCartPackage", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCartPackage_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ShoppingCartPackage_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Lead_CustomerId", + table: "Lead", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Order_CustomerId", + table: "Order", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderRefund_OrderId", + table: "OrderRefund", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductPrice_ProductId", + table: "ProductPrice", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Quote_CustomerId", + table: "Quote", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Quote_OrderId", + table: "Quote", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Quote_ShoppingCartId", + table: "Quote", + column: "ShoppingCartId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCart_CustomerId", + table: "ShoppingCart", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCart_OrderId", + table: "ShoppingCart", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_PackageId", + table: "ShoppingCartPackage", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_ShoppingCartId", + table: "ShoppingCartPackage", + column: "ShoppingCartId"); + + migrationBuilder.AddForeignKey( + name: "FK_PackageItem_ProductPrice_ProductPriceId", + table: "PackageItem", + column: "ProductPriceId", + principalTable: "ProductPrice", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ProductPrice_ProductPriceId", + table: "ShoppingCartItems", + column: "ProductPriceId", + principalTable: "ProductPrice", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ShoppingCart_ShoppingCartId", + table: "ShoppingCartItems", + column: "ShoppingCartId", + principalTable: "ShoppingCart", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs b/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs index 26d9a29..26d0c26 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs +++ b/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs @@ -17,12 +17,81 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("ProductVersion", "10.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("LiteCharms.Entities.Customer", b => + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("PackageItem", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -45,7 +114,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("Country") .HasColumnType("text"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("now()"); @@ -83,7 +152,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("Tax") .HasColumnType("text"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.Property("Website") @@ -94,10 +163,78 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.HasKey("Id"); - b.ToTable("Customer", (string)null); + b.ToTable("Customers", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Lead", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discord") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("LinkedIn") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Region") + .HasColumnType("text"); + + b.Property("Slack") + .HasColumnType("text"); + + b.Property("Tax") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.Property("Whatsapp") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -125,7 +262,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("ClickLocation") .HasColumnType("text"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("now()"); @@ -133,6 +270,9 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("CustomerId") .HasColumnType("uuid"); + b.Property("CustomerId1") + .HasColumnType("uuid"); + b.Property("FeedItemId") .HasColumnType("bigint"); @@ -145,7 +285,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("TargetId") .HasColumnType("bigint"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.Property("WebClickId") @@ -155,10 +295,12 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.HasIndex("CustomerId"); - b.ToTable("Lead", (string)null); + b.HasIndex("CustomerId1"); + + b.ToTable("Leads", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Notification", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -171,7 +313,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("CorrelationIdType") .HasColumnType("integer"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("now()"); @@ -212,15 +354,15 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations .HasColumnType("boolean") .HasDefaultValue(false); - b.Property("Recipient") - .IsRequired() - .HasColumnType("text"); - b.Property("RecipientAddress") .IsRequired() .HasColumnType("text"); - b.Property("Sender") + b.Property("RecipientName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderAddress") .IsRequired() .HasColumnType("text"); @@ -231,7 +373,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations .IsRequired() .HasColumnType("text"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -239,13 +381,13 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.ToTable("Notification", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Order", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("now()"); @@ -269,17 +411,17 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.PrimitiveCollection("Terms") .HasColumnType("jsonb"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); - b.ToTable("Order", (string)null); + b.ToTable("Orders", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -289,7 +431,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations .HasPrecision(18, 2) .HasColumnType("numeric(18,2)"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("now()"); @@ -305,79 +447,10 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.HasIndex("OrderId"); - b.ToTable("OrderRefund", (string)null); + b.ToTable("OrderRefunds", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Package", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasDefaultValueSql("now()"); - - b.Property("Description") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("ImageUrl") - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Summary") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Package", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasDefaultValueSql("now()"); - - b.Property("PackageId") - .HasColumnType("uuid"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("PackageId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("PackageItem", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -411,10 +484,10 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.HasKey("Id"); - b.ToTable("Product", (string)null); + b.ToTable("Products", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -423,7 +496,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("Active") .HasColumnType("boolean"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("now()"); @@ -439,23 +512,23 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("ProductId") .HasColumnType("uuid"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("ProductId"); - b.ToTable("ProductPrice", (string)null); + b.ToTable("ProductPrices", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Quote", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("now()"); @@ -463,7 +536,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("CustomerId") .HasColumnType("uuid"); - b.Property("ExpiredAt") + b.Property("ExpiredAt") .HasColumnType("timestamp with time zone"); b.Property("InvoiceUrl") @@ -482,7 +555,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("Status") .HasColumnType("integer"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -495,16 +568,16 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.HasIndex("ShoppingCartId") .IsUnique(); - b.ToTable("Quote", (string)null); + b.ToTable("Quotes", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("now()"); @@ -515,7 +588,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("OrderId") .HasColumnType("uuid"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -525,46 +598,60 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.HasIndex("OrderId") .IsUnique(); - b.ToTable("ShoppingCart", (string)null); + b.ToTable("ShoppingCarts", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("ProductPriceId") .HasColumnType("uuid"); + b.Property("ProductPriceId1") + .HasColumnType("uuid"); + b.Property("Quantity") - .HasColumnType("integer"); + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); b.Property("ShoppingCartId") .HasColumnType("uuid"); - b.Property("UpdatedAt") + b.Property("ShoppingCartId1") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("ProductPriceId"); + b.HasIndex("ProductPriceId1"); + b.HasIndex("ShoppingCartId"); - b.ToTable("ShoppingCartItems"); + b.HasIndex("ShoppingCartId1"); + + b.ToTable("ShoppingCartItems", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCartPackage", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") .HasDefaultValueSql("now()"); @@ -581,49 +668,18 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.HasIndex("ShoppingCartId"); - b.ToTable("ShoppingCartPackage", (string)null); + b.ToTable("ShoppingCartPackages", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Lead", b => + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId"); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.HasOne("LiteCharms.Entities.Order", "Order") - .WithMany("Refunds") - .HasForeignKey("OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => - { - b.HasOne("LiteCharms.Entities.Package", "Package") + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") .WithMany("PackageItems") .HasForeignKey("PackageId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") .WithMany() .HasForeignKey("ProductPriceId") .OnDelete(DeleteBehavior.Restrict) @@ -634,9 +690,45 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Navigation("ProductPrice"); }); - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => { - b.HasOne("LiteCharms.Entities.Product", "Product") + b.HasOne("LiteCharms.Features.Shop.Customers.Models.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", null) + .WithMany("Leads") + .HasForeignKey("CustomerId1"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product") .WithMany("ProductPrices") .HasForeignKey("ProductId") .OnDelete(DeleteBehavior.Restrict) @@ -645,21 +737,21 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Navigation("Product"); }); - modelBuilder.Entity("LiteCharms.Entities.Quote", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => { - b.HasOne("LiteCharms.Entities.Customer", "Customer") + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") .WithMany("Quotes") .HasForeignKey("CustomerId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Entities.Order", "Order") + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") .WithOne("Quote") - .HasForeignKey("LiteCharms.Entities.Quote", "OrderId"); + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId"); - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") .WithOne("Quote") - .HasForeignKey("LiteCharms.Entities.Quote", "ShoppingCartId"); + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId"); b.Navigation("Customer"); @@ -668,17 +760,17 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Navigation("ShoppingCart"); }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => { - b.HasOne("LiteCharms.Entities.Customer", "Customer") + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") .WithMany("ShoppingCarts") .HasForeignKey("CustomerId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Entities.Order", "Order") + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") .WithOne("ShoppingCart") - .HasForeignKey("LiteCharms.Entities.ShoppingCart", "OrderId") + .HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId") .OnDelete(DeleteBehavior.SetNull); b.Navigation("Customer"); @@ -686,34 +778,42 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Navigation("Order"); }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => { - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", null) .WithMany() .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId1"); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", null) .WithMany("ShoppingCartItems") .HasForeignKey("ShoppingCartId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany() + .HasForeignKey("ShoppingCartId1"); + b.Navigation("ProductPrice"); b.Navigation("ShoppingCart"); }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCartPackage", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => { - b.HasOne("LiteCharms.Entities.Package", "Package") + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") .WithMany() .HasForeignKey("PackageId") .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") .WithMany("ShoppingCartPackages") .HasForeignKey("ShoppingCartId") .OnDelete(DeleteBehavior.Cascade) @@ -724,7 +824,12 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Navigation("ShoppingCart"); }); - modelBuilder.Entity("LiteCharms.Entities.Customer", b => + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Navigation("PackageItems"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => { b.Navigation("Leads"); @@ -735,7 +840,7 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Navigation("ShoppingCarts"); }); - modelBuilder.Entity("LiteCharms.Entities.Order", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => { b.Navigation("Quote"); @@ -744,17 +849,12 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Navigation("ShoppingCart"); }); - modelBuilder.Entity("LiteCharms.Entities.Package", b => - { - b.Navigation("PackageItems"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => { b.Navigation("ProductPrices"); }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => { b.Navigation("Quote"); diff --git a/LiteCharms.Features/Shop/Postgres/ShopDbContext.cs b/LiteCharms.Features/Shop/Postgres/ShopDbContext.cs index b02f630..b8d7cc9 100644 --- a/LiteCharms.Features/Shop/Postgres/ShopDbContext.cs +++ b/LiteCharms.Features/Shop/Postgres/ShopDbContext.cs @@ -36,4 +36,34 @@ public class ShopDbContext(DbContextOptions options) : DbContext( public DbSet PackageItems { get; set; } public DbSet ShoppingCartPackages { get; set; } + + //protected override void OnModelCreating(ModelBuilder modelBuilder) + //{ + // modelBuilder.Ignore(); + // modelBuilder.Ignore(); + // modelBuilder.Ignore(); + // modelBuilder.Ignore(); + // modelBuilder.Ignore(); + // modelBuilder.Ignore(); + // modelBuilder.Ignore(); + // modelBuilder.Ignore(); + // modelBuilder.Ignore(); + // modelBuilder.Ignore(); + // modelBuilder.Ignore(); + + // modelBuilder.ApplyConfiguration(new CustomerConfiguration()); + // modelBuilder.ApplyConfiguration(new LeadConfiguration()); + // modelBuilder.ApplyConfiguration(new OrderConfiguration()); + // modelBuilder.ApplyConfiguration(new ProductConfiguration()); + // modelBuilder.ApplyConfiguration(new ProductPriceConfiguration()); + // modelBuilder.ApplyConfiguration(new NotificationConfiguration()); + // modelBuilder.ApplyConfiguration(new QuoteConfiguration()); + // modelBuilder.ApplyConfiguration(new ShoppingCartConfiguration()); + // modelBuilder.ApplyConfiguration(new ShoppingCartItemConfiguration()); + // modelBuilder.ApplyConfiguration(new PackageConfirguration()); + // modelBuilder.ApplyConfiguration(new PackageItemConfiguration()); + // modelBuilder.ApplyConfiguration(new ShoppingCartPackageConfiguration()); + + // base.OnModelCreating(modelBuilder); + //} } diff --git a/LiteCharms.Features/Shop/Products/Entities/Product.cs b/LiteCharms.Features/Shop/Products/Entities/Product.cs index 5aa0b99..a48774b 100644 --- a/LiteCharms.Features/Shop/Products/Entities/Product.cs +++ b/LiteCharms.Features/Shop/Products/Entities/Product.cs @@ -3,5 +3,6 @@ [EntityTypeConfiguration] public class Product : Models.Product { + public virtual ICollection? ProductPrices { get; set; } } diff --git a/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs index ee2fb68..4307e62 100644 --- a/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs @@ -4,7 +4,7 @@ public class ProductConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Product)); + builder.ToTable("Products"); builder.HasKey(f => f.Id); builder.Property(f => f.Name).IsRequired(); diff --git a/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs b/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs index a8daa83..beab2ca 100644 --- a/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs +++ b/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs @@ -4,7 +4,7 @@ public class ProductPriceConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(ProductPrice)); + builder.ToTable("ProductPrices"); builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); diff --git a/LiteCharms.Features/Shop/Products/Models/ProductPrice.cs b/LiteCharms.Features/Shop/Products/Models/ProductPrice.cs index 1f44247..fc1860c 100644 --- a/LiteCharms.Features/Shop/Products/Models/ProductPrice.cs +++ b/LiteCharms.Features/Shop/Products/Models/ProductPrice.cs @@ -4,9 +4,9 @@ public class ProductPrice { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public Guid ProductId { get; set; } diff --git a/LiteCharms.Features/Shop/Quotes/Entities/QuoteConfiguration.cs b/LiteCharms.Features/Shop/Quotes/Entities/QuoteConfiguration.cs index 299044e..1363973 100644 --- a/LiteCharms.Features/Shop/Quotes/Entities/QuoteConfiguration.cs +++ b/LiteCharms.Features/Shop/Quotes/Entities/QuoteConfiguration.cs @@ -4,12 +4,12 @@ public class QuoteConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Quote)); + builder.ToTable("Quotes"); builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); - builder.Property(f => f.UpdatedAt).IsRequired(false); - builder.Property(f => f.ExpiredAt).IsRequired(false); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); + builder.Property(f => f.ExpiredAt).IsRequired(false).HasDefaultValueSql(null); builder.Property(f => f.CustomerId).IsRequired(); builder.Property(f => f.OrderId); builder.Property(f => f.ShoppingCartId); diff --git a/LiteCharms.Features/Shop/Quotes/Models/Quote.cs b/LiteCharms.Features/Shop/Quotes/Models/Quote.cs index 5422348..0b5ecaf 100644 --- a/LiteCharms.Features/Shop/Quotes/Models/Quote.cs +++ b/LiteCharms.Features/Shop/Quotes/Models/Quote.cs @@ -4,11 +4,11 @@ public class Quote { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } - public DateTimeOffset? ExpiredAt { get; set; } + public DateTime? ExpiredAt { get; set; } public Guid CustomerId { get; set; } diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs index e45a04b..1609973 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs @@ -4,11 +4,11 @@ public class ShoppingCartConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(ShoppingCart)); + builder.ToTable("ShoppingCarts"); builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); - builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); builder.Property(f => f.CustomerId).IsRequired(); builder.Property(f => f.OrderId); diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItem.cs b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItem.cs index 50afb9d..b93d7da 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItem.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItem.cs @@ -2,6 +2,7 @@ namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; +[EntityTypeConfiguration] public class ShoppingCartItem : Models.ShoppingCartItem { public virtual ShoppingCart? ShoppingCart { get; set; } diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs index 3dbc5f7..7f13aca 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs @@ -6,11 +6,11 @@ public class ShoppingCartItemConfiguration : IEntityTypeConfiguration builder) { - builder.ToTable(nameof(ShoppingCartItem)); + builder.ToTable("ShoppingCartItems"); builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); - builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); builder.Property(f => f.Quantity).IsRequired().HasDefaultValue(1); builder.Property(f => f.ShoppingCartId).IsRequired(); builder.Property(f => f.ProductPriceId).IsRequired(); diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs index ee08e91..d6dc310 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs @@ -4,7 +4,7 @@ public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration builder) { - builder.ToTable(nameof(ShoppingCartPackage)); + builder.ToTable("ShoppingCartPackages"); builder.HasKey(f => f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCart.cs b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCart.cs index fe6f633..46c52d0 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCart.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCart.cs @@ -4,9 +4,9 @@ public class ShoppingCart { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public Guid CustomerId { get; set; } diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartItem.cs b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartItem.cs index 8dcc0be..99eeef6 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartItem.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartItem.cs @@ -8,9 +8,9 @@ public class ShoppingCartItem public Guid ProductPriceId { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public int Quantity { get; set; } } diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartPackage.cs b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartPackage.cs index fdc6d7b..a633be2 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartPackage.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartPackage.cs @@ -4,7 +4,7 @@ public class ShoppingCartPackage { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } public Guid ShoppingCartId { get; set; } diff --git a/LiteCharms.Features/appsettings.json b/LiteCharms.Features/appsettings.json new file mode 100644 index 0000000..aec5c2e --- /dev/null +++ b/LiteCharms.Features/appsettings.json @@ -0,0 +1,22 @@ +{ + "Email": { + "Credentials": { + "Username": "shop@litecharms.co.za" + }, + "Port": 465, + "Host": "mail.litecharms.co.za", + "UseSsl": true + }, + "Monitoring": { + "ApiKey": "", + "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", + "ServiceName": "LiteCharms.LeadGenerator" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From f606b8fd3c86fbdade4b56f763c023c31798b475 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 14 May 2026 02:48:46 +0200 Subject: [PATCH 026/153] Removed other packages from pipeline --- .drone.yml | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/.drone.yml b/.drone.yml index 0c10662..ededa97 100644 --- a/.drone.yml +++ b/.drone.yml @@ -16,31 +16,10 @@ steps: NEXUS_KEY: { from_secret: nexus_api_key } NEXUS_URL: https://nexus.khongisa.co.za/repository/nuget-hosted/ VERSION: 1.${DRONE_BUILD_NUMBER}.0 - commands: - # Abstractions - - dotnet pack LiteCharms.Abstractions/LiteCharms.Abstractions.csproj -c Release -p:PackageVersion=$VERSION -o dist/ - - dotnet nuget push dist/LiteCharms.Abstractions.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL - - # Models - - dotnet pack LiteCharms.Models/LiteCharms.Models.csproj -c Release -p:PackageVersion=$VERSION -o dist/ - - dotnet nuget push dist/LiteCharms.Models.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL - - # Infrastructure - - dotnet pack LiteCharms.Infrastructure/LiteCharms.Infrastructure.csproj -c Release -p:PackageVersion=$VERSION -o dist/ - - dotnet nuget push dist/LiteCharms.Infrastructure.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL - - # Features + commands: - dotnet pack LiteCharms.Features/LiteCharms.Features.csproj -c Release -p:PackageVersion=$VERSION -o dist/ - dotnet nuget push dist/LiteCharms.Features.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL - # Extensions - - dotnet pack LiteCharms.Extensions/LiteCharms.Extensions.csproj -c Release -p:PackageVersion=$VERSION -o dist/ - - dotnet nuget push dist/LiteCharms.Extensions.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL - - # Entities - - dotnet pack LiteCharms.Entities/LiteCharms.Entities.csproj -c Release -p:PackageVersion=$VERSION -o dist/ - - dotnet nuget push dist/LiteCharms.Entities.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL - - name: gitea-tag-release image: alpine/git environment: @@ -61,7 +40,7 @@ steps: \"tag_name\": \"$VERSION\", \"target_commitish\": \"${DRONE_COMMIT_SHA}\", \"name\": \"Library Suite $VERSION\", - \"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Abstractions\n* LiteCharms.Models\n* LiteCharms.Infrastructure\n* LiteCharms.Features\n* LiteCharms.Extensions\n* LiteCharms.Entities\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\", + \"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Abstractions\n* LiteCharms.Features\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\", \"draft\": false, \"prerelease\": false }" From 36b3656886225bcc41b8199633a24053352973cc Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 14 May 2026 02:53:36 +0200 Subject: [PATCH 027/153] Added khongisa host entry on pipeline commands --- .drone.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.drone.yml b/.drone.yml index ededa97..4baabb0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -28,6 +28,7 @@ steps: GITEA_PASS: { from_secret: git_password } VERSION: 1.${DRONE_BUILD_NUMBER}.0 commands: + - echo "169.255.58.144 gitea.khongisa.co.za" >> /etc/hosts - apk add --no-cache curl - git remote set-url origin https://$${GITEA_USER}:$${GITEA_PASS}@gitea.khongisa.co.za/litecharms/components.git - git tag $VERSION From 4523ef61510b175d086c0a2fd1f962a8fd2f587f Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 15 May 2026 07:54:35 +0200 Subject: [PATCH 028/153] Fixed Lead->Customer Relationship --- .../Shop/Leads/Entities/Lead.cs | 2 +- .../Shop/Leads/Entities/LeadConfiguration.cs | 2 +- ..._FixedLeadCustomerRelationship.Designer.cs | 776 ++++++++++++++++++ ...515055221_FixedLeadCustomerRelationship.cs | 166 ++++ .../Migrations/ShopDbContextModelSnapshot.cs | 103 +-- .../Entities/ShoppingCartItemConfiguration.cs | 8 +- 6 files changed, 951 insertions(+), 106 deletions(-) create mode 100644 LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs create mode 100644 LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs diff --git a/LiteCharms.Features/Shop/Leads/Entities/Lead.cs b/LiteCharms.Features/Shop/Leads/Entities/Lead.cs index deda2d6..cbb4ec1 100644 --- a/LiteCharms.Features/Shop/Leads/Entities/Lead.cs +++ b/LiteCharms.Features/Shop/Leads/Entities/Lead.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.Shop.Customers.Models; +using LiteCharms.Features.Shop.Customers.Entities; namespace LiteCharms.Features.Shop.Leads.Entities; diff --git a/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs b/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs index 39a0a20..e24839e 100644 --- a/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs +++ b/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs @@ -24,7 +24,7 @@ public class LeadConfiguration : IEntityTypeConfiguration builder.Property(f => f.AttributionHash).IsRequired(true); builder.HasOne(f => f.Customer) - .WithMany() + .WithMany(f => f.Leads) .HasForeignKey(f => f.CustomerId) .OnDelete(DeleteBehavior.Restrict); } diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs b/LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs new file mode 100644 index 0000000..9789b91 --- /dev/null +++ b/LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs @@ -0,0 +1,776 @@ +// +using System; +using LiteCharms.Features.Shop.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.Shop.Postgres.Migrations +{ + [DbContext(typeof(ShopDbContext))] + [Migration("20260515055221_FixedLeadCustomerRelationship")] + partial class FixedLeadCustomerRelationship + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("PackageItem", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discord") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LinkedIn") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Region") + .HasColumnType("text"); + + b.Property("Slack") + .HasColumnType("text"); + + b.Property("Tax") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.Property("Whatsapp") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdGroupId") + .HasColumnType("bigint"); + + b.Property("AdName") + .HasColumnType("bigint"); + + b.Property("AppClickId") + .HasColumnType("text"); + + b.Property("AttributionHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("CampaignId") + .HasColumnType("bigint"); + + b.Property("ClickId") + .HasColumnType("text"); + + b.Property("ClickLocation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("FeedItemId") + .HasColumnType("bigint"); + + b.Property("Source") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TargetId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebClickId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Leads", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationIdType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsInternal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Processed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); + + b.Property("Status") + .HasColumnType("integer"); + + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderRefunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("Thumbnails") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductPrices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShoppingCartId") + .IsUnique(); + + b.ToTable("Quotes", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("ShoppingCarts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductPriceId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartPackages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany("PackageItems") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Leads") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product") + .WithMany("ProductPrices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Quotes") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId"); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId"); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("ShoppingCarts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("ShoppingCart") + .HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Customer"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartItems") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductPrice"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartPackages") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Navigation("PackageItems"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Navigation("Leads"); + + b.Navigation("Orders"); + + b.Navigation("Quotes"); + + b.Navigation("ShoppingCarts"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Navigation("Quote"); + + b.Navigation("Refunds"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Navigation("ProductPrices"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Navigation("Quote"); + + b.Navigation("ShoppingCartItems"); + + b.Navigation("ShoppingCartPackages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs b/LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs new file mode 100644 index 0000000..1d701f8 --- /dev/null +++ b/LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs @@ -0,0 +1,166 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.Shop.Postgres.Migrations +{ + /// + public partial class FixedLeadCustomerRelationship : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Leads_Customer_CustomerId", + table: "Leads"); + + migrationBuilder.DropForeignKey( + name: "FK_Leads_Customers_CustomerId1", + table: "Leads"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropTable( + name: "Customer"); + + migrationBuilder.DropIndex( + name: "IX_ShoppingCartItems_ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropIndex( + name: "IX_ShoppingCartItems_ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropIndex( + name: "IX_Leads_CustomerId1", + table: "Leads"); + + migrationBuilder.DropColumn( + name: "ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropColumn( + name: "ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropColumn( + name: "CustomerId1", + table: "Leads"); + + migrationBuilder.AddForeignKey( + name: "FK_Leads_Customers_CustomerId", + table: "Leads", + column: "CustomerId", + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Leads_Customers_CustomerId", + table: "Leads"); + + migrationBuilder.AddColumn( + name: "ProductPriceId1", + table: "ShoppingCartItems", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "ShoppingCartId1", + table: "ShoppingCartItems", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "CustomerId1", + table: "Leads", + type: "uuid", + nullable: true); + + migrationBuilder.CreateTable( + name: "Customer", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Active = table.Column(type: "boolean", nullable: false), + Address = table.Column(type: "text", nullable: true), + City = table.Column(type: "text", nullable: true), + Company = table.Column(type: "text", nullable: true), + Country = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Discord = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: true), + LastName = table.Column(type: "text", nullable: true), + LinkedIn = table.Column(type: "text", nullable: true), + Name = table.Column(type: "text", nullable: true), + Phone = table.Column(type: "text", nullable: true), + PostalCode = table.Column(type: "text", nullable: true), + Region = table.Column(type: "text", nullable: true), + Slack = table.Column(type: "text", nullable: true), + Tax = table.Column(type: "text", nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Website = table.Column(type: "text", nullable: true), + Whatsapp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Customer", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ProductPriceId1", + table: "ShoppingCartItems", + column: "ProductPriceId1"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ShoppingCartId1", + table: "ShoppingCartItems", + column: "ShoppingCartId1"); + + migrationBuilder.CreateIndex( + name: "IX_Leads_CustomerId1", + table: "Leads", + column: "CustomerId1"); + + migrationBuilder.AddForeignKey( + name: "FK_Leads_Customer_CustomerId", + table: "Leads", + column: "CustomerId", + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Leads_Customers_CustomerId1", + table: "Leads", + column: "CustomerId1", + principalTable: "Customers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1", + table: "ShoppingCartItems", + column: "ProductPriceId1", + principalTable: "ProductPrices", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1", + table: "ShoppingCartItems", + column: "ShoppingCartId1", + principalTable: "ShoppingCarts", + principalColumn: "Id"); + } + } +} diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs b/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs index 26d0c26..955c903 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs +++ b/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs @@ -166,74 +166,6 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.ToTable("Customers", (string)null); }); - modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Models.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .HasColumnType("text"); - - b.Property("LastName") - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer"); - }); - modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => { b.Property("Id") @@ -270,9 +202,6 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("CustomerId") .HasColumnType("uuid"); - b.Property("CustomerId1") - .HasColumnType("uuid"); - b.Property("FeedItemId") .HasColumnType("bigint"); @@ -295,8 +224,6 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.HasIndex("CustomerId"); - b.HasIndex("CustomerId1"); - b.ToTable("Leads", (string)null); }); @@ -615,9 +542,6 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("ProductPriceId") .HasColumnType("uuid"); - b.Property("ProductPriceId1") - .HasColumnType("uuid"); - b.Property("Quantity") .ValueGeneratedOnAdd() .HasColumnType("integer") @@ -626,9 +550,6 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("ShoppingCartId") .HasColumnType("uuid"); - b.Property("ShoppingCartId1") - .HasColumnType("uuid"); - b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -636,12 +557,8 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.HasIndex("ProductPriceId"); - b.HasIndex("ProductPriceId1"); - b.HasIndex("ShoppingCartId"); - b.HasIndex("ShoppingCartId1"); - b.ToTable("ShoppingCartItems", (string)null); }); @@ -692,15 +609,11 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => { - b.HasOne("LiteCharms.Features.Shop.Customers.Models.Customer", "Customer") - .WithMany() + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Leads") .HasForeignKey("CustomerId") .OnDelete(DeleteBehavior.Restrict); - b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", null) - .WithMany("Leads") - .HasForeignKey("CustomerId1"); - b.Navigation("Customer"); }); @@ -780,26 +693,18 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => { - b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", null) + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") .WithMany() .HasForeignKey("ProductPriceId") .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId1"); - - b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", null) + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") .WithMany("ShoppingCartItems") .HasForeignKey("ShoppingCartId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") - .WithMany() - .HasForeignKey("ShoppingCartId1"); - b.Navigation("ProductPrice"); b.Navigation("ShoppingCart"); diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs index 7f13aca..5670346 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs @@ -1,6 +1,4 @@ -using LiteCharms.Features.Shop.Products.Entities; - -namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; +namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; public class ShoppingCartItemConfiguration : IEntityTypeConfiguration { @@ -15,13 +13,13 @@ public class ShoppingCartItemConfiguration : IEntityTypeConfiguration f.ShoppingCartId).IsRequired(); builder.Property(f => f.ProductPriceId).IsRequired(); - builder.HasOne() + builder.HasOne(f => f.ShoppingCart) .WithMany(s => s.ShoppingCartItems) .HasForeignKey(f => f.ShoppingCartId) .IsRequired() .OnDelete(DeleteBehavior.Cascade); - builder.HasOne() + builder.HasOne(f => f.ProductPrice) .WithMany() .HasForeignKey(f => f.ProductPriceId) .IsRequired() From 65687d231e5957e99c3afe831731410cb3d8665e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 15 May 2026 08:37:58 +0200 Subject: [PATCH 029/153] Ensured UTC is used --- LiteCharms.Features/Quartz/JobOrchestrator.cs | 7 +++---- LiteCharms.Features/Quartz/MediatorJob.cs | 10 +++++++--- .../Shop/CartPackages/PackageService.cs | 3 +-- LiteCharms.Features/Shop/Leads/LeadService.cs | 1 + .../Handlers/ProcessEmailNotificationsEventHandler.cs | 9 ++++----- LiteCharms.Features/Shop/Orders/OrderService.cs | 3 +-- LiteCharms.Features/Shop/Products/ProductService.cs | 7 +++---- LiteCharms.Features/Shop/Quotes/QuoteService.cs | 5 +++-- .../Shop/ShoppingCarts/ShoppingCartService.cs | 3 +-- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/LiteCharms.Features/Quartz/JobOrchestrator.cs b/LiteCharms.Features/Quartz/JobOrchestrator.cs index 4f578b5..ed90adf 100644 --- a/LiteCharms.Features/Quartz/JobOrchestrator.cs +++ b/LiteCharms.Features/Quartz/JobOrchestrator.cs @@ -1,6 +1,5 @@ using LiteCharms.Features.Abstractions; using LiteCharms.Features.Quartz.Abstractions; -using static LiteCharms.Features.Extensions.Timezones; namespace LiteCharms.Features.Quartz; @@ -47,12 +46,12 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra .StoreDurably() .Build(); - var now = SouthAfricanTimeZone.UtcNow(); + var now = DateTime.UtcNow; var trigger = global::Quartz.TriggerBuilder.Create() .WithIdentity(triggerKey) - .WithDescription($"Scheduled via Main Job at {now:g}") - .WithCronSchedule(cronExpression, cron => cron.InTimeZone(SouthAfricanTimeZone) + .WithDescription($"Scheduled via Main Job at {now:g} UTC") + .WithCronSchedule(cronExpression, cron => cron .WithMisfireHandlingInstructionFireAndProceed()) .StartAt(now) .Build(); diff --git a/LiteCharms.Features/Quartz/MediatorJob.cs b/LiteCharms.Features/Quartz/MediatorJob.cs index b3ea928..f67dda1 100644 --- a/LiteCharms.Features/Quartz/MediatorJob.cs +++ b/LiteCharms.Features/Quartz/MediatorJob.cs @@ -1,4 +1,5 @@ using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Mediator; namespace LiteCharms.Features.Quartz; @@ -13,9 +14,12 @@ public class MediatorJob(IMediator mediator) : IJob where TNotifi var notification = JsonSerializer.Deserialize(data); - if(notification is null) return; + if (notification is null) return; + + using var activity = MediatorTelemetry.Source.StartActivity($"Quartz: {typeof(TNotification).Name}"); + + activity?.SetTag("event.correlation_id", notification.CorrelationId); - if(notification is TNotification) - await mediator.Publish(notification, context.CancellationToken); + await mediator.Publish(notification, context.CancellationToken); } } diff --git a/LiteCharms.Features/Shop/CartPackages/PackageService.cs b/LiteCharms.Features/Shop/CartPackages/PackageService.cs index 1df6c00..3479998 100644 --- a/LiteCharms.Features/Shop/CartPackages/PackageService.cs +++ b/LiteCharms.Features/Shop/CartPackages/PackageService.cs @@ -51,7 +51,6 @@ public class PackageService(IDbContextFactory contextFactory) var newPackage = context.Packages.Add(new Entities.Package { - UpdatedAt = null, Name = name, Summary = summary, Description = description, @@ -206,7 +205,7 @@ public class PackageService(IDbContextFactory contextFactory) package.Summary = summary; package.Description = description; package.ImageUrl = ImageUrl; - package.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + package.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() diff --git a/LiteCharms.Features/Shop/Leads/LeadService.cs b/LiteCharms.Features/Shop/Leads/LeadService.cs index f8b4637..f099235 100644 --- a/LiteCharms.Features/Shop/Leads/LeadService.cs +++ b/LiteCharms.Features/Shop/Leads/LeadService.cs @@ -103,6 +103,7 @@ public class LeadService(IDbContextFactory contextFactory) return Result.Fail(new Error($"Lead with ID {leadId} not found.")); lead.Status = status; + lead.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() diff --git a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs index 6a11fda..18009ce 100644 --- a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs +++ b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs @@ -1,7 +1,6 @@ using LiteCharms.Features.Email; -using LiteCharms.Features.Shop.Notifications.Entities; +using LiteCharms.Features.Shop.Notifications.Models; using LiteCharms.Features.Shop.Postgres; -using static LiteCharms.Features.Extensions.Timezones; namespace LiteCharms.Features.Shop.Notifications.Events.Handlers; @@ -17,8 +16,8 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory o.Priority) - .ThenBy(o => o.CreatedAt) + .OrderByDescending(o => o.CreatedAt) + .ThenBy(o => o.Priority) .Where(n => n.CorrelationIdType == CorrelationIdTypes.Email) .Where(n => n.Direction == NotificationDirection.Outgoing) .Take(message.MaxRecords) @@ -44,7 +43,7 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory contextFactory) return Result.Fail(new Error($"Order {request.OrderId} not found")); order.Status = request.Status; - order.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + order.UpdatedAt = DateTime.UtcNow; if(!string.IsNullOrWhiteSpace(request.InvoiceUrl)) order.InvoiceUrl = request.InvoiceUrl; diff --git a/LiteCharms.Features/Shop/Products/ProductService.cs b/LiteCharms.Features/Shop/Products/ProductService.cs index 4b9b294..9016da5 100644 --- a/LiteCharms.Features/Shop/Products/ProductService.cs +++ b/LiteCharms.Features/Shop/Products/ProductService.cs @@ -1,7 +1,6 @@ using LiteCharms.Features.Extensions; using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.Shop.Products.Models; -using static LiteCharms.Features.Extensions.Timezones; namespace LiteCharms.Features.Shop.Products; @@ -19,7 +18,7 @@ public class ProductService(IDbContextFactory contextFactory) return Result.Fail($"Could not find product price with ID {productPriceId}"); price.Active = active; - price.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + price.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() @@ -200,7 +199,7 @@ public class ProductService(IDbContextFactory contextFactory) return Result.Fail($"Could not find product price with ID {productPriceId}"); existingPrice.Active = false; - existingPrice.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + existingPrice.UpdatedAt = DateTime.UtcNow; if (!(await context.SaveChangesAsync(cancellationToken) > 0)) return Result.Fail($"Failed to deactivate existing price of ID {productPriceId}, try again later"); @@ -212,7 +211,7 @@ public class ProductService(IDbContextFactory contextFactory) var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); existingPrice.Active = true; - existingPrice.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + existingPrice.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Fail("Reverted to old price, creation of new price failed") diff --git a/LiteCharms.Features/Shop/Quotes/QuoteService.cs b/LiteCharms.Features/Shop/Quotes/QuoteService.cs index 7e726c1..c3de336 100644 --- a/LiteCharms.Features/Shop/Quotes/QuoteService.cs +++ b/LiteCharms.Features/Shop/Quotes/QuoteService.cs @@ -2,7 +2,6 @@ using LiteCharms.Features.Models; using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.Shop.Quotes.Models; -using static LiteCharms.Features.Extensions.Timezones; namespace LiteCharms.Features.Shop.Quotes; @@ -26,6 +25,7 @@ public class QuoteService(IDbContextFactory contextFactory) return Result.Fail(new Error($"Quote with id {quoteId} is already assigned to order with id {orderId}")); quote.OrderId = orderId; + quote.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() @@ -52,6 +52,7 @@ public class QuoteService(IDbContextFactory contextFactory) return Result.Fail(new Error($"Shopping Cart with id {shoppingCartId} not found")); quote.ShoppingCartId = shoppingCartId; + quote.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() @@ -140,7 +141,7 @@ public class QuoteService(IDbContextFactory contextFactory) return Result.Fail(new Error("Quote not found.")); quote.Status = status; - quote.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + quote.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() diff --git a/LiteCharms.Features/Shop/ShoppingCarts/ShoppingCartService.cs b/LiteCharms.Features/Shop/ShoppingCarts/ShoppingCartService.cs index b4fdd2f..d7eaf16 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/ShoppingCartService.cs +++ b/LiteCharms.Features/Shop/ShoppingCarts/ShoppingCartService.cs @@ -1,7 +1,6 @@ using LiteCharms.Features.Extensions; using LiteCharms.Features.Shop.Postgres; using LiteCharms.Features.Shop.ShoppingCarts.Models; -using static LiteCharms.Features.Extensions.Timezones; namespace LiteCharms.Features.Shop.ShoppingCarts; @@ -284,7 +283,7 @@ public class ShoppingCartService(IDbContextFactory contextFactory return Result.Fail($"Shopping cart item could not be found with id {shoppingCartItemId}"); item.Quantity = quantity; - item.UpdatedAt = SouthAfricanTimeZone.UtcNow(); + item.UpdatedAt = DateTime.UtcNow; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() From be9c83c8a35609964ad50a9845bc534d1eb60bde Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 15 May 2026 08:39:10 +0200 Subject: [PATCH 030/153] Removed Abstractions listing from Git Tag --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 4baabb0..d5b9121 100644 --- a/.drone.yml +++ b/.drone.yml @@ -41,7 +41,7 @@ steps: \"tag_name\": \"$VERSION\", \"target_commitish\": \"${DRONE_COMMIT_SHA}\", \"name\": \"Library Suite $VERSION\", - \"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Abstractions\n* LiteCharms.Features\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\", + \"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Features\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\", \"draft\": false, \"prerelease\": false }" From 0f91f102e576fc1f7a76cca21d3c02f3225baec1 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 15 May 2026 09:51:26 +0200 Subject: [PATCH 031/153] Optimised quartz --- LiteCharms.Features/Extensions/Quartz.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/LiteCharms.Features/Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs index e22b1ca..28299e8 100644 --- a/LiteCharms.Features/Extensions/Quartz.cs +++ b/LiteCharms.Features/Extensions/Quartz.cs @@ -34,7 +34,7 @@ public static class Quartz storage.UseClustering(cluster => { cluster.CheckinInterval = TimeSpan.FromSeconds(30); - cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2); + cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(20); }); }); }); @@ -48,6 +48,8 @@ public static class Quartz services.ConfigureCommon(); + services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); + services.AddQuartz(config => { config.SchedulerName = schedulerName; @@ -72,7 +74,7 @@ public static class Quartz storage.UseClustering(cluster => { cluster.CheckinInterval = TimeSpan.FromSeconds(30); - cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2); + cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(20); }); }); }); @@ -86,14 +88,14 @@ public static class Quartz { options.Scheduling.IgnoreDuplicates = true; options.Scheduling.OverWriteExistingData = true; + options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins"; options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins"; }); services.AddTransient(); services.AddTransient(); - services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); - + return services; } } From dfc62c8fe1a3e9eb13543ffe5f502e7b18e2b91f Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 15 May 2026 22:28:18 +0200 Subject: [PATCH 032/153] Set misfireThreshold to 2min and eased Cluster node checkin limit --- LiteCharms.Features/Extensions/Quartz.cs | 6 ++++-- LiteCharms.Features/Quartz/JobOrchestrator.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/LiteCharms.Features/Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs index 28299e8..fe6103a 100644 --- a/LiteCharms.Features/Extensions/Quartz.cs +++ b/LiteCharms.Features/Extensions/Quartz.cs @@ -34,7 +34,7 @@ public static class Quartz storage.UseClustering(cluster => { cluster.CheckinInterval = TimeSpan.FromSeconds(30); - cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(20); + cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90); }); }); }); @@ -62,6 +62,8 @@ public static class Quartz config.UseDefaultThreadPool(options => options.MaxConcurrency = 1); config.UseTimeZoneConverter(); + config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString()); + config.UsePersistentStore(storage => { storage.PerformSchemaValidation = false; @@ -74,7 +76,7 @@ public static class Quartz storage.UseClustering(cluster => { cluster.CheckinInterval = TimeSpan.FromSeconds(30); - cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(20); + cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90); }); }); }); diff --git a/LiteCharms.Features/Quartz/JobOrchestrator.cs b/LiteCharms.Features/Quartz/JobOrchestrator.cs index ed90adf..7873683 100644 --- a/LiteCharms.Features/Quartz/JobOrchestrator.cs +++ b/LiteCharms.Features/Quartz/JobOrchestrator.cs @@ -52,7 +52,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra .WithIdentity(triggerKey) .WithDescription($"Scheduled via Main Job at {now:g} UTC") .WithCronSchedule(cronExpression, cron => cron - .WithMisfireHandlingInstructionFireAndProceed()) + .WithMisfireHandlingInstructionIgnoreMisfires()) .StartAt(now) .Build(); From 7f4246ac63f30e992ea33b354ca254bce4c53dd5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 15 May 2026 23:08:15 +0200 Subject: [PATCH 033/153] Added debug logging --- .../Handlers/ProcessEmailNotificationsEventHandler.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs index 18009ce..4ed3f81 100644 --- a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs +++ b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs @@ -13,6 +13,8 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory SendEmailAsync(Notification notification, EmailService service, CancellationToken cancellationToken = default) From a31f75c5ef286a8537ed6fdcd3d8308643ddf66d Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 15 May 2026 23:21:31 +0200 Subject: [PATCH 034/153] Added additional logging and traces --- LiteCharms.Features/Quartz/JobOrchestrator.cs | 2 +- LiteCharms.Features/Quartz/MediatorJob.cs | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/LiteCharms.Features/Quartz/JobOrchestrator.cs b/LiteCharms.Features/Quartz/JobOrchestrator.cs index 7873683..4929543 100644 --- a/LiteCharms.Features/Quartz/JobOrchestrator.cs +++ b/LiteCharms.Features/Quartz/JobOrchestrator.cs @@ -50,7 +50,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra var trigger = global::Quartz.TriggerBuilder.Create() .WithIdentity(triggerKey) - .WithDescription($"Scheduled via Main Job at {now:g} UTC") + .WithDescription($"Scheduled via Main Job at {now:g}") .WithCronSchedule(cronExpression, cron => cron .WithMisfireHandlingInstructionIgnoreMisfires()) .StartAt(now) diff --git a/LiteCharms.Features/Quartz/MediatorJob.cs b/LiteCharms.Features/Quartz/MediatorJob.cs index f67dda1..9b52972 100644 --- a/LiteCharms.Features/Quartz/MediatorJob.cs +++ b/LiteCharms.Features/Quartz/MediatorJob.cs @@ -10,16 +10,28 @@ public class MediatorJob(IMediator mediator) : IJob where TNotifi { var data = context.MergedJobDataMap["Payload"] as string; - if (string.IsNullOrWhiteSpace(data)) return; + if (string.IsNullOrWhiteSpace(data)) + { + Trace.WriteLine("Job Payload missing, job ended"); + + return; + } var notification = JsonSerializer.Deserialize(data); - if (notification is null) return; + if (notification is null) + { + Trace.WriteLine("Notification could not be JSon converted from data string, job ended"); + + return; + } using var activity = MediatorTelemetry.Source.StartActivity($"Quartz: {typeof(TNotification).Name}"); activity?.SetTag("event.correlation_id", notification.CorrelationId); await mediator.Publish(notification, context.CancellationToken); + + Trace.WriteLine("Job published"); } } From e978aa17f87df590c7fce879c80e8362e0cf275c Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 15 May 2026 23:52:38 +0200 Subject: [PATCH 035/153] Added an empty constructor to ProcessEmailNotificationEvent --- .../Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs b/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs index d496ab9..366332f 100644 --- a/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs +++ b/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs @@ -8,6 +8,8 @@ public class ProcessEmailNotificationsEvent : EventBase, IEvent public int MaxRecords { get; set; } + public ProcessEmailNotificationsEvent() { MaxRecords = 1000; } + private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords; public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords); From 1471d9e59748309c316419ef52ea4bc35c040f3e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 16 May 2026 00:04:58 +0200 Subject: [PATCH 036/153] Refactored batch drop logic --- .../Events/Handlers/ProcessEmailNotificationsEventHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs index 4ed3f81..f12242b 100644 --- a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs +++ b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs @@ -27,7 +27,7 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory Date: Sat, 16 May 2026 00:28:31 +0200 Subject: [PATCH 037/153] Fixed email sending logic --- .../NotificationsFeatureTests.cs | 11 +++++++++++ LiteCharms.Features/Email/EmailService.cs | 5 +++-- .../ProcessEmailNotificationsEventHandler.cs | 16 +++++++++------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs index ede7fd8..2c7518b 100644 --- a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs +++ b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs @@ -1,4 +1,5 @@ using LiteCharms.Features.Shop.Notifications; +using LiteCharms.Features.Shop.Notifications.Events; namespace LiteCharms.Features.Tests; @@ -32,4 +33,14 @@ public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper foreach (var error in createResult.Errors) output.WriteLine(error.Message); } + + [Fact] + public async Task ProcessEmailNotificationsEvent_ShouldSucceed() + { + var notification = ProcessEmailNotificationsEvent.Create(); + + await fixture.Mediator.Publish(notification); + + Assert.True(true); + } } diff --git a/LiteCharms.Features/Email/EmailService.cs b/LiteCharms.Features/Email/EmailService.cs index 3e6c103..72c5665 100644 --- a/LiteCharms.Features/Email/EmailService.cs +++ b/LiteCharms.Features/Email/EmailService.cs @@ -35,8 +35,9 @@ public class EmailService(IOptions options) : IDisposable var bodyBuilder = new BodyBuilder(); - foreach (var attachment in message.Body?.Attachments!) - bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken); + if (message.Body!.Properties.HasAttachments) + foreach (var attachment in message.Body?.Attachments!) + bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken); if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message; if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message; diff --git a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs index f12242b..6917777 100644 --- a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs +++ b/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs @@ -1,4 +1,5 @@ -using LiteCharms.Features.Email; +using k8s.KubeConfigModels; +using LiteCharms.Features.Email; using LiteCharms.Features.Shop.Notifications.Models; using LiteCharms.Features.Shop.Postgres; @@ -13,15 +14,16 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory o.CreatedAt) .ThenBy(o => o.Priority) - .Where(n => n.CorrelationIdType == CorrelationIdTypes.Email) - .Where(n => n.Direction == NotificationDirection.Outgoing) + .Where(n => n.Platform == NotificationPlatforms.Email && + n.Direction == NotificationDirection.Outgoing && n.Processed == false) .Take(message.MaxRecords) .ToListAsync(cancellationToken); @@ -48,7 +50,7 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory Date: Sun, 17 May 2026 13:12:58 +0200 Subject: [PATCH 038/153] Refactored GetNotificationsAsync() date handling --- .../NotificationsFeatureTests.cs | 20 ++++++++++++++++++- .../Shop/Notifications/NotificationService.cs | 5 +++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs index 2c7518b..7ee7e9a 100644 --- a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs +++ b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs @@ -1,4 +1,5 @@ -using LiteCharms.Features.Shop.Notifications; +using LiteCharms.Features.Models; +using LiteCharms.Features.Shop.Notifications; using LiteCharms.Features.Shop.Notifications.Events; namespace LiteCharms.Features.Tests; @@ -34,6 +35,23 @@ public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper foreach (var error in createResult.Errors) output.WriteLine(error.Message); } + [Fact] + public async Task GetNotifications_ShouldReturn_AllNotifications() + { + DateRange range = new() + { + From = DateOnly.FromDateTime(new DateTime(2026, 05, 01, 0, 0, 0, DateTimeKind.Utc)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 10 + }; + + var getResult = await notificationService.GetNotificationsAsync(range); + + Assert.True(getResult.IsSuccess); + + foreach (var error in getResult.Errors) output.WriteLine(error.Message); + } + [Fact] public async Task ProcessEmailNotificationsEvent_ShouldSucceed() { diff --git a/LiteCharms.Features/Shop/Notifications/NotificationService.cs b/LiteCharms.Features/Shop/Notifications/NotificationService.cs index 59493e4..63eb992 100644 --- a/LiteCharms.Features/Shop/Notifications/NotificationService.cs +++ b/LiteCharms.Features/Shop/Notifications/NotificationService.cs @@ -63,8 +63,8 @@ public class NotificationService(IDbContextFactory contextFactory { try { - var fromDate = range.From.ToDateTime(TimeOnly.MinValue); - var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); using var context = await contextFactory.CreateDbContextAsync(cancellationToken); @@ -96,6 +96,7 @@ public class NotificationService(IDbContextFactory contextFactory return Result.Fail(new Error($"Notification with id {request.NotificationId} not found.")); notification.Processed = request.Processed; + notification.UpdatedAt = DateTime.UtcNow; if (request.HasError) { From da141311ff3940dd1285fdaddb45d9ea91bc735d Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 17 May 2026 16:00:35 +0200 Subject: [PATCH 039/153] Simplified notification updating --- LiteCharms.Features.Tests/NotificationsFeatureTests.cs | 2 +- .../Shop/Notifications/NotificationService.cs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs index 7ee7e9a..05b9f9f 100644 --- a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs +++ b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs @@ -40,7 +40,7 @@ public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper { DateRange range = new() { - From = DateOnly.FromDateTime(new DateTime(2026, 05, 01, 0, 0, 0, DateTimeKind.Utc)), + From = DateOnly.FromDateTime(new DateTime(2026, 04, 01, 0, 0, 0, DateTimeKind.Utc)), To = DateOnly.FromDateTime(DateTime.UtcNow), MaxRecords = 10 }; diff --git a/LiteCharms.Features/Shop/Notifications/NotificationService.cs b/LiteCharms.Features/Shop/Notifications/NotificationService.cs index 63eb992..928a131 100644 --- a/LiteCharms.Features/Shop/Notifications/NotificationService.cs +++ b/LiteCharms.Features/Shop/Notifications/NotificationService.cs @@ -97,12 +97,8 @@ public class NotificationService(IDbContextFactory contextFactory notification.Processed = request.Processed; notification.UpdatedAt = DateTime.UtcNow; - - if (request.HasError) - { - notification.HasError = request.HasError; - notification.Errors = request.Errors; - } + notification.HasError = request.HasError; + notification.Errors = request.Errors; return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() From f245bc94e1d6a41b2973634bc731e49105b948aa Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 19 May 2026 10:23:36 +0200 Subject: [PATCH 040/153] Added S3 support --- LiteCharms.Features/Extensions/S3.cs | 31 +++++++++++++++++++ .../LiteCharms.Features.csproj | 10 ++++++ .../S3/Configuration/S3Settings.cs | 14 +++++++++ LiteCharms.Features/S3/S3Service.cs | 28 +++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 LiteCharms.Features/Extensions/S3.cs create mode 100644 LiteCharms.Features/S3/Configuration/S3Settings.cs create mode 100644 LiteCharms.Features/S3/S3Service.cs diff --git a/LiteCharms.Features/Extensions/S3.cs b/LiteCharms.Features/Extensions/S3.cs new file mode 100644 index 0000000..0a99a49 --- /dev/null +++ b/LiteCharms.Features/Extensions/S3.cs @@ -0,0 +1,31 @@ +using Amazon.Runtime; +using LiteCharms.Features.S3; +using LiteCharms.Features.S3.Configuration; + +namespace LiteCharms.Features.Extensions; + +public static class S3 +{ + public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration) + { + var optionsSection = configuration.GetSection(nameof(S3Settings)); + services.Configure(optionsSection); + + var options = optionsSection.Get() + ?? throw new InvalidOperationException("S3 configuration section is missing."); + + var credentials = new BasicAWSCredentials(options.AccessKey, options.SecretKey); + + var s3Config = new AmazonS3Config + { + ServiceURL = options.ServiceUrl, + AuthenticationRegion = options.Region, + ForcePathStyle = true, + }; + + services.AddSingleton(new AmazonS3Client(credentials, s3Config)); + services.AddScoped(); + + return services; + } +} diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index a9aedad..1e65e11 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -128,6 +128,16 @@ + + + + + + + + + + diff --git a/LiteCharms.Features/S3/Configuration/S3Settings.cs b/LiteCharms.Features/S3/Configuration/S3Settings.cs new file mode 100644 index 0000000..dfcb0c0 --- /dev/null +++ b/LiteCharms.Features/S3/Configuration/S3Settings.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.S3.Configuration; + +public 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; } +} diff --git a/LiteCharms.Features/S3/S3Service.cs b/LiteCharms.Features/S3/S3Service.cs new file mode 100644 index 0000000..b55c267 --- /dev/null +++ b/LiteCharms.Features/S3/S3Service.cs @@ -0,0 +1,28 @@ +namespace LiteCharms.Features.S3; + +public class S3Service(IAmazonS3 amazonS3) +{ + public async Task> UploadFileAsync(string bucketName, string fileName, Stream fileStream, string contentType, string cdnBaseUrl, CancellationToken cancellationToken = default) + { + try + { + var putRequest = new PutObjectRequest + { + BucketName = bucketName, + Key = fileName, + InputStream = fileStream, + ContentType = contentType + }; + + var response = await amazonS3.PutObjectAsync(putRequest, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.OK + ? Result.Fail($"Failed to upload {fileName} to S3.") + : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); + } + } +} From 52d204e286cd6aad5bdf196da9a73f76caad715a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 19 May 2026 11:34:00 +0200 Subject: [PATCH 041/153] Refactored service to internalise the CDN --- LiteCharms.Features/Extensions/S3.cs | 63 +++++++++++++------ .../LiteCharms.Features.csproj | 1 + .../S3/Abstractions/IS3Service.cs | 6 ++ .../S3/Abstractions/S3ServiceBase.cs | 8 +++ .../S3/BookstoreInvoicesS3Service.cs | 38 +++++++++++ .../S3/BookstoreQuotesS3Service.cs | 38 +++++++++++ LiteCharms.Features/S3/BookstoreS3Service.cs | 38 +++++++++++ .../S3/Configuration/S3Settings.cs | 2 + LiteCharms.Features/S3/Constants.cs | 12 ++++ LiteCharms.Features/S3/S3Service.cs | 28 --------- 10 files changed, 188 insertions(+), 46 deletions(-) create mode 100644 LiteCharms.Features/S3/Abstractions/IS3Service.cs create mode 100644 LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs create mode 100644 LiteCharms.Features/S3/BookstoreInvoicesS3Service.cs create mode 100644 LiteCharms.Features/S3/BookstoreQuotesS3Service.cs create mode 100644 LiteCharms.Features/S3/BookstoreS3Service.cs create mode 100644 LiteCharms.Features/S3/Constants.cs delete mode 100644 LiteCharms.Features/S3/S3Service.cs diff --git a/LiteCharms.Features/Extensions/S3.cs b/LiteCharms.Features/Extensions/S3.cs index 0a99a49..5518b7d 100644 --- a/LiteCharms.Features/Extensions/S3.cs +++ b/LiteCharms.Features/Extensions/S3.cs @@ -1,6 +1,6 @@ -using Amazon.Runtime; -using LiteCharms.Features.S3; -using LiteCharms.Features.S3.Configuration; +using LiteCharms.Features.S3; +using LiteCharms.Features.S3.Abstractions; +using static LiteCharms.Features.S3.Constants; namespace LiteCharms.Features.Extensions; @@ -8,23 +8,50 @@ public static class S3 { public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration) { - var optionsSection = configuration.GetSection(nameof(S3Settings)); - services.Configure(optionsSection); - - var options = optionsSection.Get() - ?? throw new InvalidOperationException("S3 configuration section is missing."); - - var credentials = new BasicAWSCredentials(options.AccessKey, options.SecretKey); - - var s3Config = new AmazonS3Config + if (configuration.GetSection(BookshopBucketName) is not null) { - ServiceURL = options.ServiceUrl, - AuthenticationRegion = options.Region, - ForcePathStyle = true, - }; + services.AddKeyedSingleton(BookshopBucketName, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopS3SettingsSection}:AccessKey").Value, + configuration.GetSection($"{BookshopS3SettingsSection}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{BookshopS3SettingsSection}:Region").Value, + ForcePathStyle = true, + })); - services.AddSingleton(new AmazonS3Client(credentials, s3Config)); - services.AddScoped(); + services.AddKeyedScoped(BookshopBucketName); + } + + if (configuration.GetSection(BookshopInvoicesBucketName) is not null) + { + services.AddKeyedSingleton(BookshopInvoicesBucketName, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopInvoicesBucketName}:AccessKey").Value, + configuration.GetSection($"{BookshopInvoicesBucketName}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{BookshopInvoicesBucketName}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{BookshopInvoicesBucketName}:Region").Value, + ForcePathStyle = true, + })); + + services.AddKeyedScoped(BookshopInvoicesBucketName); + } + + if (configuration.GetSection(BookshopQuotesBucketName) is not null) + { + services.AddKeyedSingleton(BookshopQuotesBucketName, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopQuotesBucketName}:AccessKey").Value, + configuration.GetSection($"{BookshopQuotesBucketName}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{BookshopQuotesBucketName}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{BookshopQuotesBucketName}:Region").Value, + ForcePathStyle = true, + })); + + services.AddKeyedScoped(BookshopQuotesBucketName); + } return services; } diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 1e65e11..ab20ae2 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -137,6 +137,7 @@ + diff --git a/LiteCharms.Features/S3/Abstractions/IS3Service.cs b/LiteCharms.Features/S3/Abstractions/IS3Service.cs new file mode 100644 index 0000000..8684b2d --- /dev/null +++ b/LiteCharms.Features/S3/Abstractions/IS3Service.cs @@ -0,0 +1,6 @@ +namespace LiteCharms.Features.S3.Abstractions; + +public interface IS3Service +{ + Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default); +} diff --git a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs new file mode 100644 index 0000000..7b9f147 --- /dev/null +++ b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.S3.Abstractions; + +public abstract class S3ServiceBase(IAmazonS3 amazonS3) +{ + protected readonly IAmazonS3 client = amazonS3; + + public abstract Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default); +} diff --git a/LiteCharms.Features/S3/BookstoreInvoicesS3Service.cs b/LiteCharms.Features/S3/BookstoreInvoicesS3Service.cs new file mode 100644 index 0000000..9614d6f --- /dev/null +++ b/LiteCharms.Features/S3/BookstoreInvoicesS3Service.cs @@ -0,0 +1,38 @@ +using LiteCharms.Features.S3.Abstractions; + +namespace LiteCharms.Features.S3; + +public class BookstoreInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(Constants.BookshopInvoicesBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + public override async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) + { + try + { + var bucketName = configuration.GetSection($"{Constants.BookshopInvoicesS3SettingsSection}:BucketName").Value!; + var cdnBaseUrl = configuration.GetSection($"{Constants.BookshopInvoicesS3SettingsSection}:CdnBaseUrl").Value!; + + if(string.IsNullOrWhiteSpace(bucketName)) + return Result.Fail("Bucket name is not configured."); + + if (string.IsNullOrWhiteSpace(cdnBaseUrl)) + return Result.Fail("CDN base URL is not configured."); + + var response = await client.PutObjectAsync(new PutObjectRequest + { + BucketName = bucketName, + Key = fileName, + InputStream = fileStream, + ContentType = contentType + }, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.OK + ? Result.Fail($"Failed to upload {fileName} to S3.") + : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/S3/BookstoreQuotesS3Service.cs b/LiteCharms.Features/S3/BookstoreQuotesS3Service.cs new file mode 100644 index 0000000..79b1abc --- /dev/null +++ b/LiteCharms.Features/S3/BookstoreQuotesS3Service.cs @@ -0,0 +1,38 @@ +using LiteCharms.Features.S3.Abstractions; + +namespace LiteCharms.Features.S3; + +public class BookstoreQuotesS3Service(IConfiguration configuration, [FromKeyedServices(Constants.BookshopQuotesBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + public override async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) + { + try + { + var bucketName = configuration.GetSection($"{Constants.BookshopQuotesS3SettingsSection}:BucketName").Value!; + var cdnBaseUrl = configuration.GetSection($"{Constants.BookshopQuotesS3SettingsSection}:CdnBaseUrl").Value!; + + if(string.IsNullOrWhiteSpace(bucketName)) + return Result.Fail("Bucket name is not configured."); + + if (string.IsNullOrWhiteSpace(cdnBaseUrl)) + return Result.Fail("CDN base URL is not configured."); + + var response = await client.PutObjectAsync(new PutObjectRequest + { + BucketName = bucketName, + Key = fileName, + InputStream = fileStream, + ContentType = contentType + }, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.OK + ? Result.Fail($"Failed to upload {fileName} to S3.") + : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/S3/BookstoreS3Service.cs b/LiteCharms.Features/S3/BookstoreS3Service.cs new file mode 100644 index 0000000..2edf506 --- /dev/null +++ b/LiteCharms.Features/S3/BookstoreS3Service.cs @@ -0,0 +1,38 @@ +using LiteCharms.Features.S3.Abstractions; + +namespace LiteCharms.Features.S3; + +public class BookstoreS3Service(IConfiguration configuration, [FromKeyedServices(Constants.BookshopBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + public override async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) + { + try + { + var bucketName = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:BucketName").Value!; + var cdnBaseUrl = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:CdnBaseUrl").Value!; + + if(string.IsNullOrWhiteSpace(bucketName)) + return Result.Fail("Bucket name is not configured."); + + if (string.IsNullOrWhiteSpace(cdnBaseUrl)) + return Result.Fail("CDN base URL is not configured."); + + var response = await client.PutObjectAsync(new PutObjectRequest + { + BucketName = bucketName, + Key = fileName, + InputStream = fileStream, + ContentType = contentType + }, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.OK + ? Result.Fail($"Failed to upload {fileName} to S3.") + : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/S3/Configuration/S3Settings.cs b/LiteCharms.Features/S3/Configuration/S3Settings.cs index dfcb0c0..6be7460 100644 --- a/LiteCharms.Features/S3/Configuration/S3Settings.cs +++ b/LiteCharms.Features/S3/Configuration/S3Settings.cs @@ -11,4 +11,6 @@ public class S3Settings public string? BucketName { get; set; } public string? Region { get; set; } + + public string? CdnBaseUrl { get; set; } } diff --git a/LiteCharms.Features/S3/Constants.cs b/LiteCharms.Features/S3/Constants.cs new file mode 100644 index 0000000..3da4501 --- /dev/null +++ b/LiteCharms.Features/S3/Constants.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.S3; + +public static class Constants +{ + public const string BookshopS3SettingsSection = "BookshopS3Settings"; + public const string BookshopInvoicesS3SettingsSection = "BookshopInvoicesS3Settings"; + public const string BookshopQuotesS3SettingsSection = "BookshopQuotesS3Settings"; + + public const string BookshopBucketName = "bookshop"; + public const string BookshopInvoicesBucketName = "bookshop.invoices"; + public const string BookshopQuotesBucketName = "bookshop.quotes"; +} diff --git a/LiteCharms.Features/S3/S3Service.cs b/LiteCharms.Features/S3/S3Service.cs deleted file mode 100644 index b55c267..0000000 --- a/LiteCharms.Features/S3/S3Service.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace LiteCharms.Features.S3; - -public class S3Service(IAmazonS3 amazonS3) -{ - public async Task> UploadFileAsync(string bucketName, string fileName, Stream fileStream, string contentType, string cdnBaseUrl, CancellationToken cancellationToken = default) - { - try - { - var putRequest = new PutObjectRequest - { - BucketName = bucketName, - Key = fileName, - InputStream = fileStream, - ContentType = contentType - }; - - var response = await amazonS3.PutObjectAsync(putRequest, cancellationToken); - - return response.HttpStatusCode != System.Net.HttpStatusCode.OK - ? Result.Fail($"Failed to upload {fileName} to S3.") - : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); - } - catch (Exception ex) - { - return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); - } - } -} From 89a343a85fb2fad7bb4019478521f737ca9f15f2 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 19 May 2026 14:57:14 +0200 Subject: [PATCH 042/153] Updated how i use configs --- LiteCharms.Features/S3/BookstoreS3Service.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LiteCharms.Features/S3/BookstoreS3Service.cs b/LiteCharms.Features/S3/BookstoreS3Service.cs index 2edf506..0ac895b 100644 --- a/LiteCharms.Features/S3/BookstoreS3Service.cs +++ b/LiteCharms.Features/S3/BookstoreS3Service.cs @@ -5,13 +5,13 @@ namespace LiteCharms.Features.S3; public class BookstoreS3Service(IConfiguration configuration, [FromKeyedServices(Constants.BookshopBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service { + private readonly string bucketName = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:BucketName").Value ?? ""; + private readonly string cdnBaseUrl = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:CdnBaseUrl").Value ?? ""; + public override async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) { try { - var bucketName = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:BucketName").Value!; - var cdnBaseUrl = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:CdnBaseUrl").Value!; - if(string.IsNullOrWhiteSpace(bucketName)) return Result.Fail("Bucket name is not configured."); From d6fdf1b9c8702f03a59cf113c2a8096ee0c39eae Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 20 May 2026 08:01:44 +0200 Subject: [PATCH 043/153] Refactored the S3 services to properly upload the file --- LiteCharms.Features.Tests/CommonFixture.cs | 6 ++- .../LiteCharms.Features.Tests.csproj | 10 ++-- .../S3ServiceFeatureTests.cs | 28 ++++++++++ LiteCharms.Features.Tests/appsettings.json | 12 +++++ LiteCharms.Features/Extensions/S3.cs | 54 ++++++++++--------- .../S3/Abstractions/S3ServiceBase.cs | 38 ++++++++++++- .../S3/BookshopInvoicesS3Service.cs | 11 ++++ .../S3/BookshopQuotesS3Service.cs | 11 ++++ LiteCharms.Features/S3/BookshopS3Service.cs | 11 ++++ .../S3/BookstoreInvoicesS3Service.cs | 38 ------------- .../S3/BookstoreQuotesS3Service.cs | 38 ------------- LiteCharms.Features/S3/BookstoreS3Service.cs | 38 ------------- 12 files changed, 148 insertions(+), 147 deletions(-) create mode 100644 LiteCharms.Features.Tests/S3ServiceFeatureTests.cs create mode 100644 LiteCharms.Features/S3/BookshopInvoicesS3Service.cs create mode 100644 LiteCharms.Features/S3/BookshopQuotesS3Service.cs create mode 100644 LiteCharms.Features/S3/BookshopS3Service.cs delete mode 100644 LiteCharms.Features/S3/BookstoreInvoicesS3Service.cs delete mode 100644 LiteCharms.Features/S3/BookstoreQuotesS3Service.cs delete mode 100644 LiteCharms.Features/S3/BookstoreS3Service.cs diff --git a/LiteCharms.Features.Tests/CommonFixture.cs b/LiteCharms.Features.Tests/CommonFixture.cs index a73bed2..b9085c4 100644 --- a/LiteCharms.Features.Tests/CommonFixture.cs +++ b/LiteCharms.Features.Tests/CommonFixture.cs @@ -14,18 +14,20 @@ public class CommonFixture : IDisposable { Configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json") .AddUserSecrets() + .AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); - Services = new ServiceCollection() + Services = new ServiceCollection() .AddMediator() .AddLogging() .AddShopServices() .AddEmailServiceBus() + .AddGarageS3(Configuration) .AddShopDatabase(Configuration) .AddEmailServices(Configuration) + .AddSingleton(Configuration) .BuildServiceProvider(); Mediator = Services.GetRequiredService(); diff --git a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj index 7070851..55f1b2e 100644 --- a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj +++ b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj @@ -27,10 +27,10 @@ - - - - + + + + @@ -43,7 +43,7 @@ - PreserveNewest + Always diff --git a/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs b/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs new file mode 100644 index 0000000..b7f7ac4 --- /dev/null +++ b/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs @@ -0,0 +1,28 @@ +using LiteCharms.Features.S3.Abstractions; + +namespace LiteCharms.Features.Tests; + +public class S3ServiceFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture +{ + [Fact] + public async Task BookshopS3Service_MustReturnUrl() + { + var service = fixture.Services.GetKeyedService(S3.Constants.BookshopQuotesBucketName); + + var fileName = "appsettings.json"; + + string path = Path.Combine(Directory.GetCurrentDirectory(), fileName); + + Assert.True(File.Exists(path)); + + var stream = File.OpenRead(path); + + var result = await service!.UploadFileAsync(fileName, stream, MimeKit.MimeTypes.GetMimeType(fileName)); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + Assert.NotEmpty(result.Value); + + output.WriteLine(result.Value); + } +} diff --git a/LiteCharms.Features.Tests/appsettings.json b/LiteCharms.Features.Tests/appsettings.json index aec5c2e..1066af9 100644 --- a/LiteCharms.Features.Tests/appsettings.json +++ b/LiteCharms.Features.Tests/appsettings.json @@ -1,4 +1,16 @@ { + "BookshopS3Settings": { + "ServiceUrl": "http://192.168.1.177:30900", + "Region": "garage", + "BucketName": "bookshop", + "CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za" + }, + "BookshopQuotesS3Settings": { + "ServiceUrl": "http://192.168.1.177:30900", + "Region": "garage", + "BucketName": "bookshop.quotes", + "CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za" + }, "Email": { "Credentials": { "Username": "shop@litecharms.co.za" diff --git a/LiteCharms.Features/Extensions/S3.cs b/LiteCharms.Features/Extensions/S3.cs index 5518b7d..2b2fd1e 100644 --- a/LiteCharms.Features/Extensions/S3.cs +++ b/LiteCharms.Features/Extensions/S3.cs @@ -7,8 +7,8 @@ namespace LiteCharms.Features.Extensions; public static class S3 { public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration) - { - if (configuration.GetSection(BookshopBucketName) is not null) + { + if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value)) { services.AddKeyedSingleton(BookshopBucketName, (provider, client) => new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopS3SettingsSection}:AccessKey").Value, @@ -18,39 +18,45 @@ public static class S3 ServiceURL = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value, AuthenticationRegion = configuration.GetSection($"{BookshopS3SettingsSection}:Region").Value, ForcePathStyle = true, + EndpointDiscoveryEnabled = true, + UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://") })); - services.AddKeyedScoped(BookshopBucketName); + services.AddKeyedScoped(BookshopBucketName); } - if (configuration.GetSection(BookshopInvoicesBucketName) is not null) + if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:ServiceUrl").Value)) { services.AddKeyedSingleton(BookshopInvoicesBucketName, (provider, client) => - new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopInvoicesBucketName}:AccessKey").Value, - configuration.GetSection($"{BookshopInvoicesBucketName}:SecretKey").Value), + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:AccessKey").Value, + configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:SecretKey").Value), new AmazonS3Config { - ServiceURL = configuration.GetSection($"{BookshopInvoicesBucketName}:ServiceUrl").Value, - AuthenticationRegion = configuration.GetSection($"{BookshopInvoicesBucketName}:Region").Value, - ForcePathStyle = true, - })); - - services.AddKeyedScoped(BookshopInvoicesBucketName); - } - - if (configuration.GetSection(BookshopQuotesBucketName) is not null) - { - services.AddKeyedSingleton(BookshopQuotesBucketName, (provider, client) => - new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopQuotesBucketName}:AccessKey").Value, - configuration.GetSection($"{BookshopQuotesBucketName}:SecretKey").Value), - new AmazonS3Config - { - ServiceURL = configuration.GetSection($"{BookshopQuotesBucketName}:ServiceUrl").Value, - AuthenticationRegion = configuration.GetSection($"{BookshopQuotesBucketName}:Region").Value, + ServiceURL = configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:Region").Value, ForcePathStyle = true, + EndpointDiscoveryEnabled = true, + UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://") })); - services.AddKeyedScoped(BookshopQuotesBucketName); + services.AddKeyedScoped(BookshopInvoicesBucketName); + } + + if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopQuotesS3SettingsSection}:ServiceUrl").Value)) + { + services.AddKeyedSingleton(BookshopQuotesBucketName, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopQuotesS3SettingsSection}:AccessKey").Value, + configuration.GetSection($"{BookshopQuotesS3SettingsSection}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{BookshopQuotesS3SettingsSection}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{BookshopQuotesS3SettingsSection}:Region").Value, + ForcePathStyle = true, + EndpointDiscoveryEnabled = true, + UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://") + })); + + services.AddKeyedScoped(BookshopQuotesBucketName); } return services; diff --git a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs index 7b9f147..1b232fc 100644 --- a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs +++ b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs @@ -2,7 +2,41 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3) { - protected readonly IAmazonS3 client = amazonS3; + protected readonly IAmazonS3 Client = amazonS3; - public abstract Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default); + protected abstract string BucketName { get; } + protected abstract string CdnBaseUrl { get; } + + public virtual async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) + { + try + { + if (string.IsNullOrWhiteSpace(BucketName)) + return Result.Fail("Bucket name is not configured."); + + if (string.IsNullOrWhiteSpace(CdnBaseUrl)) + return Result.Fail("CDN base URL is not configured."); + + var fileKey = $"{Guid.NewGuid():N}{Path.GetExtension(fileName)}"; + + var putRequest = new PutObjectRequest + { + BucketName = BucketName, + Key = fileKey, + InputStream = fileStream, + ContentType = contentType, + UseChunkEncoding = false + }; + + var response = await Client.PutObjectAsync(putRequest, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.OK + ? Result.Fail($"Failed to upload {fileName} to S3.") + : Result.Ok($"{CdnBaseUrl}/{fileKey}"); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); + } + } } diff --git a/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs b/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs new file mode 100644 index 0000000..8093591 --- /dev/null +++ b/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.S3.Abstractions; +using static LiteCharms.Features.S3.Constants; + +namespace LiteCharms.Features.S3; + +public class BookshopInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopInvoicesBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + protected override string BucketName => configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:BucketName").Value ?? ""; + protected override string CdnBaseUrl => configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:CdnBaseUrl").Value ?? ""; +} diff --git a/LiteCharms.Features/S3/BookshopQuotesS3Service.cs b/LiteCharms.Features/S3/BookshopQuotesS3Service.cs new file mode 100644 index 0000000..0f87fa0 --- /dev/null +++ b/LiteCharms.Features/S3/BookshopQuotesS3Service.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.S3.Abstractions; +using static LiteCharms.Features.S3.Constants; + +namespace LiteCharms.Features.S3; + +public class BookshopQuotesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopQuotesBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + protected override string BucketName => configuration.GetSection($"{BookshopQuotesS3SettingsSection}:BucketName").Value ?? ""; + protected override string CdnBaseUrl => configuration.GetSection($"{BookshopQuotesS3SettingsSection}:CdnBaseUrl").Value ?? ""; +} diff --git a/LiteCharms.Features/S3/BookshopS3Service.cs b/LiteCharms.Features/S3/BookshopS3Service.cs new file mode 100644 index 0000000..aff9cf5 --- /dev/null +++ b/LiteCharms.Features/S3/BookshopS3Service.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.S3.Abstractions; +using static LiteCharms.Features.S3.Constants; + +namespace LiteCharms.Features.S3; + +public class BookshopS3Service(IConfiguration configuration, [FromKeyedServices(BookshopBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + protected override string BucketName => configuration.GetSection($"{BookshopS3SettingsSection}:BucketName").Value ?? ""; + protected override string CdnBaseUrl => configuration.GetSection($"{BookshopS3SettingsSection}:CdnBaseUrl").Value ?? ""; +} diff --git a/LiteCharms.Features/S3/BookstoreInvoicesS3Service.cs b/LiteCharms.Features/S3/BookstoreInvoicesS3Service.cs deleted file mode 100644 index 9614d6f..0000000 --- a/LiteCharms.Features/S3/BookstoreInvoicesS3Service.cs +++ /dev/null @@ -1,38 +0,0 @@ -using LiteCharms.Features.S3.Abstractions; - -namespace LiteCharms.Features.S3; - -public class BookstoreInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(Constants.BookshopInvoicesBucketName)] IAmazonS3 amazonS3) : - S3ServiceBase(amazonS3), IS3Service -{ - public override async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) - { - try - { - var bucketName = configuration.GetSection($"{Constants.BookshopInvoicesS3SettingsSection}:BucketName").Value!; - var cdnBaseUrl = configuration.GetSection($"{Constants.BookshopInvoicesS3SettingsSection}:CdnBaseUrl").Value!; - - if(string.IsNullOrWhiteSpace(bucketName)) - return Result.Fail("Bucket name is not configured."); - - if (string.IsNullOrWhiteSpace(cdnBaseUrl)) - return Result.Fail("CDN base URL is not configured."); - - var response = await client.PutObjectAsync(new PutObjectRequest - { - BucketName = bucketName, - Key = fileName, - InputStream = fileStream, - ContentType = contentType - }, cancellationToken); - - return response.HttpStatusCode != System.Net.HttpStatusCode.OK - ? Result.Fail($"Failed to upload {fileName} to S3.") - : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); - } - catch (Exception ex) - { - return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/S3/BookstoreQuotesS3Service.cs b/LiteCharms.Features/S3/BookstoreQuotesS3Service.cs deleted file mode 100644 index 79b1abc..0000000 --- a/LiteCharms.Features/S3/BookstoreQuotesS3Service.cs +++ /dev/null @@ -1,38 +0,0 @@ -using LiteCharms.Features.S3.Abstractions; - -namespace LiteCharms.Features.S3; - -public class BookstoreQuotesS3Service(IConfiguration configuration, [FromKeyedServices(Constants.BookshopQuotesBucketName)] IAmazonS3 amazonS3) : - S3ServiceBase(amazonS3), IS3Service -{ - public override async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) - { - try - { - var bucketName = configuration.GetSection($"{Constants.BookshopQuotesS3SettingsSection}:BucketName").Value!; - var cdnBaseUrl = configuration.GetSection($"{Constants.BookshopQuotesS3SettingsSection}:CdnBaseUrl").Value!; - - if(string.IsNullOrWhiteSpace(bucketName)) - return Result.Fail("Bucket name is not configured."); - - if (string.IsNullOrWhiteSpace(cdnBaseUrl)) - return Result.Fail("CDN base URL is not configured."); - - var response = await client.PutObjectAsync(new PutObjectRequest - { - BucketName = bucketName, - Key = fileName, - InputStream = fileStream, - ContentType = contentType - }, cancellationToken); - - return response.HttpStatusCode != System.Net.HttpStatusCode.OK - ? Result.Fail($"Failed to upload {fileName} to S3.") - : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); - } - catch (Exception ex) - { - return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/S3/BookstoreS3Service.cs b/LiteCharms.Features/S3/BookstoreS3Service.cs deleted file mode 100644 index 0ac895b..0000000 --- a/LiteCharms.Features/S3/BookstoreS3Service.cs +++ /dev/null @@ -1,38 +0,0 @@ -using LiteCharms.Features.S3.Abstractions; - -namespace LiteCharms.Features.S3; - -public class BookstoreS3Service(IConfiguration configuration, [FromKeyedServices(Constants.BookshopBucketName)] IAmazonS3 amazonS3) : - S3ServiceBase(amazonS3), IS3Service -{ - private readonly string bucketName = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:BucketName").Value ?? ""; - private readonly string cdnBaseUrl = configuration.GetSection($"{Constants.BookshopS3SettingsSection}:CdnBaseUrl").Value ?? ""; - - public override async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) - { - try - { - if(string.IsNullOrWhiteSpace(bucketName)) - return Result.Fail("Bucket name is not configured."); - - if (string.IsNullOrWhiteSpace(cdnBaseUrl)) - return Result.Fail("CDN base URL is not configured."); - - var response = await client.PutObjectAsync(new PutObjectRequest - { - BucketName = bucketName, - Key = fileName, - InputStream = fileStream, - ContentType = contentType - }, cancellationToken); - - return response.HttpStatusCode != System.Net.HttpStatusCode.OK - ? Result.Fail($"Failed to upload {fileName} to S3.") - : Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName)); - } - catch (Exception ex) - { - return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); - } - } -} From ccf30ac36b7d17a0b71fdab9aee72ae7b1c68b42 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 20 May 2026 15:32:54 +0200 Subject: [PATCH 044/153] Optimised UploadFileAsync() Implemented and tested DeleteFileAsync() --- .../S3ServiceFeatureTests.cs | 28 ++++++++++- LiteCharms.Features/Extensions/Hash.cs | 8 +++- .../S3/Abstractions/IS3Service.cs | 1 + .../S3/Abstractions/S3ServiceBase.cs | 47 +++++++++++++++++-- LiteCharms.Features/Shop/Leads/LeadService.cs | 2 +- 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs b/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs index b7f7ac4..3697a7a 100644 --- a/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs +++ b/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs @@ -7,7 +7,7 @@ public class S3ServiceFeatureTests(CommonFixture fixture, ITestOutputHelper outp [Fact] public async Task BookshopS3Service_MustReturnUrl() { - var service = fixture.Services.GetKeyedService(S3.Constants.BookshopQuotesBucketName); + var service = fixture.Services.GetKeyedService(S3.Constants.BookshopBucketName); var fileName = "appsettings.json"; @@ -25,4 +25,30 @@ public class S3ServiceFeatureTests(CommonFixture fixture, ITestOutputHelper outp output.WriteLine(result.Value); } + + [Fact] + public async Task BookshopS3Service_MustDeleteFile() + { + var service = fixture.Services.GetKeyedService(S3.Constants.BookshopBucketName); + + var fileName = "appsettings.json"; + + string path = Path.Combine(Directory.GetCurrentDirectory(), fileName); + + Assert.True(File.Exists(path)); + + var stream = File.OpenRead(path); + + var uploadResult = await service!.UploadFileAsync(fileName, stream, MimeKit.MimeTypes.GetMimeType(fileName)); + + Assert.True(uploadResult.IsSuccess); + Assert.NotNull(uploadResult.Value); + Assert.NotEmpty(uploadResult.Value); + + var fileKey = uploadResult.Value.Split('/').Last(); + + var deleteResult = await service!.DeleteFileAsync(fileKey); + + Assert.True(deleteResult.IsSuccess); + } } diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs index ab76ecc..d903299 100644 --- a/LiteCharms.Features/Extensions/Hash.cs +++ b/LiteCharms.Features/Extensions/Hash.cs @@ -2,6 +2,12 @@ public static class Hash { - public static Func GenerateSha256HashString = (input) => + public static Func StringToSha256Hash = (input) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!))); + + public static Func StreamToSha256Hash = (stream) => + Convert.ToHexString(SHA256.HashData(stream)); + + public static Func BytesToSha256Hash = (bytes) => + Convert.ToHexString(SHA256.HashData(bytes)); } diff --git a/LiteCharms.Features/S3/Abstractions/IS3Service.cs b/LiteCharms.Features/S3/Abstractions/IS3Service.cs index 8684b2d..4c0cdb5 100644 --- a/LiteCharms.Features/S3/Abstractions/IS3Service.cs +++ b/LiteCharms.Features/S3/Abstractions/IS3Service.cs @@ -3,4 +3,5 @@ public interface IS3Service { Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default); + Task DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default); } diff --git a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs index 1b232fc..6679d5d 100644 --- a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs +++ b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.S3.Abstractions; +using static LiteCharms.Features.Extensions.Hash; + +namespace LiteCharms.Features.S3.Abstractions; public abstract class S3ServiceBase(IAmazonS3 amazonS3) { @@ -17,17 +19,31 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3) if (string.IsNullOrWhiteSpace(CdnBaseUrl)) return Result.Fail("CDN base URL is not configured."); - var fileKey = $"{Guid.NewGuid():N}{Path.GetExtension(fileName)}"; + using var stream = new MemoryStream(); + + await fileStream.CopyToAsync(stream, cancellationToken); + await fileStream.DisposeAsync(); + + stream.Seek(0, SeekOrigin.Begin); + + var fileHash = StreamToSha256Hash(stream); + + if(string.IsNullOrWhiteSpace(fileHash)) + return Result.Fail("Failed to compute file hash."); + + var fileKey = $"{fileHash.ToLower()}{Path.GetExtension(fileName)}"; var putRequest = new PutObjectRequest { BucketName = BucketName, Key = fileKey, - InputStream = fileStream, + 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 @@ -39,4 +55,29 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3) return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); } } + + public virtual async Task DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) + { + try + { + if (string.IsNullOrWhiteSpace(BucketName)) + return Result.Fail("Bucket name is not configured."); + + var deleteRequest = new DeleteObjectRequest + { + BucketName = BucketName, + Key = fileKey + }; + + var response = await Client.DeleteObjectAsync(deleteRequest, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.NoContent + ? Result.Fail($"Failed to delete {fileKey} from S3.") + : Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error deleting {fileKey} from S3: {ex.Message}").CausedBy(ex)); + } + } } diff --git a/LiteCharms.Features/Shop/Leads/LeadService.cs b/LiteCharms.Features/Shop/Leads/LeadService.cs index f099235..18c5063 100644 --- a/LiteCharms.Features/Shop/Leads/LeadService.cs +++ b/LiteCharms.Features/Shop/Leads/LeadService.cs @@ -28,7 +28,7 @@ public class LeadService(IDbContextFactory contextFactory) FeedItemId = request.FeedItemId, Status = LeadStatus.New, TargetId = request.TargetId, - AttributionHash = GenerateSha256HashString.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}") + AttributionHash = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}") }); return await context.SaveChangesAsync(cancellationToken) > 0 From 3656223b5f596095383a95825e553b776a50145a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 20 May 2026 21:13:58 +0200 Subject: [PATCH 045/153] Added product metadata Added dates to product model Migrated database changes --- .../{EmailEnquiry.cs => EmailEnquiryModel.cs} | 2 +- .../Extensions/EntityModeMappers.cs | 6 +- .../Shop/CartPackages/PackageService.cs | 1 - ...520191059_AddedProductMetadata.Designer.cs | 788 ++++++++++++++++++ .../20260520191059_AddedProductMetadata.cs | 71 ++ .../Migrations/ShopDbContextModelSnapshot.cs | 14 +- .../Products/Entities/ProductConfiguration.cs | 5 +- .../Entities/ProductPriceConfiguration.cs | 2 +- .../Products/Models/CreateProductModel.cs | 42 + .../Shop/Products/Models/Product.cs | 6 + .../Shop/Products/Models/ProductMetadata.cs | 12 + .../Shop/Products/Models/Records.cs | 2 + .../Shop/Products/ProductService.cs | 59 +- 13 files changed, 998 insertions(+), 12 deletions(-) rename LiteCharms.Features/Email/Models/{EmailEnquiry.cs => EmailEnquiryModel.cs} (94%) create mode 100644 LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs create mode 100644 LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs create mode 100644 LiteCharms.Features/Shop/Products/Models/CreateProductModel.cs create mode 100644 LiteCharms.Features/Shop/Products/Models/ProductMetadata.cs diff --git a/LiteCharms.Features/Email/Models/EmailEnquiry.cs b/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs similarity index 94% rename from LiteCharms.Features/Email/Models/EmailEnquiry.cs rename to LiteCharms.Features/Email/Models/EmailEnquiryModel.cs index 97c2fe3..b13e3a7 100644 --- a/LiteCharms.Features/Email/Models/EmailEnquiry.cs +++ b/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Email.Models; -public sealed class EmailEnquiry +public sealed class EmailEnquiryModel { [Required] [MinLength(2)] diff --git a/LiteCharms.Features/Extensions/EntityModeMappers.cs b/LiteCharms.Features/Extensions/EntityModeMappers.cs index d73a028..9dce6e8 100644 --- a/LiteCharms.Features/Extensions/EntityModeMappers.cs +++ b/LiteCharms.Features/Extensions/EntityModeMappers.cs @@ -176,13 +176,15 @@ public static class EntityModeMappers public static Product ToModel(this Features.Shop.Products.Entities.Product entity) => new() { - Id = entity.Id, + Id = entity.Id, + CreatedAt = entity.CreatedAt, Name = entity.Name, Description = entity.Description, Active = entity.Active, Summary = entity.Summary, ImageUrl = entity.ImageUrl, - Thumbnails = entity.Thumbnails + Thumbnails = entity.Thumbnails, + Metadata = entity.Metadata, }; public static ProductPrice ToModel(this Features.Shop.Products.Entities.ProductPrice entity) => diff --git a/LiteCharms.Features/Shop/CartPackages/PackageService.cs b/LiteCharms.Features/Shop/CartPackages/PackageService.cs index 3479998..f2aeda5 100644 --- a/LiteCharms.Features/Shop/CartPackages/PackageService.cs +++ b/LiteCharms.Features/Shop/CartPackages/PackageService.cs @@ -2,7 +2,6 @@ using LiteCharms.Features.Models; using LiteCharms.Features.Shop.CartPackages.Models; using LiteCharms.Features.Shop.Postgres; -using static LiteCharms.Features.Extensions.Timezones; namespace LiteCharms.Features.Shop.CartPackages; diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs b/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs new file mode 100644 index 0000000..7ac8666 --- /dev/null +++ b/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs @@ -0,0 +1,788 @@ +// +using System; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Products.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.Shop.Postgres.Migrations +{ + [DbContext(typeof(ShopDbContext))] + [Migration("20260520191059_AddedProductMetadata")] + partial class AddedProductMetadata + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("PackageItem", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discord") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LinkedIn") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Region") + .HasColumnType("text"); + + b.Property("Slack") + .HasColumnType("text"); + + b.Property("Tax") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.Property("Whatsapp") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdGroupId") + .HasColumnType("bigint"); + + b.Property("AdName") + .HasColumnType("bigint"); + + b.Property("AppClickId") + .HasColumnType("text"); + + b.Property("AttributionHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("CampaignId") + .HasColumnType("bigint"); + + b.Property("ClickId") + .HasColumnType("text"); + + b.Property("ClickLocation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("FeedItemId") + .HasColumnType("bigint"); + + b.Property("Source") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TargetId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebClickId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Leads", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationIdType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsInternal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Processed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); + + b.Property("Status") + .HasColumnType("integer"); + + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderRefunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Metadata") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("Thumbnails") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductPrices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShoppingCartId") + .IsUnique(); + + b.ToTable("Quotes", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("ShoppingCarts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductPriceId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartPackages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany("PackageItems") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Leads") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product") + .WithMany("ProductPrices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Quotes") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId"); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId"); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("ShoppingCarts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("ShoppingCart") + .HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Customer"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartItems") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductPrice"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartPackages") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Navigation("PackageItems"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Navigation("Leads"); + + b.Navigation("Orders"); + + b.Navigation("Quotes"); + + b.Navigation("ShoppingCarts"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Navigation("Quote"); + + b.Navigation("Refunds"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Navigation("ProductPrices"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Navigation("Quote"); + + b.Navigation("ShoppingCartItems"); + + b.Navigation("ShoppingCartPackages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs b/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs new file mode 100644 index 0000000..142f394 --- /dev/null +++ b/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs @@ -0,0 +1,71 @@ +using System; +using LiteCharms.Features.Shop.Products.Models; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.Shop.Postgres.Migrations +{ + /// + public partial class AddedProductMetadata : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Active", + table: "Products", + type: "boolean", + nullable: false, + defaultValue: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: true); + + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "Products", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "Metadata", + table: "Products", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "Products", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "Products"); + + migrationBuilder.DropColumn( + name: "Metadata", + table: "Products"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "Products"); + + migrationBuilder.AlterColumn( + name: "Active", + table: "Products", + type: "boolean", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: false); + } + } +} diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs b/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs index 955c903..55c8c90 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs +++ b/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs @@ -1,6 +1,7 @@ // using System; using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Products.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -386,7 +387,12 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("Active") .ValueGeneratedOnAdd() .HasColumnType("boolean") - .HasDefaultValue(true); + .HasDefaultValue(false); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Description") .IsRequired() @@ -397,6 +403,9 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations .HasMaxLength(2048) .HasColumnType("character varying(2048)"); + b.Property("Metadata") + .HasColumnType("jsonb"); + b.Property("Name") .IsRequired() .HasColumnType("text"); @@ -409,6 +418,9 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.PrimitiveCollection("Thumbnails") .HasColumnType("jsonb"); + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + b.HasKey("Id"); b.ToTable("Products", (string)null); diff --git a/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs index 4307e62..6449acb 100644 --- a/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs @@ -8,10 +8,13 @@ public class ProductConfiguration : IEntityTypeConfiguration builder.HasKey(f => f.Id); builder.Property(f => f.Name).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); builder.Property(f => f.Description).IsRequired().HasMaxLength(2048); builder.Property(f => f.ImageUrl).IsRequired(false).HasMaxLength(2048); builder.Property(f => f.Thumbnails).HasColumnType("jsonb").IsRequired(false); - builder.Property(f => f.Active).HasDefaultValue(true); + builder.Property(f => f.Active).HasDefaultValue(false); + builder.Property(f => f.Metadata).HasColumnType("jsonb").IsRequired(false); } } diff --git a/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs b/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs index beab2ca..265dc6a 100644 --- a/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs +++ b/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs @@ -7,7 +7,7 @@ public class ProductPriceConfiguration : IEntityTypeConfiguration builder.ToTable("ProductPrices"); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.ProductId).IsRequired(); builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2); diff --git a/LiteCharms.Features/Shop/Products/Models/CreateProductModel.cs b/LiteCharms.Features/Shop/Products/Models/CreateProductModel.cs new file mode 100644 index 0000000..355347e --- /dev/null +++ b/LiteCharms.Features/Shop/Products/Models/CreateProductModel.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace LiteCharms.Features.Shop.Products.Models; + +public class CreateProductModel +{ + [MaxLength(128)] + [Required(ErrorMessage = "Product name is required.")] + public string? Name { get; set; } + + [MaxLength(512)] + [Required(ErrorMessage = "Summary is required.")] + public string? Summary { get; set; } + + [MaxLength(2048)] + [Required(ErrorMessage = "Description is required.")] + public string? Description { get; set; } + + [Range(0.01, double.MaxValue, ErrorMessage = "Price must be greater than zero.")] + public decimal Price { get; set; } + + [MaxLength(128)] + [Required(ErrorMessage = "Author metadata is required.")] + public string? Author { get; set; } + + [Required(ErrorMessage = "Publication Date is required.")] + public DateTime PublishDate { get; set; } = DateTime.Today; + + [MaxLength(255)] + [Required(ErrorMessage = "Copyright Information field is required.")] + public string? CopyrightInfo { get; set; } + + [MaxLength(128)] + [Required(ErrorMessage = "ISBN code is required.")] + [RegularExpression(@"^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$", ErrorMessage = "Please enter a valid ISBN-10 or ISBN-13 string.")] + public string? Isbn { get; set; } + + [Required(ErrorMessage = "Primary image is required.")] + public string? ImageUrl { get; set; } + + public List Thumbnails { get; set; } = []; +} diff --git a/LiteCharms.Features/Shop/Products/Models/Product.cs b/LiteCharms.Features/Shop/Products/Models/Product.cs index 8fdc778..e4e10d2 100644 --- a/LiteCharms.Features/Shop/Products/Models/Product.cs +++ b/LiteCharms.Features/Shop/Products/Models/Product.cs @@ -4,6 +4,10 @@ public class Product { public Guid Id { get; set; } + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + public string? Name { get; set; } public string? Summary { get; set; } @@ -15,4 +19,6 @@ public class Product public string[]? Thumbnails { get; set; } public bool Active { get; set; } + + public ProductMetadata? Metadata { get; set; } } diff --git a/LiteCharms.Features/Shop/Products/Models/ProductMetadata.cs b/LiteCharms.Features/Shop/Products/Models/ProductMetadata.cs new file mode 100644 index 0000000..da56f1a --- /dev/null +++ b/LiteCharms.Features/Shop/Products/Models/ProductMetadata.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.Shop.Products.Models; + +public class ProductMetadata +{ + public string? Manufacturer { get; set; } + + public string? ManufactureDate { get; set; } + + public string? CopyrightInfo { get; set; } + + public string? SerialNumber { get; set; } +} diff --git a/LiteCharms.Features/Shop/Products/Models/Records.cs b/LiteCharms.Features/Shop/Products/Models/Records.cs index b5579b8..8027e18 100644 --- a/LiteCharms.Features/Shop/Products/Models/Records.cs +++ b/LiteCharms.Features/Shop/Products/Models/Records.cs @@ -11,4 +11,6 @@ public record CreateProduct public required string ImageUrl { get; set; } public string[]? Thumbnails { get; set; } + + public ProductMetadata? Metadata { get; set; } } diff --git a/LiteCharms.Features/Shop/Products/ProductService.cs b/LiteCharms.Features/Shop/Products/ProductService.cs index 9016da5..685fb5c 100644 --- a/LiteCharms.Features/Shop/Products/ProductService.cs +++ b/LiteCharms.Features/Shop/Products/ProductService.cs @@ -67,10 +67,11 @@ public class ProductService(IDbContextFactory contextFactory) var newProduct = context.Products.Add(new Entities.Product { Name = request.Name, - Summary = request.Summary, + Summary = request.Summary, Description = request.Description, ImageUrl = request.ImageUrl, - Thumbnails = request.Thumbnails + Thumbnails = request.Thumbnails, + Metadata = request.Metadata }); return await context.SaveChangesAsync(cancellationToken) > 0 @@ -89,12 +90,12 @@ public class ProductService(IDbContextFactory contextFactory) try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - + var newProductPrice = context.ProductPrices.Add(new Entities.ProductPrice { Price = price, Discount = discount, - ProductId = productId + ProductId = productId }); return await context.SaveChangesAsync(cancellationToken) > 0 @@ -206,7 +207,7 @@ public class ProductService(IDbContextFactory contextFactory) var result = await CreateProductPriceAsync(existingPrice.ProductId, price, discount, cancellationToken); - if(result.IsFailed) + if (result.IsFailed) { var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); @@ -225,4 +226,52 @@ public class ProductService(IDbContextFactory contextFactory) return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } + + public async ValueTask SetProductPriceStatusAsync(Guid productPriceId, bool active, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var productPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + + if (productPrice is null) + return Result.Fail($"Could not find product price with ID {productPriceId}"); + + productPrice.Active = active; + productPrice.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to change product price status by ID {productPriceId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateProductMetadataAsync(Guid productId, ProductMetadata metadata, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + + if (product is null) + return Result.Fail($"Could not find product with ID {productId}"); + + product.Metadata = metadata; + product.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update product metadata by ID {productId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } } From 7d5e9a18d822d834d4909dd2564c4c22b10195ce Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 23 May 2026 11:48:47 +0200 Subject: [PATCH 046/153] Added MidrandShop feature and spl;it extensions and healthchecks --- .../ProductsFeatureTests.cs | 19 +++++++++++++ .../SendShopEmailEnquiryEventHandler.cs | 2 +- .../Email/Extensions/Constants.cs | 8 ------ LiteCharms.Features/Extensions/Email.cs | 5 +++- .../Extensions/HealthChecks.cs | 23 ++++++++++++--- LiteCharms.Features/Extensions/Postgres.cs | 17 +++++++++-- LiteCharms.Features/Extensions/Quartz.cs | 8 ++++-- .../MidrandShopQuartzHealthCheck.cs | 28 +++++++++++++++++++ .../PostgresMidrandShopHealthCheck.cs | 28 +++++++++++++++++++ ...lthCheck.cs => PostgresShopHealthCheck.cs} | 12 ++++---- .../HealthChecks/QuartzHealthCheck.cs | 23 --------------- .../HealthChecks/ShopQuartzHealthCheck.cs | 28 +++++++++++++++++++ .../Postgres/MidrandShopDbContext.cs | 6 ++++ .../Postgres/MidrandShopDbContextFactory.cs | 21 ++++++++++++++ .../Shop/Postgres/ShopDbContextFactory.cs | 6 ++-- 15 files changed, 185 insertions(+), 49 deletions(-) create mode 100644 LiteCharms.Features.Tests/ProductsFeatureTests.cs delete mode 100644 LiteCharms.Features/Email/Extensions/Constants.cs create mode 100644 LiteCharms.Features/HealthChecks/MidrandShopQuartzHealthCheck.cs create mode 100644 LiteCharms.Features/HealthChecks/PostgresMidrandShopHealthCheck.cs rename LiteCharms.Features/HealthChecks/{PostgresHealthCheck.cs => PostgresShopHealthCheck.cs} (62%) delete mode 100644 LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs create mode 100644 LiteCharms.Features/HealthChecks/ShopQuartzHealthCheck.cs create mode 100644 LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContext.cs create mode 100644 LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContextFactory.cs diff --git a/LiteCharms.Features.Tests/ProductsFeatureTests.cs b/LiteCharms.Features.Tests/ProductsFeatureTests.cs new file mode 100644 index 0000000..1aee1cf --- /dev/null +++ b/LiteCharms.Features.Tests/ProductsFeatureTests.cs @@ -0,0 +1,19 @@ +using LiteCharms.Features.Shop.Products; + +namespace LiteCharms.Features.Tests; + +public class ProductsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture +{ + [Fact] + public async Task GetProductsAsync_ReturnsProducts() + { + var productService = fixture.Services.GetRequiredService(); + + var result = await productService.GetProductsAsync(); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + + output.WriteLine($"Retrieved {result.Value.Length} products."); + } +} diff --git a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs index f4173a6..1b7fda9 100644 --- a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs +++ b/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs @@ -1,6 +1,6 @@ using LiteCharms.Features.Shop; using LiteCharms.Features.Shop.Notifications; -using static LiteCharms.Features.Email.Extensions.Constants; +using static LiteCharms.Features.Extensions.Email; namespace LiteCharms.Features.Email.Events.Handlers; diff --git a/LiteCharms.Features/Email/Extensions/Constants.cs b/LiteCharms.Features/Email/Extensions/Constants.cs deleted file mode 100644 index 1d02879..0000000 --- a/LiteCharms.Features/Email/Extensions/Constants.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LiteCharms.Features.Email.Extensions; - -public static class Constants -{ - public const string ShopSchedulerName = "shop"; - public const string ShopEmailFromName = "Khongisa Shop"; - public const string ShopEmailFromAddress = "shop@litecharms.co.za"; -} diff --git a/LiteCharms.Features/Extensions/Email.cs b/LiteCharms.Features/Extensions/Email.cs index ae3756d..d52f04a 100644 --- a/LiteCharms.Features/Extensions/Email.cs +++ b/LiteCharms.Features/Extensions/Email.cs @@ -4,7 +4,10 @@ using LiteCharms.Features.Email.Configuration; namespace LiteCharms.Features.Extensions; public static class Email -{ +{ + public const string ShopEmailFromName = "Khongisa Shop"; + public const string ShopEmailFromAddress = "shop@litecharms.co.za"; + public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration) { services.Configure(configuration.GetSection("Email")); diff --git a/LiteCharms.Features/Extensions/HealthChecks.cs b/LiteCharms.Features/Extensions/HealthChecks.cs index ebabd8f..f27502a 100644 --- a/LiteCharms.Features/Extensions/HealthChecks.cs +++ b/LiteCharms.Features/Extensions/HealthChecks.cs @@ -1,19 +1,34 @@ using LiteCharms.Features.HealthChecks; +using static LiteCharms.Features.Extensions.Postgres; namespace LiteCharms.Features.Extensions; public static class HealthChecks { - public static IServiceCollection AddQuartzHealtchCheck(this IServiceCollection services) + public static IServiceCollection AddShopQuartzHealthCheck(this IServiceCollection services) { - services.AddHealthChecks().AddCheck("Quartz"); + services.AddHealthChecks().AddCheck("ShopQuartz"); return services; } - public static IServiceCollection AddPostgresHealtchCheck(this IServiceCollection services) + public static IServiceCollection AddMidrandShopQuartzHealthCheck(this IServiceCollection services) { - services.AddHealthChecks().AddCheck("PostgreSQL"); + services.AddHealthChecks().AddCheck("MidrandShopQuartz"); + + return services; + } + + public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck(ShopDbConfigName); + + return services; + } + + public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck(MidrandShopDbConfigName); return services; } diff --git a/LiteCharms.Features/Extensions/Postgres.cs b/LiteCharms.Features/Extensions/Postgres.cs index 0e70287..982f5f9 100644 --- a/LiteCharms.Features/Extensions/Postgres.cs +++ b/LiteCharms.Features/Extensions/Postgres.cs @@ -1,13 +1,26 @@ -using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.MidrandShop.Postgres; +using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Extensions; public static class Postgres { + public const string MidrandShopDbConfigName = "PostgresMidrandShop"; + public const string ShopDbConfigName = "PostgresShop"; + public const string SchedulerDbConfigName = "PostgresScheduler"; + public static IServiceCollection AddShopDatabase(this IServiceCollection services, IConfiguration configuration) { services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString("PostgresShop"))); + options.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName))); + + return services; + } + + public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) + { + services.AddPooledDbContextFactory(options => + options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName))); return services; } diff --git a/LiteCharms.Features/Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs index fe6103a..7db12a8 100644 --- a/LiteCharms.Features/Extensions/Quartz.cs +++ b/LiteCharms.Features/Extensions/Quartz.cs @@ -1,15 +1,17 @@ using LiteCharms.Features.Quartz; using LiteCharms.Features.Quartz.Abstractions; +using static LiteCharms.Features.Extensions.Postgres; namespace LiteCharms.Features.Extensions; public static class Quartz { - private const string databaseConfigName = "PostgresScheduler"; + public const string ShopSchedulerName = "shop"; + public const string MidrandShopSchedulerName = "midrandshop"; public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration) { - var connectionString = configuration.GetConnectionString(databaseConfigName); + var connectionString = configuration.GetConnectionString(SchedulerDbConfigName); services.ConfigureCommon(); @@ -44,7 +46,7 @@ public static class Quartz public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration) { - var connectionString = configuration.GetConnectionString(databaseConfigName); + var connectionString = configuration.GetConnectionString(SchedulerDbConfigName); services.ConfigureCommon(); diff --git a/LiteCharms.Features/HealthChecks/MidrandShopQuartzHealthCheck.cs b/LiteCharms.Features/HealthChecks/MidrandShopQuartzHealthCheck.cs new file mode 100644 index 0000000..9588d24 --- /dev/null +++ b/LiteCharms.Features/HealthChecks/MidrandShopQuartzHealthCheck.cs @@ -0,0 +1,28 @@ +using static LiteCharms.Features.Extensions.Quartz; + +namespace LiteCharms.Features.HealthChecks; + +public class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck +{ + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var scheduler = await schedulerFactory.GetScheduler(MidrandShopSchedulerName, cancellationToken); + + if(scheduler == null) + return HealthCheckResult.Unhealthy($"Scheduler with name '{MidrandShopSchedulerName}' not found."); + + if (!scheduler.IsStarted) + return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler is not running"); + + await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken); + + return HealthCheckResult.Healthy($"{MidrandShopSchedulerName} Quartz scheduler is ready"); + } + catch (SchedulerException) + { + return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler cannot connect to the store"); + } + } +} diff --git a/LiteCharms.Features/HealthChecks/PostgresMidrandShopHealthCheck.cs b/LiteCharms.Features/HealthChecks/PostgresMidrandShopHealthCheck.cs new file mode 100644 index 0000000..f88b22e --- /dev/null +++ b/LiteCharms.Features/HealthChecks/PostgresMidrandShopHealthCheck.cs @@ -0,0 +1,28 @@ +using static LiteCharms.Features.Extensions.Postgres; + +namespace LiteCharms.Features.HealthChecks; + +public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck +{ + private readonly string connectionString = configuration.GetConnectionString(MidrandShopDbConfigName)!; + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + await using var dataSource = NpgsqlDataSource.Create(connectionString); + await using var connection = await dataSource.OpenConnectionAsync(cancellationToken); + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT 1"; + + await command.ExecuteScalarAsync(cancellationToken); + + return HealthCheckResult.Healthy($"{MidrandShopDbConfigName} is responsive."); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy($"{MidrandShopDbConfigName} is unreachable.", ex); + } + } +} \ No newline at end of file diff --git a/LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs b/LiteCharms.Features/HealthChecks/PostgresShopHealthCheck.cs similarity index 62% rename from LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs rename to LiteCharms.Features/HealthChecks/PostgresShopHealthCheck.cs index 377da08..4a65640 100644 --- a/LiteCharms.Features/HealthChecks/PostgresHealthCheck.cs +++ b/LiteCharms.Features/HealthChecks/PostgresShopHealthCheck.cs @@ -1,8 +1,10 @@ -namespace LiteCharms.Features.HealthChecks; +using static LiteCharms.Features.Extensions.Postgres; -public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck +namespace LiteCharms.Features.HealthChecks; + +public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthCheck { - private readonly string connectionString = configuration.GetConnectionString("PostgresShop")!; + private readonly string connectionString = configuration.GetConnectionString(ShopDbConfigName)!; public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { @@ -16,11 +18,11 @@ public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck await command.ExecuteScalarAsync(cancellationToken); - return HealthCheckResult.Healthy("PostgreSQL is responsive."); + return HealthCheckResult.Healthy($"{ShopDbConfigName} is responsive."); } catch (Exception ex) { - return HealthCheckResult.Unhealthy("PostgreSQL is unreachable.", ex); + return HealthCheckResult.Unhealthy($"{ShopDbConfigName} is unreachable.", ex); } } } \ No newline at end of file diff --git a/LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs b/LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs deleted file mode 100644 index 59eb397..0000000 --- a/LiteCharms.Features/HealthChecks/QuartzHealthCheck.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace LiteCharms.Features.HealthChecks; - -public class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck -{ - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - try - { - var scheduler = await schedulerFactory.GetScheduler(cancellationToken); - - if (!scheduler.IsStarted) - return HealthCheckResult.Unhealthy("Quartz scheduler is not running"); - - await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken); - - return HealthCheckResult.Healthy("Quartz scheduler is ready"); - } - catch (SchedulerException) - { - return HealthCheckResult.Unhealthy("Quartz scheduler cannot connect to the store"); - } - } -} diff --git a/LiteCharms.Features/HealthChecks/ShopQuartzHealthCheck.cs b/LiteCharms.Features/HealthChecks/ShopQuartzHealthCheck.cs new file mode 100644 index 0000000..0ccae12 --- /dev/null +++ b/LiteCharms.Features/HealthChecks/ShopQuartzHealthCheck.cs @@ -0,0 +1,28 @@ +using static LiteCharms.Features.Extensions.Quartz; + +namespace LiteCharms.Features.HealthChecks; + +public class ShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck +{ + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var scheduler = await schedulerFactory.GetScheduler(ShopSchedulerName, cancellationToken); + + if(scheduler == null) + return HealthCheckResult.Unhealthy($"Scheduler with name '{ShopSchedulerName}' not found."); + + if (!scheduler.IsStarted) + return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler is not running"); + + await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken); + + return HealthCheckResult.Healthy($"{ShopSchedulerName} Quartz scheduler is ready"); + } + catch (SchedulerException) + { + return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler cannot connect to the store"); + } + } +} diff --git a/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContext.cs b/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContext.cs new file mode 100644 index 0000000..efa8047 --- /dev/null +++ b/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContext.cs @@ -0,0 +1,6 @@ +namespace LiteCharms.Features.MidrandShop.Postgres; + +public class MidrandShopDbContext(DbContextOptions options) : DbContext(options) +{ + +} diff --git a/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContextFactory.cs b/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContextFactory.cs new file mode 100644 index 0000000..deb8ff4 --- /dev/null +++ b/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContextFactory.cs @@ -0,0 +1,21 @@ +using static LiteCharms.Features.Extensions.Postgres; + +namespace LiteCharms.Features.MidrandShop.Postgres; + +public class MidrandShopDbContextFactory : IDesignTimeDbContextFactory +{ + public MidrandShopDbContext CreateDbContext(string[] args) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddUserSecrets(typeof(MidrandShopDbContext).Assembly) + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables() + .Build(); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName)); + + return new MidrandShopDbContext(optionsBuilder.Options); + } +} diff --git a/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs b/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs index b8437d9..b8e61dc 100644 --- a/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs +++ b/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.Shop.Postgres; +using static LiteCharms.Features.Extensions.Postgres; + +namespace LiteCharms.Features.Shop.Postgres; public class ShopDbContextFactory : IDesignTimeDbContextFactory { @@ -12,7 +14,7 @@ public class ShopDbContextFactory : IDesignTimeDbContextFactory .Build(); var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgresShop")); + optionsBuilder.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName)); return new ShopDbContext(optionsBuilder.Options); } From 424c1c6f8c0f64f00d051d978a8985f9e13e9628 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 23 May 2026 11:52:47 +0200 Subject: [PATCH 047/153] Fixed email test --- LiteCharms.Features.Tests/NotificationsFeatureTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs index 05b9f9f..2b500e8 100644 --- a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs +++ b/LiteCharms.Features.Tests/NotificationsFeatureTests.cs @@ -1,6 +1,7 @@ using LiteCharms.Features.Models; using LiteCharms.Features.Shop.Notifications; using LiteCharms.Features.Shop.Notifications.Events; +using static LiteCharms.Features.Extensions.Email; namespace LiteCharms.Features.Tests; @@ -20,8 +21,8 @@ public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper Priority = Shop.Priorities.Medium, Sender = "xUnit Test", SenderAddress = "khwezi@mngoma.africa", - Recipient = $"{Email.Extensions.Constants.ShopEmailFromName} [Test]", - RecipientAddress = Email.Extensions.Constants.ShopEmailFromAddress, + Recipient = $"{ShopEmailFromName} [Test]", + RecipientAddress = ShopEmailFromAddress, Subject = "Test Message", Message = "This is an automation test", IsHtml = false, From 032b9e18189c72c36cea86c22bda1c79eb3277f0 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 23 May 2026 11:54:32 +0200 Subject: [PATCH 048/153] Updated nuget packages --- LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj | 2 +- LiteCharms.Features/LiteCharms.Features.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj index 55f1b2e..6f93280 100644 --- a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj +++ b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index ab20ae2..99f18de 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -131,8 +131,8 @@ - - + + From 70c6e0bfbc591cb123c08f6d36f66683e5eae803 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 24 May 2026 13:19:09 +0200 Subject: [PATCH 049/153] Split Features to create space for more projects --- .drone.yml | 4 +- LiteCharms.Features.TechShop.Tests/Fixture.cs | 38 ++++ .../LiteCharms.Features.TechShop.Tests.csproj | 57 ++++++ .../NotificationsFeatureTests.cs | 18 +- .../ProductsFeatureTests.cs | 6 +- .../appsettings.json | 34 ++++ .../CartPackages/Entities/Package.cs | 2 +- .../Entities/PackageConfirguration.cs | 2 +- .../CartPackages/Entities/PackageItem.cs | 4 +- .../Entities/PackageItemConfiguration.cs | 2 +- .../CartPackages/Models/Package.cs | 2 +- .../CartPackages/Models/PackageItem.cs | 2 +- .../CartPackages/PackageService.cs | 7 +- .../Customers/CustomerService.cs | 7 +- .../Customers/Entities/Customer.cs | 10 +- .../Entities/CustomerConfiguration.cs | 2 +- .../Customers/Models/Customer.cs | 2 +- .../Customers/Models/Records.cs | 2 +- .../Enums.cs | 21 +-- .../Extensions/HealthChecks.cs | 22 +++ .../Extensions/Mappers.cs | 47 ++--- .../Extensions/Postgres.cs | 16 ++ .../Extensions/Shop.cs | 27 +++ .../HealthChecks/PostgresShopHealthCheck.cs | 4 +- .../HealthChecks/ShopQuartzHealthCheck.cs | 2 +- .../Leads/Entities/Lead.cs | 4 +- .../Leads/Entities/LeadConfiguration.cs | 2 +- .../Leads/LeadService.cs | 7 +- .../Leads/Models/Lead.cs | 2 +- .../Leads/Models/Records.cs | 2 +- .../LiteCharms.Features.TechShop.csproj | 166 ++++++++++++++++++ .../Notifications/Entities/Notification.cs | 2 +- .../Entities/NotificationConfiguration.cs | 2 +- .../ProcessEmailNotificationsEventHandler.cs | 9 +- .../SendShopEmailEnquiryEventHandler.cs | 8 +- .../Events/ProcessEmailNotificationsEvent.cs | 2 +- .../Events/SendShopEmailEnquiryEvent.cs | 3 +- .../Notifications/Models/Notification.cs | 4 +- .../Notifications/Models/Records.cs | 2 +- .../Notifications/NotificationService.cs | 7 +- .../Orders/Entities/Order.cs | 8 +- .../Orders/Entities/OrderConfiguration.cs | 2 +- .../Orders/Entities/OrderRefund.cs | 2 +- .../Entities/OrderRefundConfiguration.cs | 2 +- .../Orders/Models/Order.cs | 4 +- .../Orders/Models/OrderRefund.cs | 2 +- .../Orders/Models/Records.cs | 4 +- .../Orders/OrderService.cs | 7 +- .../20260512065421_Init.Designer.cs | 4 +- .../Migrations/20260512065421_Init.cs | 2 +- ...514004002_UsedStringTableNames.Designer.cs | 4 +- .../20260514004002_UsedStringTableNames.cs | 2 +- ..._FixedLeadCustomerRelationship.Designer.cs | 4 +- ...515055221_FixedLeadCustomerRelationship.cs | 2 +- ...520191059_AddedProductMetadata.Designer.cs | 6 +- .../20260520191059_AddedProductMetadata.cs | 4 +- .../Migrations/ShopDbContextModelSnapshot.cs | 6 +- .../Postgres/ShopDbContext.cs | 39 ++++ .../Postgres/ShopDbContextFactory.cs | 4 +- .../Products/Entities/Product.cs | 2 +- .../Products/Entities/ProductConfiguration.cs | 2 +- .../Products/Entities/ProductPrice.cs | 2 +- .../Entities/ProductPriceConfiguration.cs | 2 +- .../Products/Models/CreateProductModel.cs | 2 +- .../Products/Models/Product.cs | 2 +- .../Products/Models/ProductMetadata.cs | 2 +- .../Products/Models/ProductPrice.cs | 2 +- .../Products/Models/Records.cs | 2 +- .../Products/ProductService.cs | 8 +- .../Quotes/Entities/Quote.cs | 8 +- .../Quotes/Entities/QuoteConfiguration.cs | 2 +- .../Quotes/Models/Quote.cs | 2 +- .../Quotes/QuoteService.cs | 7 +- .../ShoppingCarts/Entities/ShoppingCart.cs | 8 +- .../Entities/ShoppingCartConfiguration.cs | 2 +- .../Entities/ShoppingCartItem.cs | 4 +- .../Entities/ShoppingCartItemConfiguration.cs | 2 +- .../Entities/ShoppingCartPackage.cs | 4 +- .../ShoppingCartPackageConfiguration.cs | 2 +- .../ShoppingCarts/Models/ShoppingCart.cs | 2 +- .../ShoppingCarts/Models/ShoppingCartItem.cs | 2 +- .../Models/ShoppingCartPackage.cs | 2 +- .../ShoppingCarts/ShoppingCartService.cs | 8 +- LiteCharms.Features.TechShop/appsettings.json | 22 +++ .../{CommonFixture.cs => Fixture.cs} | 10 +- .../S3ServiceFeatureTests.cs | 2 +- LiteCharms.Features/Email/EmailService.cs | 1 - LiteCharms.Features/Email/Models/Response.cs | 4 +- LiteCharms.Features/Enums.cs | 20 +++ .../Extensions/HealthChecks.cs | 14 -- LiteCharms.Features/Extensions/Postgres.cs | 10 -- LiteCharms.Features/Extensions/Shop.cs | 27 --- .../LiteCharms.Features.csproj | 3 - .../Shop/Postgres/ShopDbContext.cs | 69 -------- LiteCharmsShared.slnx | 2 + 95 files changed, 621 insertions(+), 314 deletions(-) create mode 100644 LiteCharms.Features.TechShop.Tests/Fixture.cs create mode 100644 LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj rename {LiteCharms.Features.Tests => LiteCharms.Features.TechShop.Tests}/NotificationsFeatureTests.cs (75%) rename {LiteCharms.Features.Tests => LiteCharms.Features.TechShop.Tests}/ProductsFeatureTests.cs (66%) create mode 100644 LiteCharms.Features.TechShop.Tests/appsettings.json rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/CartPackages/Entities/Package.cs (72%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/CartPackages/Entities/PackageConfirguration.cs (91%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/CartPackages/Entities/PackageItem.cs (65%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/CartPackages/Entities/PackageItemConfiguration.cs (93%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/CartPackages/Models/Package.cs (85%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/CartPackages/Models/PackageItem.cs (79%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/CartPackages/PackageService.cs (97%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Customers/CustomerService.cs (96%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Customers/Entities/Customer.cs (56%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Customers/Entities/CustomerConfiguration.cs (94%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Customers/Models/Customer.cs (93%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Customers/Models/Records.cs (96%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Enums.cs (71%) create mode 100644 LiteCharms.Features.TechShop/Extensions/HealthChecks.cs rename LiteCharms.Features/Extensions/EntityModeMappers.cs => LiteCharms.Features.TechShop/Extensions/Mappers.cs (75%) create mode 100644 LiteCharms.Features.TechShop/Extensions/Postgres.cs create mode 100644 LiteCharms.Features.TechShop/Extensions/Shop.cs rename {LiteCharms.Features => LiteCharms.Features.TechShop}/HealthChecks/PostgresShopHealthCheck.cs (88%) rename {LiteCharms.Features => LiteCharms.Features.TechShop}/HealthChecks/ShopQuartzHealthCheck.cs (95%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Leads/Entities/Lead.cs (55%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Leads/Entities/LeadConfiguration.cs (95%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Leads/LeadService.cs (96%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Leads/Models/Lead.cs (92%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Leads/Models/Records.cs (91%) create mode 100644 LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Notifications/Entities/Notification.cs (63%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Notifications/Entities/NotificationConfiguration.cs (95%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs (94%) rename {LiteCharms.Features/Email => LiteCharms.Features.TechShop/Notifications}/Events/Handlers/SendShopEmailEnquiryEventHandler.cs (75%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Notifications/Events/ProcessEmailNotificationsEvent.cs (89%) rename {LiteCharms.Features/Email => LiteCharms.Features.TechShop/Notifications}/Events/SendShopEmailEnquiryEvent.cs (94%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Notifications/Models/Notification.cs (89%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Notifications/Models/Records.cs (93%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Notifications/NotificationService.cs (95%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Orders/Entities/Order.cs (58%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Orders/Entities/OrderConfiguration.cs (94%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Orders/Entities/OrderRefund.cs (73%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Orders/Entities/OrderRefundConfiguration.cs (92%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Orders/Models/Order.cs (81%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Orders/Models/OrderRefund.cs (80%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Orders/Models/Records.cs (89%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Orders/OrderService.cs (98%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Postgres/Migrations/20260512065421_Init.Designer.cs (99%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Postgres/Migrations/20260512065421_Init.cs (99%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs (99%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Postgres/Migrations/20260514004002_UsedStringTableNames.cs (99%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs (99%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs (99%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs (99%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Postgres/Migrations/20260520191059_AddedProductMetadata.cs (94%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Postgres/Migrations/ShopDbContextModelSnapshot.cs (99%) create mode 100644 LiteCharms.Features.TechShop/Postgres/ShopDbContext.cs rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Postgres/ShopDbContextFactory.cs (85%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Products/Entities/Product.cs (74%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Products/Entities/ProductConfiguration.cs (93%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Products/Entities/ProductPrice.cs (73%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Products/Entities/ProductPriceConfiguration.cs (93%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Products/Models/CreateProductModel.cs (96%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Products/Models/Product.cs (88%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Products/Models/ProductMetadata.cs (79%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Products/Models/ProductPrice.cs (84%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Products/Models/Records.cs (84%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Products/ProductService.cs (98%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Quotes/Entities/Quote.cs (52%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Quotes/Entities/QuoteConfiguration.cs (95%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Quotes/Models/Quote.cs (88%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/Quotes/QuoteService.cs (96%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/ShoppingCarts/Entities/ShoppingCart.cs (66%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/ShoppingCarts/Entities/ShoppingCartConfiguration.cs (93%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/ShoppingCarts/Entities/ShoppingCartItem.cs (68%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs (94%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/ShoppingCarts/Entities/ShoppingCartPackage.cs (67%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs (93%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/ShoppingCarts/Models/ShoppingCart.cs (78%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/ShoppingCarts/Models/ShoppingCartItem.cs (81%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/ShoppingCarts/Models/ShoppingCartPackage.cs (76%) rename {LiteCharms.Features/Shop => LiteCharms.Features.TechShop}/ShoppingCarts/ShoppingCartService.cs (98%) create mode 100644 LiteCharms.Features.TechShop/appsettings.json rename LiteCharms.Features.Tests/{CommonFixture.cs => Fixture.cs} (81%) create mode 100644 LiteCharms.Features/Enums.cs delete mode 100644 LiteCharms.Features/Extensions/Shop.cs delete mode 100644 LiteCharms.Features/Shop/Postgres/ShopDbContext.cs diff --git a/.drone.yml b/.drone.yml index d5b9121..49e0b2b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -19,6 +19,8 @@ steps: commands: - dotnet pack LiteCharms.Features/LiteCharms.Features.csproj -c Release -p:PackageVersion=$VERSION -o dist/ - dotnet nuget push dist/LiteCharms.Features.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + - dotnet pack LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj -c Release -p:PackageVersion=$VERSION -o dist/ + - dotnet nuget push dist/LiteCharms.Features.TechShop.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL - name: gitea-tag-release image: alpine/git @@ -41,7 +43,7 @@ steps: \"tag_name\": \"$VERSION\", \"target_commitish\": \"${DRONE_COMMIT_SHA}\", \"name\": \"Library Suite $VERSION\", - \"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Features\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\", + \"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Features\n* LiteCharms.Features.TechShop\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\", \"draft\": false, \"prerelease\": false }" diff --git a/LiteCharms.Features.TechShop.Tests/Fixture.cs b/LiteCharms.Features.TechShop.Tests/Fixture.cs new file mode 100644 index 0000000..eadca78 --- /dev/null +++ b/LiteCharms.Features.TechShop.Tests/Fixture.cs @@ -0,0 +1,38 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.TechShop.Extensions; + +namespace LiteCharms.Features.TechShop.Tests; + +public class Fixture : IDisposable +{ + public IConfiguration Configuration { get; set; } + + public IServiceProvider Services { get; set; } + + public IMediator Mediator { get; set; } + + public Fixture() + { + Configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddUserSecrets() + .AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .Build(); + + Services = new ServiceCollection() + .AddMediator() + .AddLogging() + .AddTechShopServices() + .AddEmailServiceBus() + .AddGarageS3(Configuration) + .AddTechShopDatabase(Configuration) + .AddEmailServices(Configuration) + .AddSingleton(Configuration) + .BuildServiceProvider(); + + Mediator = Services.GetRequiredService(); + } + + public void Dispose() { } +} diff --git a/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj b/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj new file mode 100644 index 0000000..5c64f32 --- /dev/null +++ b/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj @@ -0,0 +1,57 @@ + + + + net10.0 + enable + enable + false + fa06c1a6-2ba8-4c47-b19b-11e0f78e7b92 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + Always + + + + \ No newline at end of file diff --git a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs b/LiteCharms.Features.TechShop.Tests/NotificationsFeatureTests.cs similarity index 75% rename from LiteCharms.Features.Tests/NotificationsFeatureTests.cs rename to LiteCharms.Features.TechShop.Tests/NotificationsFeatureTests.cs index 2b500e8..6a447fa 100644 --- a/LiteCharms.Features.Tests/NotificationsFeatureTests.cs +++ b/LiteCharms.Features.TechShop.Tests/NotificationsFeatureTests.cs @@ -1,24 +1,24 @@ using LiteCharms.Features.Models; -using LiteCharms.Features.Shop.Notifications; -using LiteCharms.Features.Shop.Notifications.Events; +using LiteCharms.Features.TechShop.Notifications; +using LiteCharms.Features.TechShop.Notifications.Events; using static LiteCharms.Features.Extensions.Email; -namespace LiteCharms.Features.Tests; +namespace LiteCharms.Features.TechShop.Tests; -public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture +public class NotificationsFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture { private readonly NotificationService notificationService = fixture.Services.GetRequiredService(); [Fact] public async Task CreateNotificationCommand_ShouldSucceed() { - Shop.Notifications.Models.CreateNotification request = new() + Notifications.Models.CreateNotification request = new() { CorrelationId = Guid.CreateVersion7().ToString(), - CorrelationIdType = Shop.CorrelationIdTypes.None, - Direction = Shop.NotificationDirection.Outgoing, - Platform = Shop.NotificationPlatforms.Email, - Priority = Shop.Priorities.Medium, + CorrelationIdType = CorrelationIdTypes.None, + Direction = NotificationDirection.Outgoing, + Platform = NotificationPlatforms.Email, + Priority = Priorities.Medium, Sender = "xUnit Test", SenderAddress = "khwezi@mngoma.africa", Recipient = $"{ShopEmailFromName} [Test]", diff --git a/LiteCharms.Features.Tests/ProductsFeatureTests.cs b/LiteCharms.Features.TechShop.Tests/ProductsFeatureTests.cs similarity index 66% rename from LiteCharms.Features.Tests/ProductsFeatureTests.cs rename to LiteCharms.Features.TechShop.Tests/ProductsFeatureTests.cs index 1aee1cf..2386847 100644 --- a/LiteCharms.Features.Tests/ProductsFeatureTests.cs +++ b/LiteCharms.Features.TechShop.Tests/ProductsFeatureTests.cs @@ -1,8 +1,8 @@ -using LiteCharms.Features.Shop.Products; +using LiteCharms.Features.TechShop.Products; -namespace LiteCharms.Features.Tests; +namespace LiteCharms.Features.TechShop.Tests; -public class ProductsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture +public class ProductsFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture { [Fact] public async Task GetProductsAsync_ReturnsProducts() diff --git a/LiteCharms.Features.TechShop.Tests/appsettings.json b/LiteCharms.Features.TechShop.Tests/appsettings.json new file mode 100644 index 0000000..1066af9 --- /dev/null +++ b/LiteCharms.Features.TechShop.Tests/appsettings.json @@ -0,0 +1,34 @@ +{ + "BookshopS3Settings": { + "ServiceUrl": "http://192.168.1.177:30900", + "Region": "garage", + "BucketName": "bookshop", + "CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za" + }, + "BookshopQuotesS3Settings": { + "ServiceUrl": "http://192.168.1.177:30900", + "Region": "garage", + "BucketName": "bookshop.quotes", + "CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za" + }, + "Email": { + "Credentials": { + "Username": "shop@litecharms.co.za" + }, + "Port": 465, + "Host": "mail.litecharms.co.za", + "UseSsl": true + }, + "Monitoring": { + "ApiKey": "", + "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", + "ServiceName": "LiteCharms.LeadGenerator" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/LiteCharms.Features/Shop/CartPackages/Entities/Package.cs b/LiteCharms.Features.TechShop/CartPackages/Entities/Package.cs similarity index 72% rename from LiteCharms.Features/Shop/CartPackages/Entities/Package.cs rename to LiteCharms.Features.TechShop/CartPackages/Entities/Package.cs index 3e4d210..147b6f0 100644 --- a/LiteCharms.Features/Shop/CartPackages/Entities/Package.cs +++ b/LiteCharms.Features.TechShop/CartPackages/Entities/Package.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.CartPackages.Entities; +namespace LiteCharms.Features.TechShop.CartPackages.Entities; [EntityTypeConfiguration] public class Package : Models.Package diff --git a/LiteCharms.Features/Shop/CartPackages/Entities/PackageConfirguration.cs b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageConfirguration.cs similarity index 91% rename from LiteCharms.Features/Shop/CartPackages/Entities/PackageConfirguration.cs rename to LiteCharms.Features.TechShop/CartPackages/Entities/PackageConfirguration.cs index cfc89ff..3f1b9dd 100644 --- a/LiteCharms.Features/Shop/CartPackages/Entities/PackageConfirguration.cs +++ b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageConfirguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.CartPackages.Entities; +namespace LiteCharms.Features.TechShop.CartPackages.Entities; public class PackageConfirguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/CartPackages/Entities/PackageItem.cs b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageItem.cs similarity index 65% rename from LiteCharms.Features/Shop/CartPackages/Entities/PackageItem.cs rename to LiteCharms.Features.TechShop/CartPackages/Entities/PackageItem.cs index 306975e..8a8715f 100644 --- a/LiteCharms.Features/Shop/CartPackages/Entities/PackageItem.cs +++ b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageItem.cs @@ -1,6 +1,6 @@ -using LiteCharms.Features.Shop.Products.Entities; +using LiteCharms.Features.TechShop.Products.Entities; -namespace LiteCharms.Features.Shop.CartPackages.Entities; +namespace LiteCharms.Features.TechShop.CartPackages.Entities; [EntityTypeConfiguration] public class PackageItem : Models.PackageItem diff --git a/LiteCharms.Features/Shop/CartPackages/Entities/PackageItemConfiguration.cs b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageItemConfiguration.cs similarity index 93% rename from LiteCharms.Features/Shop/CartPackages/Entities/PackageItemConfiguration.cs rename to LiteCharms.Features.TechShop/CartPackages/Entities/PackageItemConfiguration.cs index c6f1009..c77222a 100644 --- a/LiteCharms.Features/Shop/CartPackages/Entities/PackageItemConfiguration.cs +++ b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageItemConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.CartPackages.Entities; +namespace LiteCharms.Features.TechShop.CartPackages.Entities; public class PackageItemConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/CartPackages/Models/Package.cs b/LiteCharms.Features.TechShop/CartPackages/Models/Package.cs similarity index 85% rename from LiteCharms.Features/Shop/CartPackages/Models/Package.cs rename to LiteCharms.Features.TechShop/CartPackages/Models/Package.cs index 9d2e8b0..10f8224 100644 --- a/LiteCharms.Features/Shop/CartPackages/Models/Package.cs +++ b/LiteCharms.Features.TechShop/CartPackages/Models/Package.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.CartPackages.Models; +namespace LiteCharms.Features.TechShop.CartPackages.Models; public class Package { diff --git a/LiteCharms.Features/Shop/CartPackages/Models/PackageItem.cs b/LiteCharms.Features.TechShop/CartPackages/Models/PackageItem.cs similarity index 79% rename from LiteCharms.Features/Shop/CartPackages/Models/PackageItem.cs rename to LiteCharms.Features.TechShop/CartPackages/Models/PackageItem.cs index ff81c0a..795b759 100644 --- a/LiteCharms.Features/Shop/CartPackages/Models/PackageItem.cs +++ b/LiteCharms.Features.TechShop/CartPackages/Models/PackageItem.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.CartPackages.Models; +namespace LiteCharms.Features.TechShop.CartPackages.Models; public class PackageItem { diff --git a/LiteCharms.Features/Shop/CartPackages/PackageService.cs b/LiteCharms.Features.TechShop/CartPackages/PackageService.cs similarity index 97% rename from LiteCharms.Features/Shop/CartPackages/PackageService.cs rename to LiteCharms.Features.TechShop/CartPackages/PackageService.cs index f2aeda5..da2f986 100644 --- a/LiteCharms.Features/Shop/CartPackages/PackageService.cs +++ b/LiteCharms.Features.TechShop/CartPackages/PackageService.cs @@ -1,9 +1,10 @@ using LiteCharms.Features.Extensions; using LiteCharms.Features.Models; -using LiteCharms.Features.Shop.CartPackages.Models; -using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.TechShop.CartPackages.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Postgres; -namespace LiteCharms.Features.Shop.CartPackages; +namespace LiteCharms.Features.TechShop.CartPackages; public class PackageService(IDbContextFactory contextFactory) { diff --git a/LiteCharms.Features/Shop/Customers/CustomerService.cs b/LiteCharms.Features.TechShop/Customers/CustomerService.cs similarity index 96% rename from LiteCharms.Features/Shop/Customers/CustomerService.cs rename to LiteCharms.Features.TechShop/Customers/CustomerService.cs index 531ecfe..c8ea2cd 100644 --- a/LiteCharms.Features/Shop/Customers/CustomerService.cs +++ b/LiteCharms.Features.TechShop/Customers/CustomerService.cs @@ -1,9 +1,10 @@ using LiteCharms.Features.Extensions; using LiteCharms.Features.Models; -using LiteCharms.Features.Shop.Customers.Models; -using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.TechShop.Customers.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Postgres; -namespace LiteCharms.Features.Shop.Customers; +namespace LiteCharms.Features.TechShop.Customers; public class CustomerService(IDbContextFactory contextFactory) { diff --git a/LiteCharms.Features/Shop/Customers/Entities/Customer.cs b/LiteCharms.Features.TechShop/Customers/Entities/Customer.cs similarity index 56% rename from LiteCharms.Features/Shop/Customers/Entities/Customer.cs rename to LiteCharms.Features.TechShop/Customers/Entities/Customer.cs index 7b78bc8..d021a92 100644 --- a/LiteCharms.Features/Shop/Customers/Entities/Customer.cs +++ b/LiteCharms.Features.TechShop/Customers/Entities/Customer.cs @@ -1,9 +1,9 @@ -using LiteCharms.Features.Shop.Leads.Entities; -using LiteCharms.Features.Shop.Orders.Entities; -using LiteCharms.Features.Shop.Quotes.Entities; -using LiteCharms.Features.Shop.ShoppingCarts.Entities; +using LiteCharms.Features.TechShop.Leads.Entities; +using LiteCharms.Features.TechShop.Orders.Entities; +using LiteCharms.Features.TechShop.Quotes.Entities; +using LiteCharms.Features.TechShop.ShoppingCarts.Entities; -namespace LiteCharms.Features.Shop.Customers.Entities; +namespace LiteCharms.Features.TechShop.Customers.Entities; [EntityTypeConfiguration] public class Customer : Models.Customer diff --git a/LiteCharms.Features/Shop/Customers/Entities/CustomerConfiguration.cs b/LiteCharms.Features.TechShop/Customers/Entities/CustomerConfiguration.cs similarity index 94% rename from LiteCharms.Features/Shop/Customers/Entities/CustomerConfiguration.cs rename to LiteCharms.Features.TechShop/Customers/Entities/CustomerConfiguration.cs index f5ce014..e6a23a0 100644 --- a/LiteCharms.Features/Shop/Customers/Entities/CustomerConfiguration.cs +++ b/LiteCharms.Features.TechShop/Customers/Entities/CustomerConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Customers.Entities; +namespace LiteCharms.Features.TechShop.Customers.Entities; public class CustomerConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/Customers/Models/Customer.cs b/LiteCharms.Features.TechShop/Customers/Models/Customer.cs similarity index 93% rename from LiteCharms.Features/Shop/Customers/Models/Customer.cs rename to LiteCharms.Features.TechShop/Customers/Models/Customer.cs index d4cec8c..2904a3e 100644 --- a/LiteCharms.Features/Shop/Customers/Models/Customer.cs +++ b/LiteCharms.Features.TechShop/Customers/Models/Customer.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Customers.Models; +namespace LiteCharms.Features.TechShop.Customers.Models; public class Customer { diff --git a/LiteCharms.Features/Shop/Customers/Models/Records.cs b/LiteCharms.Features.TechShop/Customers/Models/Records.cs similarity index 96% rename from LiteCharms.Features/Shop/Customers/Models/Records.cs rename to LiteCharms.Features.TechShop/Customers/Models/Records.cs index 0b4c788..35c8e45 100644 --- a/LiteCharms.Features/Shop/Customers/Models/Records.cs +++ b/LiteCharms.Features.TechShop/Customers/Models/Records.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Customers.Models; +namespace LiteCharms.Features.TechShop.Customers.Models; public record CreateCustomer { diff --git a/LiteCharms.Features/Shop/Enums.cs b/LiteCharms.Features.TechShop/Enums.cs similarity index 71% rename from LiteCharms.Features/Shop/Enums.cs rename to LiteCharms.Features.TechShop/Enums.cs index 6f25546..d228c10 100644 --- a/LiteCharms.Features/Shop/Enums.cs +++ b/LiteCharms.Features.TechShop/Enums.cs @@ -1,16 +1,4 @@ -namespace LiteCharms.Features.Shop; - -public enum EmailStatuses : int -{ - GeneralError = 0, - AuthenticationError = 1, - ProtocolError = 2, - Connected = 3, - Disconnected = 4, - TooManyConnections = 5, - ConnectionAborted = 6, - Success = 7 -} +namespace LiteCharms.Features.TechShop; public enum CorrelationIdTypes : int { @@ -27,13 +15,6 @@ public enum CorrelationIdTypes : int LinkedIn = 10 } -public enum Priorities : int -{ - Low = 0, - Medium = 1, - High = 2, -} - public enum NotificationPlatforms : int { Email = 1, diff --git a/LiteCharms.Features.TechShop/Extensions/HealthChecks.cs b/LiteCharms.Features.TechShop/Extensions/HealthChecks.cs new file mode 100644 index 0000000..ee2a4a4 --- /dev/null +++ b/LiteCharms.Features.TechShop/Extensions/HealthChecks.cs @@ -0,0 +1,22 @@ +using LiteCharms.Features.TechShop.HealthChecks; +using static LiteCharms.Features.TechShop.Extensions.Postgres; + +namespace LiteCharms.Features.TechShop.Extensions; + +public static class HealthChecks +{ + public static IServiceCollection AddShopQuartzHealthCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck("ShopQuartz"); + + return services; + } + + + public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck(ShopDbConfigName); + + return services; + } +} diff --git a/LiteCharms.Features/Extensions/EntityModeMappers.cs b/LiteCharms.Features.TechShop/Extensions/Mappers.cs similarity index 75% rename from LiteCharms.Features/Extensions/EntityModeMappers.cs rename to LiteCharms.Features.TechShop/Extensions/Mappers.cs index 9dce6e8..2190f0e 100644 --- a/LiteCharms.Features/Extensions/EntityModeMappers.cs +++ b/LiteCharms.Features.TechShop/Extensions/Mappers.cs @@ -1,17 +1,18 @@ -using LiteCharms.Features.Shop.CartPackages.Models; -using LiteCharms.Features.Shop.Customers.Models; -using LiteCharms.Features.Shop.Leads.Models; -using LiteCharms.Features.Shop.Notifications.Models; -using LiteCharms.Features.Shop.Orders.Models; -using LiteCharms.Features.Shop.Products.Models; -using LiteCharms.Features.Shop.Quotes.Models; -using LiteCharms.Features.Shop.ShoppingCarts.Models; +using LiteCharms.Features.TechShop.CartPackages.Models; +using LiteCharms.Features.TechShop.Customers.Models; +using LiteCharms.Features.TechShop.Leads.Models; +using LiteCharms.Features.TechShop.Notifications.Models; +using LiteCharms.Features.TechShop.Orders.Models; +using LiteCharms.Features.TechShop.Products.Entities; +using LiteCharms.Features.TechShop.Products.Models; +using LiteCharms.Features.TechShop.Quotes.Models; +using LiteCharms.Features.TechShop.ShoppingCarts.Models; -namespace LiteCharms.Features.Extensions; +namespace LiteCharms.Features.TechShop.Extensions; -public static class EntityModeMappers +public static class Mappers { - public static ShoppingCartPackage ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage entity) => + public static ShoppingCartPackage ToModel(this ShoppingCarts.Entities.ShoppingCartPackage entity) => new() { Id = entity.Id, @@ -20,7 +21,7 @@ public static class EntityModeMappers ShoppingCartId = entity.ShoppingCartId }; - public static PackageItem ToModel(this Features.Shop.CartPackages.Entities.PackageItem entity) => + public static PackageItem ToModel(this CartPackages.Entities.PackageItem entity) => new() { Id = entity.Id, @@ -30,7 +31,7 @@ public static class EntityModeMappers ProductPriceId = entity.ProductPriceId }; - public static Package ToModel(this Features.Shop.CartPackages.Entities.Package entity) => + public static Package ToModel(this CartPackages.Entities.Package entity) => new() { Id = entity.Id, @@ -43,7 +44,7 @@ public static class EntityModeMappers Summary = entity.Summary }; - public static ShoppingCartItem ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartItem entity) => + public static ShoppingCartItem ToModel(this ShoppingCarts.Entities.ShoppingCartItem entity) => new() { Id = entity.Id, @@ -54,7 +55,7 @@ public static class EntityModeMappers ShoppingCartId = entity.ShoppingCartId }; - public static ShoppingCart ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCart entity) => + public static ShoppingCart ToModel(this ShoppingCarts.Entities.ShoppingCart entity) => new() { Id = entity.Id, @@ -64,7 +65,7 @@ public static class EntityModeMappers OrderId = entity.OrderId }; - public static Quote ToModel(this Features.Shop.Quotes.Entities.Quote entity) => + public static Quote ToModel(this Quotes.Entities.Quote entity) => new() { Id = entity.Id, @@ -79,7 +80,7 @@ public static class EntityModeMappers OrderId = entity.OrderId }; - public static Notification ToModel(this Features.Shop.Notifications.Entities.Notification entity) => + public static Notification ToModel(this Notifications.Entities.Notification entity) => new() { Id = entity.Id, @@ -103,7 +104,7 @@ public static class EntityModeMappers Errors = entity.Errors }; - public static Customer ToModel(this Features.Shop.Customers.Entities.Customer entity) => + public static Customer ToModel(this Customers.Entities.Customer entity) => new() { Id = entity.Id, @@ -128,7 +129,7 @@ public static class EntityModeMappers Whatsapp = entity.Whatsapp }; - public static Lead ToModel(this Features.Shop.Leads.Entities.Lead entity) => + public static Lead ToModel(this Leads.Entities.Lead entity) => new() { Id = entity.Id, @@ -149,7 +150,7 @@ public static class EntityModeMappers Status = entity.Status }; - public static Order ToModel(this Features.Shop.Orders.Entities.Order entity) => + public static Order ToModel(this Orders.Entities.Order entity) => new() { Id = entity.Id, @@ -163,7 +164,7 @@ public static class EntityModeMappers InvoiceUrl = entity.InvoiceUrl }; - public static OrderRefund ToModel(this Features.Shop.Orders.Entities.OrderRefund entity) => + public static OrderRefund ToModel(this Orders.Entities.OrderRefund entity) => new() { Id = entity.Id, @@ -173,7 +174,7 @@ public static class EntityModeMappers Amount = entity.Amount }; - public static Product ToModel(this Features.Shop.Products.Entities.Product entity) => + public static Products.Models.Product ToModel(this Products.Entities.Product entity) => new() { Id = entity.Id, @@ -187,7 +188,7 @@ public static class EntityModeMappers Metadata = entity.Metadata, }; - public static ProductPrice ToModel(this Features.Shop.Products.Entities.ProductPrice entity) => + public static Products.Models.ProductPrice ToModel(this Products.Entities.ProductPrice entity) => new() { Id = entity.Id, diff --git a/LiteCharms.Features.TechShop/Extensions/Postgres.cs b/LiteCharms.Features.TechShop/Extensions/Postgres.cs new file mode 100644 index 0000000..a264fa2 --- /dev/null +++ b/LiteCharms.Features.TechShop/Extensions/Postgres.cs @@ -0,0 +1,16 @@ +using LiteCharms.Features.TechShop.Postgres; + +namespace LiteCharms.Features.TechShop.Extensions; + +public static class Postgres +{ + public const string ShopDbConfigName = "PostgresShop"; + + public static IServiceCollection AddTechShopDatabase(this IServiceCollection services, IConfiguration configuration) + { + services.AddPooledDbContextFactory(options => + options.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName))); + + return services; + } +} diff --git a/LiteCharms.Features.TechShop/Extensions/Shop.cs b/LiteCharms.Features.TechShop/Extensions/Shop.cs new file mode 100644 index 0000000..fd96fc6 --- /dev/null +++ b/LiteCharms.Features.TechShop/Extensions/Shop.cs @@ -0,0 +1,27 @@ +using LiteCharms.Features.TechShop.CartPackages; +using LiteCharms.Features.TechShop.Customers; +using LiteCharms.Features.TechShop.Leads; +using LiteCharms.Features.TechShop.Notifications; +using LiteCharms.Features.TechShop.Orders; +using LiteCharms.Features.TechShop.Products; +using LiteCharms.Features.TechShop.Quotes; +using LiteCharms.Features.TechShop.ShoppingCarts; + +namespace LiteCharms.Features.TechShop.Extensions; + +public static class Shop +{ + public static IServiceCollection AddTechShopServices(this IServiceCollection services) + { + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + return services; + } +} diff --git a/LiteCharms.Features/HealthChecks/PostgresShopHealthCheck.cs b/LiteCharms.Features.TechShop/HealthChecks/PostgresShopHealthCheck.cs similarity index 88% rename from LiteCharms.Features/HealthChecks/PostgresShopHealthCheck.cs rename to LiteCharms.Features.TechShop/HealthChecks/PostgresShopHealthCheck.cs index 4a65640..db55287 100644 --- a/LiteCharms.Features/HealthChecks/PostgresShopHealthCheck.cs +++ b/LiteCharms.Features.TechShop/HealthChecks/PostgresShopHealthCheck.cs @@ -1,6 +1,6 @@ -using static LiteCharms.Features.Extensions.Postgres; +using static LiteCharms.Features.TechShop.Extensions.Postgres; -namespace LiteCharms.Features.HealthChecks; +namespace LiteCharms.Features.TechShop.HealthChecks; public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthCheck { diff --git a/LiteCharms.Features/HealthChecks/ShopQuartzHealthCheck.cs b/LiteCharms.Features.TechShop/HealthChecks/ShopQuartzHealthCheck.cs similarity index 95% rename from LiteCharms.Features/HealthChecks/ShopQuartzHealthCheck.cs rename to LiteCharms.Features.TechShop/HealthChecks/ShopQuartzHealthCheck.cs index 0ccae12..5599d26 100644 --- a/LiteCharms.Features/HealthChecks/ShopQuartzHealthCheck.cs +++ b/LiteCharms.Features.TechShop/HealthChecks/ShopQuartzHealthCheck.cs @@ -1,6 +1,6 @@ using static LiteCharms.Features.Extensions.Quartz; -namespace LiteCharms.Features.HealthChecks; +namespace LiteCharms.Features.TechShop.HealthChecks; public class ShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck { diff --git a/LiteCharms.Features/Shop/Leads/Entities/Lead.cs b/LiteCharms.Features.TechShop/Leads/Entities/Lead.cs similarity index 55% rename from LiteCharms.Features/Shop/Leads/Entities/Lead.cs rename to LiteCharms.Features.TechShop/Leads/Entities/Lead.cs index cbb4ec1..8ce5357 100644 --- a/LiteCharms.Features/Shop/Leads/Entities/Lead.cs +++ b/LiteCharms.Features.TechShop/Leads/Entities/Lead.cs @@ -1,6 +1,6 @@ -using LiteCharms.Features.Shop.Customers.Entities; +using LiteCharms.Features.TechShop.Customers.Entities; -namespace LiteCharms.Features.Shop.Leads.Entities; +namespace LiteCharms.Features.TechShop.Leads.Entities; [EntityTypeConfiguration] public class Lead : Models.Lead diff --git a/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs b/LiteCharms.Features.TechShop/Leads/Entities/LeadConfiguration.cs similarity index 95% rename from LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs rename to LiteCharms.Features.TechShop/Leads/Entities/LeadConfiguration.cs index e24839e..c5beca0 100644 --- a/LiteCharms.Features/Shop/Leads/Entities/LeadConfiguration.cs +++ b/LiteCharms.Features.TechShop/Leads/Entities/LeadConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Leads.Entities; +namespace LiteCharms.Features.TechShop.Leads.Entities; public class LeadConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/Leads/LeadService.cs b/LiteCharms.Features.TechShop/Leads/LeadService.cs similarity index 96% rename from LiteCharms.Features/Shop/Leads/LeadService.cs rename to LiteCharms.Features.TechShop/Leads/LeadService.cs index 18c5063..79c9858 100644 --- a/LiteCharms.Features/Shop/Leads/LeadService.cs +++ b/LiteCharms.Features.TechShop/Leads/LeadService.cs @@ -1,10 +1,11 @@ using LiteCharms.Features.Extensions; using LiteCharms.Features.Models; -using LiteCharms.Features.Shop.Leads.Models; -using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Leads.Models; +using LiteCharms.Features.TechShop.Postgres; using static LiteCharms.Features.Extensions.Hash; -namespace LiteCharms.Features.Shop.Leads; +namespace LiteCharms.Features.TechShop.Leads; public class LeadService(IDbContextFactory contextFactory) { diff --git a/LiteCharms.Features/Shop/Leads/Models/Lead.cs b/LiteCharms.Features.TechShop/Leads/Models/Lead.cs similarity index 92% rename from LiteCharms.Features/Shop/Leads/Models/Lead.cs rename to LiteCharms.Features.TechShop/Leads/Models/Lead.cs index adfb1ba..71bd6b7 100644 --- a/LiteCharms.Features/Shop/Leads/Models/Lead.cs +++ b/LiteCharms.Features.TechShop/Leads/Models/Lead.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Leads.Models; +namespace LiteCharms.Features.TechShop.Leads.Models; public class Lead { diff --git a/LiteCharms.Features/Shop/Leads/Models/Records.cs b/LiteCharms.Features.TechShop/Leads/Models/Records.cs similarity index 91% rename from LiteCharms.Features/Shop/Leads/Models/Records.cs rename to LiteCharms.Features.TechShop/Leads/Models/Records.cs index 9037942..0d84a14 100644 --- a/LiteCharms.Features/Shop/Leads/Models/Records.cs +++ b/LiteCharms.Features.TechShop/Leads/Models/Records.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Leads.Models; +namespace LiteCharms.Features.TechShop.Leads.Models; public record CreateLead { diff --git a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj new file mode 100644 index 0000000..a8478ad --- /dev/null +++ b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj @@ -0,0 +1,166 @@ + + + + net10.0 + enable + enable + True + ..\LiteCharms.snk + fbd8f4a2-0420-44e2-baff-4678d9e7eee1 + + + + + LiteCharms.Features.TechShop + 1.0.20 + Khwezi Mngoma + Lite Charms (PTY) Ltd + TechShop feature components for Lite Charms applications. + https://gitea.khongisa.co.za/litecharms/components + https://gitea.khongisa.co.za/litecharms/components.git + git + LICENSE + utility;dotnet + icon.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/LiteCharms.Features/Shop/Notifications/Entities/Notification.cs b/LiteCharms.Features.TechShop/Notifications/Entities/Notification.cs similarity index 63% rename from LiteCharms.Features/Shop/Notifications/Entities/Notification.cs rename to LiteCharms.Features.TechShop/Notifications/Entities/Notification.cs index 36a7dbc..b94b428 100644 --- a/LiteCharms.Features/Shop/Notifications/Entities/Notification.cs +++ b/LiteCharms.Features.TechShop/Notifications/Entities/Notification.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Notifications.Entities; +namespace LiteCharms.Features.TechShop.Notifications.Entities; [EntityTypeConfiguration] public class Notification : Models.Notification; diff --git a/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs b/LiteCharms.Features.TechShop/Notifications/Entities/NotificationConfiguration.cs similarity index 95% rename from LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs rename to LiteCharms.Features.TechShop/Notifications/Entities/NotificationConfiguration.cs index 7139d0d..089e749 100644 --- a/LiteCharms.Features/Shop/Notifications/Entities/NotificationConfiguration.cs +++ b/LiteCharms.Features.TechShop/Notifications/Entities/NotificationConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Notifications.Entities; +namespace LiteCharms.Features.TechShop.Notifications.Entities; public class NotificationConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs b/LiteCharms.Features.TechShop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs similarity index 94% rename from LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs rename to LiteCharms.Features.TechShop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs index 6917777..5a2cdd2 100644 --- a/LiteCharms.Features/Shop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs +++ b/LiteCharms.Features.TechShop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs @@ -1,9 +1,8 @@ -using k8s.KubeConfigModels; -using LiteCharms.Features.Email; -using LiteCharms.Features.Shop.Notifications.Models; -using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Email; +using LiteCharms.Features.TechShop.Notifications.Models; +using LiteCharms.Features.TechShop.Postgres; -namespace LiteCharms.Features.Shop.Notifications.Events.Handlers; +namespace LiteCharms.Features.TechShop.Notifications.Events.Handlers; public class ProcessEmailNotificationsEventHandler(IDbContextFactory contextFactory, ILogger logger, EmailService emailService) : INotificationHandler diff --git a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs b/LiteCharms.Features.TechShop/Notifications/Events/Handlers/SendShopEmailEnquiryEventHandler.cs similarity index 75% rename from LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs rename to LiteCharms.Features.TechShop/Notifications/Events/Handlers/SendShopEmailEnquiryEventHandler.cs index 1b7fda9..c6bc984 100644 --- a/LiteCharms.Features/Email/Events/Handlers/SendShopEmailEnquiryEventHandler.cs +++ b/LiteCharms.Features.TechShop/Notifications/Events/Handlers/SendShopEmailEnquiryEventHandler.cs @@ -1,14 +1,12 @@ -using LiteCharms.Features.Shop; -using LiteCharms.Features.Shop.Notifications; -using static LiteCharms.Features.Extensions.Email; +using static LiteCharms.Features.Extensions.Email; -namespace LiteCharms.Features.Email.Events.Handlers; +namespace LiteCharms.Features.TechShop.Notifications.Events.Handlers; public class SendShopEmailEnquiryEventHandler(NotificationService notificationService) : INotificationHandler { public async ValueTask Handle(SendShopEmailEnquiryEvent notification, CancellationToken cancellationToken) => - await notificationService.CreateNotificationAsync(new Shop.Notifications.Models.CreateNotification + await notificationService.CreateNotificationAsync(new Models.CreateNotification { CorrelationId = notification.CorrelationId, CorrelationIdType = CorrelationIdTypes.None, diff --git a/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs b/LiteCharms.Features.TechShop/Notifications/Events/ProcessEmailNotificationsEvent.cs similarity index 89% rename from LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs rename to LiteCharms.Features.TechShop/Notifications/Events/ProcessEmailNotificationsEvent.cs index 366332f..06a7029 100644 --- a/LiteCharms.Features/Shop/Notifications/Events/ProcessEmailNotificationsEvent.cs +++ b/LiteCharms.Features.TechShop/Notifications/Events/ProcessEmailNotificationsEvent.cs @@ -1,6 +1,6 @@ using LiteCharms.Features.Abstractions; -namespace LiteCharms.Features.Shop.Notifications.Events; +namespace LiteCharms.Features.TechShop.Notifications.Events; public class ProcessEmailNotificationsEvent : EventBase, IEvent { diff --git a/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs b/LiteCharms.Features.TechShop/Notifications/Events/SendShopEmailEnquiryEvent.cs similarity index 94% rename from LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs rename to LiteCharms.Features.TechShop/Notifications/Events/SendShopEmailEnquiryEvent.cs index 3377b44..246f433 100644 --- a/LiteCharms.Features/Email/Events/SendShopEmailEnquiryEvent.cs +++ b/LiteCharms.Features.TechShop/Notifications/Events/SendShopEmailEnquiryEvent.cs @@ -1,7 +1,6 @@ using LiteCharms.Features.Abstractions; -using LiteCharms.Features.Shop; -namespace LiteCharms.Features.Email.Events; +namespace LiteCharms.Features.TechShop.Notifications.Events; public class SendShopEmailEnquiryEvent : EventBase, IEvent { diff --git a/LiteCharms.Features/Shop/Notifications/Models/Notification.cs b/LiteCharms.Features.TechShop/Notifications/Models/Notification.cs similarity index 89% rename from LiteCharms.Features/Shop/Notifications/Models/Notification.cs rename to LiteCharms.Features.TechShop/Notifications/Models/Notification.cs index fd085a1..71ca0b3 100644 --- a/LiteCharms.Features/Shop/Notifications/Models/Notification.cs +++ b/LiteCharms.Features.TechShop/Notifications/Models/Notification.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.Shop.Notifications.Models; +using LiteCharms.Features.TechShop; + +namespace LiteCharms.Features.TechShop.Notifications.Models; public class Notification { diff --git a/LiteCharms.Features/Shop/Notifications/Models/Records.cs b/LiteCharms.Features.TechShop/Notifications/Models/Records.cs similarity index 93% rename from LiteCharms.Features/Shop/Notifications/Models/Records.cs rename to LiteCharms.Features.TechShop/Notifications/Models/Records.cs index 6d41f26..d72e5e7 100644 --- a/LiteCharms.Features/Shop/Notifications/Models/Records.cs +++ b/LiteCharms.Features.TechShop/Notifications/Models/Records.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Notifications.Models; +namespace LiteCharms.Features.TechShop.Notifications.Models; public record CreateNotification { diff --git a/LiteCharms.Features/Shop/Notifications/NotificationService.cs b/LiteCharms.Features.TechShop/Notifications/NotificationService.cs similarity index 95% rename from LiteCharms.Features/Shop/Notifications/NotificationService.cs rename to LiteCharms.Features.TechShop/Notifications/NotificationService.cs index 928a131..8826294 100644 --- a/LiteCharms.Features/Shop/Notifications/NotificationService.cs +++ b/LiteCharms.Features.TechShop/Notifications/NotificationService.cs @@ -1,9 +1,10 @@ using LiteCharms.Features.Extensions; using LiteCharms.Features.Models; -using LiteCharms.Features.Shop.Notifications.Models; -using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Notifications.Models; +using LiteCharms.Features.TechShop.Postgres; -namespace LiteCharms.Features.Shop.Notifications; +namespace LiteCharms.Features.TechShop.Notifications; public class NotificationService(IDbContextFactory contextFactory) { diff --git a/LiteCharms.Features/Shop/Orders/Entities/Order.cs b/LiteCharms.Features.TechShop/Orders/Entities/Order.cs similarity index 58% rename from LiteCharms.Features/Shop/Orders/Entities/Order.cs rename to LiteCharms.Features.TechShop/Orders/Entities/Order.cs index f5dd1d0..3996795 100644 --- a/LiteCharms.Features/Shop/Orders/Entities/Order.cs +++ b/LiteCharms.Features.TechShop/Orders/Entities/Order.cs @@ -1,8 +1,8 @@ -using LiteCharms.Features.Shop.Customers.Entities; -using LiteCharms.Features.Shop.Quotes.Entities; -using LiteCharms.Features.Shop.ShoppingCarts.Entities; +using LiteCharms.Features.TechShop.Customers.Entities; +using LiteCharms.Features.TechShop.Quotes.Entities; +using LiteCharms.Features.TechShop.ShoppingCarts.Entities; -namespace LiteCharms.Features.Shop.Orders.Entities; +namespace LiteCharms.Features.TechShop.Orders.Entities; [EntityTypeConfiguration] public class Order : Models.Order diff --git a/LiteCharms.Features/Shop/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features.TechShop/Orders/Entities/OrderConfiguration.cs similarity index 94% rename from LiteCharms.Features/Shop/Orders/Entities/OrderConfiguration.cs rename to LiteCharms.Features.TechShop/Orders/Entities/OrderConfiguration.cs index 848aaed..187c916 100644 --- a/LiteCharms.Features/Shop/Orders/Entities/OrderConfiguration.cs +++ b/LiteCharms.Features.TechShop/Orders/Entities/OrderConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Orders.Entities; +namespace LiteCharms.Features.TechShop.Orders.Entities; public class OrderConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/Orders/Entities/OrderRefund.cs b/LiteCharms.Features.TechShop/Orders/Entities/OrderRefund.cs similarity index 73% rename from LiteCharms.Features/Shop/Orders/Entities/OrderRefund.cs rename to LiteCharms.Features.TechShop/Orders/Entities/OrderRefund.cs index 8f7e462..e832b46 100644 --- a/LiteCharms.Features/Shop/Orders/Entities/OrderRefund.cs +++ b/LiteCharms.Features.TechShop/Orders/Entities/OrderRefund.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Orders.Entities; +namespace LiteCharms.Features.TechShop.Orders.Entities; [EntityTypeConfiguration] public class OrderRefund : Models.OrderRefund diff --git a/LiteCharms.Features/Shop/Orders/Entities/OrderRefundConfiguration.cs b/LiteCharms.Features.TechShop/Orders/Entities/OrderRefundConfiguration.cs similarity index 92% rename from LiteCharms.Features/Shop/Orders/Entities/OrderRefundConfiguration.cs rename to LiteCharms.Features.TechShop/Orders/Entities/OrderRefundConfiguration.cs index b33c2ee..33a336b 100644 --- a/LiteCharms.Features/Shop/Orders/Entities/OrderRefundConfiguration.cs +++ b/LiteCharms.Features.TechShop/Orders/Entities/OrderRefundConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Orders.Entities; +namespace LiteCharms.Features.TechShop.Orders.Entities; public class OrderRefundConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/Orders/Models/Order.cs b/LiteCharms.Features.TechShop/Orders/Models/Order.cs similarity index 81% rename from LiteCharms.Features/Shop/Orders/Models/Order.cs rename to LiteCharms.Features.TechShop/Orders/Models/Order.cs index 30bb75d..2146423 100644 --- a/LiteCharms.Features/Shop/Orders/Models/Order.cs +++ b/LiteCharms.Features.TechShop/Orders/Models/Order.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.Shop.Orders.Models; +using LiteCharms.Features.TechShop; + +namespace LiteCharms.Features.TechShop.Orders.Models; public class Order { diff --git a/LiteCharms.Features/Shop/Orders/Models/OrderRefund.cs b/LiteCharms.Features.TechShop/Orders/Models/OrderRefund.cs similarity index 80% rename from LiteCharms.Features/Shop/Orders/Models/OrderRefund.cs rename to LiteCharms.Features.TechShop/Orders/Models/OrderRefund.cs index 1463afd..b8ac3fb 100644 --- a/LiteCharms.Features/Shop/Orders/Models/OrderRefund.cs +++ b/LiteCharms.Features.TechShop/Orders/Models/OrderRefund.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Orders.Models; +namespace LiteCharms.Features.TechShop.Orders.Models; public class OrderRefund { diff --git a/LiteCharms.Features/Shop/Orders/Models/Records.cs b/LiteCharms.Features.TechShop/Orders/Models/Records.cs similarity index 89% rename from LiteCharms.Features/Shop/Orders/Models/Records.cs rename to LiteCharms.Features.TechShop/Orders/Models/Records.cs index 21dde34..1325c97 100644 --- a/LiteCharms.Features/Shop/Orders/Models/Records.cs +++ b/LiteCharms.Features.TechShop/Orders/Models/Records.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.Shop.Orders.Models; +using LiteCharms.Features.TechShop; + +namespace LiteCharms.Features.TechShop.Orders.Models; public record CreateOrder { diff --git a/LiteCharms.Features/Shop/Orders/OrderService.cs b/LiteCharms.Features.TechShop/Orders/OrderService.cs similarity index 98% rename from LiteCharms.Features/Shop/Orders/OrderService.cs rename to LiteCharms.Features.TechShop/Orders/OrderService.cs index ff6d571..9d5299b 100644 --- a/LiteCharms.Features/Shop/Orders/OrderService.cs +++ b/LiteCharms.Features.TechShop/Orders/OrderService.cs @@ -1,9 +1,10 @@ using LiteCharms.Features.Extensions; using LiteCharms.Features.Models; -using LiteCharms.Features.Shop.Orders.Models; -using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Orders.Models; +using LiteCharms.Features.TechShop.Postgres; -namespace LiteCharms.Features.Shop.Orders; +namespace LiteCharms.Features.TechShop.Orders; public class OrderService(IDbContextFactory contextFactory) { diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.Designer.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.Designer.cs similarity index 99% rename from LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.Designer.cs rename to LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.Designer.cs index 5900074..f7407e8 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.Designer.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.Designer.cs @@ -1,6 +1,6 @@ // using System; -using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.TechShop.Postgres; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace LiteCharms.Features.Shop.Postgres.Migrations +namespace LiteCharms.Features.TechShop.Postgres.Migrations { [DbContext(typeof(ShopDbContext))] [Migration("20260512065421_Init")] diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.cs similarity index 99% rename from LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.cs rename to LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.cs index 1220ecc..201f8d5 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/20260512065421_Init.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace LiteCharms.Features.Shop.Postgres.Migrations +namespace LiteCharms.Features.TechShop.Postgres.Migrations { /// public partial class Init : Migration diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs similarity index 99% rename from LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs rename to LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs index b0185a2..275d93a 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs @@ -1,6 +1,6 @@ // using System; -using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.TechShop.Postgres; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace LiteCharms.Features.Shop.Postgres.Migrations +namespace LiteCharms.Features.TechShop.Postgres.Migrations { [DbContext(typeof(ShopDbContext))] [Migration("20260514004002_UsedStringTableNames")] diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs similarity index 99% rename from LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs rename to LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs index 234f04a..22dc364 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace LiteCharms.Features.Shop.Postgres.Migrations +namespace LiteCharms.Features.TechShop.Postgres.Migrations { /// public partial class UsedStringTableNames : Migration diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs similarity index 99% rename from LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs rename to LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs index 9789b91..74a6cba 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs @@ -1,6 +1,6 @@ // using System; -using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.TechShop.Postgres; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace LiteCharms.Features.Shop.Postgres.Migrations +namespace LiteCharms.Features.TechShop.Postgres.Migrations { [DbContext(typeof(ShopDbContext))] [Migration("20260515055221_FixedLeadCustomerRelationship")] diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs similarity index 99% rename from LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs rename to LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs index 1d701f8..a15cd6f 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace LiteCharms.Features.Shop.Postgres.Migrations +namespace LiteCharms.Features.TechShop.Postgres.Migrations { /// public partial class FixedLeadCustomerRelationship : Migration diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs similarity index 99% rename from LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs rename to LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs index 7ac8666..82a7a0c 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs @@ -1,7 +1,7 @@ // using System; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.Products.Models; +using LiteCharms.Features.TechShop.Postgres; +using LiteCharms.Features.TechShop.Products.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace LiteCharms.Features.Shop.Postgres.Migrations +namespace LiteCharms.Features.TechShop.Postgres.Migrations { [DbContext(typeof(ShopDbContext))] [Migration("20260520191059_AddedProductMetadata")] diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs similarity index 94% rename from LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs rename to LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs index 142f394..64abe3b 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs @@ -1,10 +1,10 @@ using System; -using LiteCharms.Features.Shop.Products.Models; +using LiteCharms.Features.TechShop.Products.Models; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace LiteCharms.Features.Shop.Postgres.Migrations +namespace LiteCharms.Features.TechShop.Postgres.Migrations { /// public partial class AddedProductMetadata : Migration diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs similarity index 99% rename from LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs rename to LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs index 55c8c90..7fd9a00 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs @@ -1,7 +1,7 @@ // using System; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.Products.Models; +using LiteCharms.Features.TechShop.Postgres; +using LiteCharms.Features.TechShop.Products.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace LiteCharms.Features.Shop.Postgres.Migrations +namespace LiteCharms.Features.TechShop.Postgres.Migrations { [DbContext(typeof(ShopDbContext))] partial class ShopDbContextModelSnapshot : ModelSnapshot diff --git a/LiteCharms.Features.TechShop/Postgres/ShopDbContext.cs b/LiteCharms.Features.TechShop/Postgres/ShopDbContext.cs new file mode 100644 index 0000000..d88c141 --- /dev/null +++ b/LiteCharms.Features.TechShop/Postgres/ShopDbContext.cs @@ -0,0 +1,39 @@ +using LiteCharms.Features.TechShop.CartPackages.Entities; +using LiteCharms.Features.TechShop.Customers.Entities; +using LiteCharms.Features.TechShop.Leads.Entities; +using LiteCharms.Features.TechShop.Notifications.Entities; +using LiteCharms.Features.TechShop.Orders.Entities; +using LiteCharms.Features.TechShop.Products.Entities; +using LiteCharms.Features.TechShop.Quotes.Entities; +using LiteCharms.Features.TechShop.ShoppingCarts.Entities; + +namespace LiteCharms.Features.TechShop.Postgres; + +public class ShopDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Customers { get; set; } + + public DbSet Leads { get; set; } + + public DbSet Orders { get; set; } + + public DbSet OrderRefunds { get; set; } + + public DbSet Products { get; set; } + + public DbSet ProductPrices { get; set; } + + public DbSet Notifications { get; set; } + + public DbSet Quotes { get; set; } + + public DbSet ShoppingCarts { get; set; } + + public DbSet ShoppingCartItems { get; set; } + + public DbSet Packages { get; set; } + + public DbSet PackageItems { get; set; } + + public DbSet ShoppingCartPackages { get; set; } +} diff --git a/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs b/LiteCharms.Features.TechShop/Postgres/ShopDbContextFactory.cs similarity index 85% rename from LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs rename to LiteCharms.Features.TechShop/Postgres/ShopDbContextFactory.cs index b8e61dc..e1e64fd 100644 --- a/LiteCharms.Features/Shop/Postgres/ShopDbContextFactory.cs +++ b/LiteCharms.Features.TechShop/Postgres/ShopDbContextFactory.cs @@ -1,6 +1,6 @@ -using static LiteCharms.Features.Extensions.Postgres; +using static LiteCharms.Features.TechShop.Extensions.Postgres; -namespace LiteCharms.Features.Shop.Postgres; +namespace LiteCharms.Features.TechShop.Postgres; public class ShopDbContextFactory : IDesignTimeDbContextFactory { diff --git a/LiteCharms.Features/Shop/Products/Entities/Product.cs b/LiteCharms.Features.TechShop/Products/Entities/Product.cs similarity index 74% rename from LiteCharms.Features/Shop/Products/Entities/Product.cs rename to LiteCharms.Features.TechShop/Products/Entities/Product.cs index a48774b..2131272 100644 --- a/LiteCharms.Features/Shop/Products/Entities/Product.cs +++ b/LiteCharms.Features.TechShop/Products/Entities/Product.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Products.Entities; +namespace LiteCharms.Features.TechShop.Products.Entities; [EntityTypeConfiguration] public class Product : Models.Product diff --git a/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.TechShop/Products/Entities/ProductConfiguration.cs similarity index 93% rename from LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs rename to LiteCharms.Features.TechShop/Products/Entities/ProductConfiguration.cs index 6449acb..b35bbd1 100644 --- a/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features.TechShop/Products/Entities/ProductConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Products.Entities; +namespace LiteCharms.Features.TechShop.Products.Entities; public class ProductConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/Products/Entities/ProductPrice.cs b/LiteCharms.Features.TechShop/Products/Entities/ProductPrice.cs similarity index 73% rename from LiteCharms.Features/Shop/Products/Entities/ProductPrice.cs rename to LiteCharms.Features.TechShop/Products/Entities/ProductPrice.cs index a711cae..cca5817 100644 --- a/LiteCharms.Features/Shop/Products/Entities/ProductPrice.cs +++ b/LiteCharms.Features.TechShop/Products/Entities/ProductPrice.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Products.Entities; +namespace LiteCharms.Features.TechShop.Products.Entities; [EntityTypeConfiguration] public class ProductPrice : Models.ProductPrice diff --git a/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs b/LiteCharms.Features.TechShop/Products/Entities/ProductPriceConfiguration.cs similarity index 93% rename from LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs rename to LiteCharms.Features.TechShop/Products/Entities/ProductPriceConfiguration.cs index 265dc6a..658bd90 100644 --- a/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs +++ b/LiteCharms.Features.TechShop/Products/Entities/ProductPriceConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Products.Entities; +namespace LiteCharms.Features.TechShop.Products.Entities; public class ProductPriceConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/Products/Models/CreateProductModel.cs b/LiteCharms.Features.TechShop/Products/Models/CreateProductModel.cs similarity index 96% rename from LiteCharms.Features/Shop/Products/Models/CreateProductModel.cs rename to LiteCharms.Features.TechShop/Products/Models/CreateProductModel.cs index 355347e..c8bf92a 100644 --- a/LiteCharms.Features/Shop/Products/Models/CreateProductModel.cs +++ b/LiteCharms.Features.TechShop/Products/Models/CreateProductModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace LiteCharms.Features.Shop.Products.Models; +namespace LiteCharms.Features.TechShop.Products.Models; public class CreateProductModel { diff --git a/LiteCharms.Features/Shop/Products/Models/Product.cs b/LiteCharms.Features.TechShop/Products/Models/Product.cs similarity index 88% rename from LiteCharms.Features/Shop/Products/Models/Product.cs rename to LiteCharms.Features.TechShop/Products/Models/Product.cs index e4e10d2..b5fd71b 100644 --- a/LiteCharms.Features/Shop/Products/Models/Product.cs +++ b/LiteCharms.Features.TechShop/Products/Models/Product.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Products.Models; +namespace LiteCharms.Features.TechShop.Products.Models; public class Product { diff --git a/LiteCharms.Features/Shop/Products/Models/ProductMetadata.cs b/LiteCharms.Features.TechShop/Products/Models/ProductMetadata.cs similarity index 79% rename from LiteCharms.Features/Shop/Products/Models/ProductMetadata.cs rename to LiteCharms.Features.TechShop/Products/Models/ProductMetadata.cs index da56f1a..168f2bd 100644 --- a/LiteCharms.Features/Shop/Products/Models/ProductMetadata.cs +++ b/LiteCharms.Features.TechShop/Products/Models/ProductMetadata.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Products.Models; +namespace LiteCharms.Features.TechShop.Products.Models; public class ProductMetadata { diff --git a/LiteCharms.Features/Shop/Products/Models/ProductPrice.cs b/LiteCharms.Features.TechShop/Products/Models/ProductPrice.cs similarity index 84% rename from LiteCharms.Features/Shop/Products/Models/ProductPrice.cs rename to LiteCharms.Features.TechShop/Products/Models/ProductPrice.cs index fc1860c..0464df1 100644 --- a/LiteCharms.Features/Shop/Products/Models/ProductPrice.cs +++ b/LiteCharms.Features.TechShop/Products/Models/ProductPrice.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Products.Models; +namespace LiteCharms.Features.TechShop.Products.Models; public class ProductPrice { diff --git a/LiteCharms.Features/Shop/Products/Models/Records.cs b/LiteCharms.Features.TechShop/Products/Models/Records.cs similarity index 84% rename from LiteCharms.Features/Shop/Products/Models/Records.cs rename to LiteCharms.Features.TechShop/Products/Models/Records.cs index 8027e18..22b7dfb 100644 --- a/LiteCharms.Features/Shop/Products/Models/Records.cs +++ b/LiteCharms.Features.TechShop/Products/Models/Records.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Products.Models; +namespace LiteCharms.Features.TechShop.Products.Models; public record CreateProduct { diff --git a/LiteCharms.Features/Shop/Products/ProductService.cs b/LiteCharms.Features.TechShop/Products/ProductService.cs similarity index 98% rename from LiteCharms.Features/Shop/Products/ProductService.cs rename to LiteCharms.Features.TechShop/Products/ProductService.cs index 685fb5c..7c9eb83 100644 --- a/LiteCharms.Features/Shop/Products/ProductService.cs +++ b/LiteCharms.Features.TechShop/Products/ProductService.cs @@ -1,8 +1,8 @@ -using LiteCharms.Features.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.Products.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Postgres; +using LiteCharms.Features.TechShop.Products.Models; -namespace LiteCharms.Features.Shop.Products; +namespace LiteCharms.Features.TechShop.Products; public class ProductService(IDbContextFactory contextFactory) { diff --git a/LiteCharms.Features/Shop/Quotes/Entities/Quote.cs b/LiteCharms.Features.TechShop/Quotes/Entities/Quote.cs similarity index 52% rename from LiteCharms.Features/Shop/Quotes/Entities/Quote.cs rename to LiteCharms.Features.TechShop/Quotes/Entities/Quote.cs index 4ea6dab..aa4ada3 100644 --- a/LiteCharms.Features/Shop/Quotes/Entities/Quote.cs +++ b/LiteCharms.Features.TechShop/Quotes/Entities/Quote.cs @@ -1,8 +1,8 @@ -using LiteCharms.Features.Shop.Customers.Entities; -using LiteCharms.Features.Shop.Orders.Entities; -using LiteCharms.Features.Shop.ShoppingCarts.Entities; +using LiteCharms.Features.TechShop.Customers.Entities; +using LiteCharms.Features.TechShop.Orders.Entities; +using LiteCharms.Features.TechShop.ShoppingCarts.Entities; -namespace LiteCharms.Features.Shop.Quotes.Entities; +namespace LiteCharms.Features.TechShop.Quotes.Entities; [EntityTypeConfiguration] public class Quote : Models.Quote diff --git a/LiteCharms.Features/Shop/Quotes/Entities/QuoteConfiguration.cs b/LiteCharms.Features.TechShop/Quotes/Entities/QuoteConfiguration.cs similarity index 95% rename from LiteCharms.Features/Shop/Quotes/Entities/QuoteConfiguration.cs rename to LiteCharms.Features.TechShop/Quotes/Entities/QuoteConfiguration.cs index 1363973..3dfdc12 100644 --- a/LiteCharms.Features/Shop/Quotes/Entities/QuoteConfiguration.cs +++ b/LiteCharms.Features.TechShop/Quotes/Entities/QuoteConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Quotes.Entities; +namespace LiteCharms.Features.TechShop.Quotes.Entities; public class QuoteConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/Quotes/Models/Quote.cs b/LiteCharms.Features.TechShop/Quotes/Models/Quote.cs similarity index 88% rename from LiteCharms.Features/Shop/Quotes/Models/Quote.cs rename to LiteCharms.Features.TechShop/Quotes/Models/Quote.cs index 0b5ecaf..3effe97 100644 --- a/LiteCharms.Features/Shop/Quotes/Models/Quote.cs +++ b/LiteCharms.Features.TechShop/Quotes/Models/Quote.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.Quotes.Models; +namespace LiteCharms.Features.TechShop.Quotes.Models; public class Quote { diff --git a/LiteCharms.Features/Shop/Quotes/QuoteService.cs b/LiteCharms.Features.TechShop/Quotes/QuoteService.cs similarity index 96% rename from LiteCharms.Features/Shop/Quotes/QuoteService.cs rename to LiteCharms.Features.TechShop/Quotes/QuoteService.cs index c3de336..4e08bb8 100644 --- a/LiteCharms.Features/Shop/Quotes/QuoteService.cs +++ b/LiteCharms.Features.TechShop/Quotes/QuoteService.cs @@ -1,9 +1,10 @@ using LiteCharms.Features.Extensions; using LiteCharms.Features.Models; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.Quotes.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Postgres; +using LiteCharms.Features.TechShop.Quotes.Models; -namespace LiteCharms.Features.Shop.Quotes; +namespace LiteCharms.Features.TechShop.Quotes; public class QuoteService(IDbContextFactory contextFactory) { diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCart.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCart.cs similarity index 66% rename from LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCart.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCart.cs index cfb4bd5..153962e 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCart.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCart.cs @@ -1,8 +1,8 @@ -using LiteCharms.Features.Shop.Customers.Entities; -using LiteCharms.Features.Shop.Orders.Entities; -using LiteCharms.Features.Shop.Quotes.Entities; +using LiteCharms.Features.TechShop.Customers.Entities; +using LiteCharms.Features.TechShop.Orders.Entities; +using LiteCharms.Features.TechShop.Quotes.Entities; -namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; [EntityTypeConfiguration] public class ShoppingCart : Models.ShoppingCart diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs similarity index 93% rename from LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs index 1609973..a59b01d 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; public class ShoppingCartConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItem.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItem.cs similarity index 68% rename from LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItem.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItem.cs index b93d7da..5171b61 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItem.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItem.cs @@ -1,6 +1,6 @@ -using LiteCharms.Features.Shop.Products.Entities; +using LiteCharms.Features.TechShop.Products.Entities; -namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; [EntityTypeConfiguration] public class ShoppingCartItem : Models.ShoppingCartItem diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs similarity index 94% rename from LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs index 5670346..1e7b18a 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; public class ShoppingCartItemConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackage.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackage.cs similarity index 67% rename from LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackage.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackage.cs index 4adf983..7745f4d 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackage.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackage.cs @@ -1,6 +1,6 @@ -using LiteCharms.Features.Shop.CartPackages.Entities; +using LiteCharms.Features.TechShop.CartPackages.Entities; -namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; [EntityTypeConfiguration] public class ShoppingCartPackage : Models.ShoppingCartPackage diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs similarity index 93% rename from LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs index d6dc310..43f88a6 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.ShoppingCarts.Entities; +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration { diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCart.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCart.cs similarity index 78% rename from LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCart.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCart.cs index 46c52d0..0894a52 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCart.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCart.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.ShoppingCarts.Models; +namespace LiteCharms.Features.TechShop.ShoppingCarts.Models; public class ShoppingCart { diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartItem.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartItem.cs similarity index 81% rename from LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartItem.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartItem.cs index 99eeef6..e3760e4 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartItem.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartItem.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.ShoppingCarts.Models; +namespace LiteCharms.Features.TechShop.ShoppingCarts.Models; public class ShoppingCartItem { diff --git a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartPackage.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartPackage.cs similarity index 76% rename from LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartPackage.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartPackage.cs index a633be2..c040c5e 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/Models/ShoppingCartPackage.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartPackage.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.Shop.ShoppingCarts.Models; +namespace LiteCharms.Features.TechShop.ShoppingCarts.Models; public class ShoppingCartPackage { diff --git a/LiteCharms.Features/Shop/ShoppingCarts/ShoppingCartService.cs b/LiteCharms.Features.TechShop/ShoppingCarts/ShoppingCartService.cs similarity index 98% rename from LiteCharms.Features/Shop/ShoppingCarts/ShoppingCartService.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/ShoppingCartService.cs index d7eaf16..c8413a8 100644 --- a/LiteCharms.Features/Shop/ShoppingCarts/ShoppingCartService.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/ShoppingCartService.cs @@ -1,8 +1,8 @@ -using LiteCharms.Features.Extensions; -using LiteCharms.Features.Shop.Postgres; -using LiteCharms.Features.Shop.ShoppingCarts.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Postgres; +using LiteCharms.Features.TechShop.ShoppingCarts.Models; -namespace LiteCharms.Features.Shop.ShoppingCarts; +namespace LiteCharms.Features.TechShop.ShoppingCarts; public class ShoppingCartService(IDbContextFactory contextFactory) { diff --git a/LiteCharms.Features.TechShop/appsettings.json b/LiteCharms.Features.TechShop/appsettings.json new file mode 100644 index 0000000..aec5c2e --- /dev/null +++ b/LiteCharms.Features.TechShop/appsettings.json @@ -0,0 +1,22 @@ +{ + "Email": { + "Credentials": { + "Username": "shop@litecharms.co.za" + }, + "Port": 465, + "Host": "mail.litecharms.co.za", + "UseSsl": true + }, + "Monitoring": { + "ApiKey": "", + "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", + "ServiceName": "LiteCharms.LeadGenerator" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/LiteCharms.Features.Tests/CommonFixture.cs b/LiteCharms.Features.Tests/Fixture.cs similarity index 81% rename from LiteCharms.Features.Tests/CommonFixture.cs rename to LiteCharms.Features.Tests/Fixture.cs index b9085c4..466d0a0 100644 --- a/LiteCharms.Features.Tests/CommonFixture.cs +++ b/LiteCharms.Features.Tests/Fixture.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Tests; -public class CommonFixture : IDisposable +public class Fixture : IDisposable { public IConfiguration Configuration { get; set; } @@ -10,22 +10,20 @@ public class CommonFixture : IDisposable public IMediator Mediator { get; set; } - public CommonFixture() + public Fixture() { Configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) - .AddUserSecrets() + .AddUserSecrets() .AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); Services = new ServiceCollection() .AddMediator() - .AddLogging() - .AddShopServices() + .AddLogging() .AddEmailServiceBus() .AddGarageS3(Configuration) - .AddShopDatabase(Configuration) .AddEmailServices(Configuration) .AddSingleton(Configuration) .BuildServiceProvider(); diff --git a/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs b/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs index 3697a7a..8af9a34 100644 --- a/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs +++ b/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Tests; -public class S3ServiceFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture +public class S3ServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture { [Fact] public async Task BookshopS3Service_MustReturnUrl() diff --git a/LiteCharms.Features/Email/EmailService.cs b/LiteCharms.Features/Email/EmailService.cs index 72c5665..28c38e0 100644 --- a/LiteCharms.Features/Email/EmailService.cs +++ b/LiteCharms.Features/Email/EmailService.cs @@ -1,7 +1,6 @@ using LiteCharms.Features.Email.Configuration; using LiteCharms.Features.Email.Extensions; using LiteCharms.Features.Email.Models; -using LiteCharms.Features.Shop; namespace LiteCharms.Features.Email; diff --git a/LiteCharms.Features/Email/Models/Response.cs b/LiteCharms.Features/Email/Models/Response.cs index 4474d9e..6bc1c49 100644 --- a/LiteCharms.Features/Email/Models/Response.cs +++ b/LiteCharms.Features/Email/Models/Response.cs @@ -1,6 +1,4 @@ -using LiteCharms.Features.Shop; - -namespace LiteCharms.Features.Email.Models; +namespace LiteCharms.Features.Email.Models; public class Response { diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs new file mode 100644 index 0000000..29a915f --- /dev/null +++ b/LiteCharms.Features/Enums.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features; + +public enum EmailStatuses : int +{ + GeneralError = 0, + AuthenticationError = 1, + ProtocolError = 2, + Connected = 3, + Disconnected = 4, + TooManyConnections = 5, + ConnectionAborted = 6, + Success = 7 +} + +public enum Priorities : int +{ + Low = 0, + Medium = 1, + High = 2, +} \ No newline at end of file diff --git a/LiteCharms.Features/Extensions/HealthChecks.cs b/LiteCharms.Features/Extensions/HealthChecks.cs index f27502a..2b9ab08 100644 --- a/LiteCharms.Features/Extensions/HealthChecks.cs +++ b/LiteCharms.Features/Extensions/HealthChecks.cs @@ -5,13 +5,6 @@ namespace LiteCharms.Features.Extensions; public static class HealthChecks { - public static IServiceCollection AddShopQuartzHealthCheck(this IServiceCollection services) - { - services.AddHealthChecks().AddCheck("ShopQuartz"); - - return services; - } - public static IServiceCollection AddMidrandShopQuartzHealthCheck(this IServiceCollection services) { services.AddHealthChecks().AddCheck("MidrandShopQuartz"); @@ -19,13 +12,6 @@ public static class HealthChecks return services; } - public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services) - { - services.AddHealthChecks().AddCheck(ShopDbConfigName); - - return services; - } - public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services) { services.AddHealthChecks().AddCheck(MidrandShopDbConfigName); diff --git a/LiteCharms.Features/Extensions/Postgres.cs b/LiteCharms.Features/Extensions/Postgres.cs index 982f5f9..549c5a0 100644 --- a/LiteCharms.Features/Extensions/Postgres.cs +++ b/LiteCharms.Features/Extensions/Postgres.cs @@ -1,22 +1,12 @@ using LiteCharms.Features.MidrandShop.Postgres; -using LiteCharms.Features.Shop.Postgres; namespace LiteCharms.Features.Extensions; public static class Postgres { public const string MidrandShopDbConfigName = "PostgresMidrandShop"; - public const string ShopDbConfigName = "PostgresShop"; public const string SchedulerDbConfigName = "PostgresScheduler"; - public static IServiceCollection AddShopDatabase(this IServiceCollection services, IConfiguration configuration) - { - services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName))); - - return services; - } - public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) { services.AddPooledDbContextFactory(options => diff --git a/LiteCharms.Features/Extensions/Shop.cs b/LiteCharms.Features/Extensions/Shop.cs deleted file mode 100644 index 78502de..0000000 --- a/LiteCharms.Features/Extensions/Shop.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Features.Shop.CartPackages; -using LiteCharms.Features.Shop.Customers; -using LiteCharms.Features.Shop.Leads; -using LiteCharms.Features.Shop.Notifications; -using LiteCharms.Features.Shop.Orders; -using LiteCharms.Features.Shop.Products; -using LiteCharms.Features.Shop.Quotes; -using LiteCharms.Features.Shop.ShoppingCarts; - -namespace LiteCharms.Features.Extensions; - -public static class Shop -{ - public static IServiceCollection AddShopServices(this IServiceCollection services) - { - services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - return services; - } -} diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 99f18de..41d7dcf 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -160,8 +160,5 @@ PreserveNewest - - - diff --git a/LiteCharms.Features/Shop/Postgres/ShopDbContext.cs b/LiteCharms.Features/Shop/Postgres/ShopDbContext.cs deleted file mode 100644 index b8d7cc9..0000000 --- a/LiteCharms.Features/Shop/Postgres/ShopDbContext.cs +++ /dev/null @@ -1,69 +0,0 @@ -using LiteCharms.Features.Shop.CartPackages.Entities; -using LiteCharms.Features.Shop.Customers.Entities; -using LiteCharms.Features.Shop.Leads.Entities; -using LiteCharms.Features.Shop.Notifications.Entities; -using LiteCharms.Features.Shop.Orders.Entities; -using LiteCharms.Features.Shop.Products.Entities; -using LiteCharms.Features.Shop.Quotes.Entities; -using LiteCharms.Features.Shop.ShoppingCarts.Entities; - -namespace LiteCharms.Features.Shop.Postgres; - -public class ShopDbContext(DbContextOptions options) : DbContext(options) -{ - public DbSet Customers { get; set; } - - public DbSet Leads { get; set; } - - public DbSet Orders { get; set; } - - public DbSet OrderRefunds { get; set; } - - public DbSet Products { get; set; } - - public DbSet ProductPrices { get; set; } - - public DbSet Notifications { get; set; } - - public DbSet Quotes { get; set; } - - public DbSet ShoppingCarts { get; set; } - - public DbSet ShoppingCartItems { get; set; } - - public DbSet Packages { get; set; } - - public DbSet PackageItems { get; set; } - - public DbSet ShoppingCartPackages { get; set; } - - //protected override void OnModelCreating(ModelBuilder modelBuilder) - //{ - // modelBuilder.Ignore(); - // modelBuilder.Ignore(); - // modelBuilder.Ignore(); - // modelBuilder.Ignore(); - // modelBuilder.Ignore(); - // modelBuilder.Ignore(); - // modelBuilder.Ignore(); - // modelBuilder.Ignore(); - // modelBuilder.Ignore(); - // modelBuilder.Ignore(); - // modelBuilder.Ignore(); - - // modelBuilder.ApplyConfiguration(new CustomerConfiguration()); - // modelBuilder.ApplyConfiguration(new LeadConfiguration()); - // modelBuilder.ApplyConfiguration(new OrderConfiguration()); - // modelBuilder.ApplyConfiguration(new ProductConfiguration()); - // modelBuilder.ApplyConfiguration(new ProductPriceConfiguration()); - // modelBuilder.ApplyConfiguration(new NotificationConfiguration()); - // modelBuilder.ApplyConfiguration(new QuoteConfiguration()); - // modelBuilder.ApplyConfiguration(new ShoppingCartConfiguration()); - // modelBuilder.ApplyConfiguration(new ShoppingCartItemConfiguration()); - // modelBuilder.ApplyConfiguration(new PackageConfirguration()); - // modelBuilder.ApplyConfiguration(new PackageItemConfiguration()); - // modelBuilder.ApplyConfiguration(new ShoppingCartPackageConfiguration()); - - // base.OnModelCreating(modelBuilder); - //} -} diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index 21e11b6..a8a890a 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -2,6 +2,8 @@ + + From f67f5eaf53c4d4abf47ccbffb478b0bfac641b9f Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 24 May 2026 13:38:47 +0200 Subject: [PATCH 050/153] Added MidrandBooks project --- .drone.yml | 4 +- .../Extensions/HealthChecks.cs | 13 +- .../Extensions/Postgres.cs | 16 ++ .../MidrandShopQuartzHealthCheck.cs | 2 +- .../PostgresMidrandShopHealthCheck.cs | 4 +- .../LiteCharms.Features.MidrandBooks.csproj | 166 ++++++++++++++++++ .../Postgres/MidrandShopDbContext.cs | 2 +- .../Postgres/MidrandShopDbContextFactory.cs | 4 +- .../appsettings.json | 22 +++ .../Extensions/HealthChecks.cs | 2 +- .../Extensions/Postgres.cs | 4 +- .../HealthChecks/PostgresShopHealthCheck.cs | 6 +- .../HealthChecks/ShopQuartzHealthCheck.cs | 10 +- .../Postgres/ShopDbContextFactory.cs | 2 +- LiteCharms.Features/Extensions/Postgres.cs | 13 +- LiteCharms.Features/Extensions/Quartz.cs | 4 +- LiteCharmsShared.slnx | 13 +- 17 files changed, 241 insertions(+), 46 deletions(-) rename {LiteCharms.Features => LiteCharms.Features.MidrandBooks}/Extensions/HealthChecks.cs (67%) create mode 100644 LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs rename {LiteCharms.Features => LiteCharms.Features.MidrandBooks}/HealthChecks/MidrandShopQuartzHealthCheck.cs (95%) rename {LiteCharms.Features => LiteCharms.Features.MidrandBooks}/HealthChecks/PostgresMidrandShopHealthCheck.cs (88%) create mode 100644 LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj rename {LiteCharms.Features/MidrandShop => LiteCharms.Features.MidrandBooks}/Postgres/MidrandShopDbContext.cs (66%) rename {LiteCharms.Features/MidrandShop => LiteCharms.Features.MidrandBooks}/Postgres/MidrandShopDbContextFactory.cs (85%) create mode 100644 LiteCharms.Features.MidrandBooks/appsettings.json diff --git a/.drone.yml b/.drone.yml index 49e0b2b..12c4fd6 100644 --- a/.drone.yml +++ b/.drone.yml @@ -21,6 +21,8 @@ steps: - dotnet nuget push dist/LiteCharms.Features.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL - dotnet pack LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj -c Release -p:PackageVersion=$VERSION -o dist/ - dotnet nuget push dist/LiteCharms.Features.TechShop.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + - dotnet pack LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj -c Release -p:PackageVersion=$VERSION -o dist/ + - dotnet nuget push dist/LiteCharms.Features.MidrandBooks.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL - name: gitea-tag-release image: alpine/git @@ -43,7 +45,7 @@ steps: \"tag_name\": \"$VERSION\", \"target_commitish\": \"${DRONE_COMMIT_SHA}\", \"name\": \"Library Suite $VERSION\", - \"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Features\n* LiteCharms.Features.TechShop\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\", + \"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Features\n* LiteCharms.Features.TechShop\n* LiteCharms.Features.MidrandBooks\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\", \"draft\": false, \"prerelease\": false }" diff --git a/LiteCharms.Features/Extensions/HealthChecks.cs b/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs similarity index 67% rename from LiteCharms.Features/Extensions/HealthChecks.cs rename to LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs index 2b9ab08..fbc3e1f 100644 --- a/LiteCharms.Features/Extensions/HealthChecks.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs @@ -1,13 +1,14 @@ -using LiteCharms.Features.HealthChecks; +using LiteCharms.Features.MidrandBooks.HealthChecks; using static LiteCharms.Features.Extensions.Postgres; +using static LiteCharms.Features.MidrandBooks.Extensions.Postgres; -namespace LiteCharms.Features.Extensions; +namespace LiteCharms.Features.MidrandBooks.Extensions; public static class HealthChecks { public static IServiceCollection AddMidrandShopQuartzHealthCheck(this IServiceCollection services) { - services.AddHealthChecks().AddCheck("MidrandShopQuartz"); + services.AddHealthChecks().AddCheck(SchedulerDbConfigName); return services; } @@ -24,12 +25,6 @@ public static class HealthChecks services.AddHealthChecks() .AddCheck("Self", () => HealthCheckResult.Healthy()); - //services.AddHealthChecksUI(setup => - //{ - // setup.AddHealthCheckEndpoint("Lead Generator", $"{configuration["ASPNETCORE_URLS"]}/health"); - // setup.SetEvaluationTimeInSeconds(15); - //}).AddInMemoryStorage(databaseName: "healthuidb"); - return services; } } diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs new file mode 100644 index 0000000..11e81b0 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs @@ -0,0 +1,16 @@ +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Extensions; + +public static class Postgres +{ + public const string MidrandShopDbConfigName = "PostgresMidrandShop"; + + public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) + { + services.AddPooledDbContextFactory(options => + options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName))); + + return services; + } +} diff --git a/LiteCharms.Features/HealthChecks/MidrandShopQuartzHealthCheck.cs b/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs similarity index 95% rename from LiteCharms.Features/HealthChecks/MidrandShopQuartzHealthCheck.cs rename to LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs index 9588d24..23bdf5a 100644 --- a/LiteCharms.Features/HealthChecks/MidrandShopQuartzHealthCheck.cs +++ b/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs @@ -1,6 +1,6 @@ using static LiteCharms.Features.Extensions.Quartz; -namespace LiteCharms.Features.HealthChecks; +namespace LiteCharms.Features.MidrandBooks.HealthChecks; public class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck { diff --git a/LiteCharms.Features/HealthChecks/PostgresMidrandShopHealthCheck.cs b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs similarity index 88% rename from LiteCharms.Features/HealthChecks/PostgresMidrandShopHealthCheck.cs rename to LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs index f88b22e..e60ef2c 100644 --- a/LiteCharms.Features/HealthChecks/PostgresMidrandShopHealthCheck.cs +++ b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs @@ -1,6 +1,6 @@ -using static LiteCharms.Features.Extensions.Postgres; +using static LiteCharms.Features.MidrandBooks.Extensions.Postgres; -namespace LiteCharms.Features.HealthChecks; +namespace LiteCharms.Features.MidrandBooks.HealthChecks; public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck { diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj new file mode 100644 index 0000000..552ce45 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -0,0 +1,166 @@ + + + + net10.0 + enable + enable + True + ..\LiteCharms.snk + 5be62f49-3ed0-4468-884e-1b04e048b45a + + + + + LiteCharms.Features.MidrandBooks + 1.0.20 + Khwezi Mngoma + Lite Charms (PTY) Ltd + MidrandBooks feature components for Lite Charms applications. + https://gitea.khongisa.co.za/litecharms/components + https://gitea.khongisa.co.za/litecharms/components.git + git + LICENSE + utility;dotnet + icon.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContext.cs similarity index 66% rename from LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContext.cs rename to LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContext.cs index efa8047..19e6697 100644 --- a/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContext.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandShop.Postgres; +namespace LiteCharms.Features.MidrandBooks.Postgres; public class MidrandShopDbContext(DbContextOptions options) : DbContext(options) { diff --git a/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContextFactory.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContextFactory.cs similarity index 85% rename from LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContextFactory.cs rename to LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContextFactory.cs index deb8ff4..6aca9e4 100644 --- a/LiteCharms.Features/MidrandShop/Postgres/MidrandShopDbContextFactory.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContextFactory.cs @@ -1,6 +1,6 @@ -using static LiteCharms.Features.Extensions.Postgres; +using static LiteCharms.Features.MidrandBooks.Extensions.Postgres; -namespace LiteCharms.Features.MidrandShop.Postgres; +namespace LiteCharms.Features.MidrandBooks.Postgres; public class MidrandShopDbContextFactory : IDesignTimeDbContextFactory { diff --git a/LiteCharms.Features.MidrandBooks/appsettings.json b/LiteCharms.Features.MidrandBooks/appsettings.json new file mode 100644 index 0000000..aec5c2e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/appsettings.json @@ -0,0 +1,22 @@ +{ + "Email": { + "Credentials": { + "Username": "shop@litecharms.co.za" + }, + "Port": 465, + "Host": "mail.litecharms.co.za", + "UseSsl": true + }, + "Monitoring": { + "ApiKey": "", + "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", + "ServiceName": "LiteCharms.LeadGenerator" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/LiteCharms.Features.TechShop/Extensions/HealthChecks.cs b/LiteCharms.Features.TechShop/Extensions/HealthChecks.cs index ee2a4a4..a09ffec 100644 --- a/LiteCharms.Features.TechShop/Extensions/HealthChecks.cs +++ b/LiteCharms.Features.TechShop/Extensions/HealthChecks.cs @@ -15,7 +15,7 @@ public static class HealthChecks public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services) { - services.AddHealthChecks().AddCheck(ShopDbConfigName); + services.AddHealthChecks().AddCheck(TechShopDbConfigName); return services; } diff --git a/LiteCharms.Features.TechShop/Extensions/Postgres.cs b/LiteCharms.Features.TechShop/Extensions/Postgres.cs index a264fa2..fdf8555 100644 --- a/LiteCharms.Features.TechShop/Extensions/Postgres.cs +++ b/LiteCharms.Features.TechShop/Extensions/Postgres.cs @@ -4,12 +4,12 @@ namespace LiteCharms.Features.TechShop.Extensions; public static class Postgres { - public const string ShopDbConfigName = "PostgresShop"; + public const string TechShopDbConfigName = "PostgresShop"; public static IServiceCollection AddTechShopDatabase(this IServiceCollection services, IConfiguration configuration) { services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName))); + options.UseNpgsql(configuration.GetConnectionString(TechShopDbConfigName))); return services; } diff --git a/LiteCharms.Features.TechShop/HealthChecks/PostgresShopHealthCheck.cs b/LiteCharms.Features.TechShop/HealthChecks/PostgresShopHealthCheck.cs index db55287..6e69231 100644 --- a/LiteCharms.Features.TechShop/HealthChecks/PostgresShopHealthCheck.cs +++ b/LiteCharms.Features.TechShop/HealthChecks/PostgresShopHealthCheck.cs @@ -4,7 +4,7 @@ namespace LiteCharms.Features.TechShop.HealthChecks; public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthCheck { - private readonly string connectionString = configuration.GetConnectionString(ShopDbConfigName)!; + private readonly string connectionString = configuration.GetConnectionString(TechShopDbConfigName)!; public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { @@ -18,11 +18,11 @@ public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthChec await command.ExecuteScalarAsync(cancellationToken); - return HealthCheckResult.Healthy($"{ShopDbConfigName} is responsive."); + return HealthCheckResult.Healthy($"{TechShopDbConfigName} is responsive."); } catch (Exception ex) { - return HealthCheckResult.Unhealthy($"{ShopDbConfigName} is unreachable.", ex); + return HealthCheckResult.Unhealthy($"{TechShopDbConfigName} is unreachable.", ex); } } } \ No newline at end of file diff --git a/LiteCharms.Features.TechShop/HealthChecks/ShopQuartzHealthCheck.cs b/LiteCharms.Features.TechShop/HealthChecks/ShopQuartzHealthCheck.cs index 5599d26..3fd4e20 100644 --- a/LiteCharms.Features.TechShop/HealthChecks/ShopQuartzHealthCheck.cs +++ b/LiteCharms.Features.TechShop/HealthChecks/ShopQuartzHealthCheck.cs @@ -8,21 +8,21 @@ public class ShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealth { try { - var scheduler = await schedulerFactory.GetScheduler(ShopSchedulerName, cancellationToken); + var scheduler = await schedulerFactory.GetScheduler(TechShopSchedulerName, cancellationToken); if(scheduler == null) - return HealthCheckResult.Unhealthy($"Scheduler with name '{ShopSchedulerName}' not found."); + return HealthCheckResult.Unhealthy($"Scheduler with name '{TechShopSchedulerName}' not found."); if (!scheduler.IsStarted) - return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler is not running"); + return HealthCheckResult.Unhealthy($"{TechShopSchedulerName} Quartz scheduler is not running"); await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken); - return HealthCheckResult.Healthy($"{ShopSchedulerName} Quartz scheduler is ready"); + return HealthCheckResult.Healthy($"{TechShopSchedulerName} Quartz scheduler is ready"); } catch (SchedulerException) { - return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler cannot connect to the store"); + return HealthCheckResult.Unhealthy($"{TechShopSchedulerName} Quartz scheduler cannot connect to the store"); } } } diff --git a/LiteCharms.Features.TechShop/Postgres/ShopDbContextFactory.cs b/LiteCharms.Features.TechShop/Postgres/ShopDbContextFactory.cs index e1e64fd..eda522c 100644 --- a/LiteCharms.Features.TechShop/Postgres/ShopDbContextFactory.cs +++ b/LiteCharms.Features.TechShop/Postgres/ShopDbContextFactory.cs @@ -14,7 +14,7 @@ public class ShopDbContextFactory : IDesignTimeDbContextFactory .Build(); var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName)); + optionsBuilder.UseNpgsql(configuration.GetConnectionString(TechShopDbConfigName)); return new ShopDbContext(optionsBuilder.Options); } diff --git a/LiteCharms.Features/Extensions/Postgres.cs b/LiteCharms.Features/Extensions/Postgres.cs index 549c5a0..8a9541a 100644 --- a/LiteCharms.Features/Extensions/Postgres.cs +++ b/LiteCharms.Features/Extensions/Postgres.cs @@ -1,17 +1,6 @@ -using LiteCharms.Features.MidrandShop.Postgres; - -namespace LiteCharms.Features.Extensions; +namespace LiteCharms.Features.Extensions; public static class Postgres { - public const string MidrandShopDbConfigName = "PostgresMidrandShop"; public const string SchedulerDbConfigName = "PostgresScheduler"; - - public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) - { - services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName))); - - return services; - } } diff --git a/LiteCharms.Features/Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs index 7db12a8..6c4c32e 100644 --- a/LiteCharms.Features/Extensions/Quartz.cs +++ b/LiteCharms.Features/Extensions/Quartz.cs @@ -6,8 +6,8 @@ namespace LiteCharms.Features.Extensions; public static class Quartz { - public const string ShopSchedulerName = "shop"; - public const string MidrandShopSchedulerName = "midrandshop"; + public const string TechShopSchedulerName = "tech-shop"; + public const string MidrandShopSchedulerName = "midrand-shop"; public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration) { diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index a8a890a..3546fc5 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -2,8 +2,13 @@ - - - - + + + + + + + + + From 08a64d157806abc62ceca8ffe57675245db39395 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 24 May 2026 13:42:59 +0200 Subject: [PATCH 051/153] Added test MidrandBooks projet --- .../Fixture.cs | 38 ++++++++++++++ ...eCharms.Features.MidrandBooks.Tests.csproj | 51 +++++++++++++++++++ .../appsettings.json | 34 +++++++++++++ .../LiteCharms.Features.TechShop.Tests.csproj | 6 --- LiteCharmsShared.slnx | 1 + 5 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/Fixture.cs create mode 100644 LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj create mode 100644 LiteCharms.Features.MidrandBooks.Tests/appsettings.json diff --git a/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs b/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs new file mode 100644 index 0000000..38c8bd7 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs @@ -0,0 +1,38 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.MidrandBooks.Extensions; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class Fixture : IDisposable +{ + public IConfiguration Configuration { get; set; } + + public IServiceProvider Services { get; set; } + + public IMediator Mediator { get; set; } + + public Fixture() + { + Configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddUserSecrets() + .AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .Build(); + + Services = new ServiceCollection() + .AddMediator() + .AddLogging() + //.AddMidrandShopServices() + .AddEmailServiceBus() + .AddGarageS3(Configuration) + .AddMidrandShopDatabase(Configuration) + .AddEmailServices(Configuration) + .AddSingleton(Configuration) + .BuildServiceProvider(); + + Mediator = Services.GetRequiredService(); + } + + public void Dispose() { } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj new file mode 100644 index 0000000..824c4ff --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj @@ -0,0 +1,51 @@ + + + + net10.0 + enable + enable + false + b205af96-ceef-44e1-851c-458c9fd1c437 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + Always + + + + \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Tests/appsettings.json b/LiteCharms.Features.MidrandBooks.Tests/appsettings.json new file mode 100644 index 0000000..1066af9 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/appsettings.json @@ -0,0 +1,34 @@ +{ + "BookshopS3Settings": { + "ServiceUrl": "http://192.168.1.177:30900", + "Region": "garage", + "BucketName": "bookshop", + "CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za" + }, + "BookshopQuotesS3Settings": { + "ServiceUrl": "http://192.168.1.177:30900", + "Region": "garage", + "BucketName": "bookshop.quotes", + "CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za" + }, + "Email": { + "Credentials": { + "Username": "shop@litecharms.co.za" + }, + "Port": 465, + "Host": "mail.litecharms.co.za", + "UseSsl": true + }, + "Monitoring": { + "ApiKey": "", + "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", + "ServiceName": "LiteCharms.LeadGenerator" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj b/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj index 5c64f32..8ad847f 100644 --- a/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj +++ b/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj @@ -48,10 +48,4 @@ - - - Always - - - \ No newline at end of file diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index 3546fc5..7b1a9f7 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -8,6 +8,7 @@ + From 87da491ed63aed40a97a80db175fa05c3f0122a1 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 24 May 2026 13:50:41 +0200 Subject: [PATCH 052/153] Refactored connection string name --- .../Extensions/Postgres.cs | 2 +- .../appsettings.json | 2 +- .../LiteCharms.Features.csproj | 5 ----- LiteCharms.Features/appsettings.json | 22 ------------------- 4 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 LiteCharms.Features/appsettings.json diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs index 11e81b0..86173d9 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs @@ -4,7 +4,7 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Postgres { - public const string MidrandShopDbConfigName = "PostgresMidrandShop"; + public const string MidrandShopDbConfigName = "PostgresMidrandBooks"; public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) { diff --git a/LiteCharms.Features.MidrandBooks/appsettings.json b/LiteCharms.Features.MidrandBooks/appsettings.json index aec5c2e..e3261e6 100644 --- a/LiteCharms.Features.MidrandBooks/appsettings.json +++ b/LiteCharms.Features.MidrandBooks/appsettings.json @@ -10,7 +10,7 @@ "Monitoring": { "ApiKey": "", "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", - "ServiceName": "LiteCharms.LeadGenerator" + "ServiceName": "MidrandBooks" }, "Logging": { "LogLevel": { diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 41d7dcf..fab834b 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -155,10 +155,5 @@ - - - PreserveNewest - - diff --git a/LiteCharms.Features/appsettings.json b/LiteCharms.Features/appsettings.json deleted file mode 100644 index aec5c2e..0000000 --- a/LiteCharms.Features/appsettings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "Email": { - "Credentials": { - "Username": "shop@litecharms.co.za" - }, - "Port": 465, - "Host": "mail.litecharms.co.za", - "UseSsl": true - }, - "Monitoring": { - "ApiKey": "", - "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", - "ServiceName": "LiteCharms.LeadGenerator" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} From d55bf4f82f99849004cb3492c53a2ade48856956 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 25 May 2026 22:18:53 +0200 Subject: [PATCH 053/153] Created Author, Book, AuthorBook, Page and Product with Price --- .../AuthorBooks/BooksService.cs | 104 ++++++++ .../AuthorBooks/Entities/AuthorBook.cs | 14 + .../Entities/AuthorBookConfiguration.cs | 28 ++ .../AuthorBooks/Models/AuthorBook.cs | 20 ++ .../Authors/AuthorService.cs | 182 +++++++++++++ .../Authors/Entities/Author.cs | 9 + .../Authors/Entities/AuthorConfiguration.cs | 24 ++ .../Authors/Models/Author.cs | 36 +++ .../Authors/Models/Records.cs | 30 +++ LiteCharms.Features.MidrandBooks/Enums.cs | 62 +++++ .../Extensions/HealthChecks.cs | 2 +- .../Extensions/Mappers.cs | 84 ++++++ .../Extensions/Postgres.cs | 6 +- .../PostgresMidrandShopHealthCheck.cs | 6 +- .../Pages/Entities/BookPage.cs | 9 + .../Pages/Entities/BookPageConfiguration.cs | 26 ++ .../Pages/Models/BookPage.cs | 28 ++ .../Pages/Models/CreateBookPage.cs | 18 ++ .../Pages/Models/UpdateBookPage.cs | 3 + .../Pages/PagesService.cs | 225 ++++++++++++++++ .../Postgres/MidrandBooksDbContext.cs | 19 ++ ...ory.cs => MidrandBooksDbContextFactory.cs} | 12 +- .../Postgres/MidrandShopDbContext.cs | 6 - .../Products/Entities/Product.cs | 7 + .../Products/Entities/ProductConfiguration.cs | 22 ++ .../Products/Entities/ProductPrice.cs | 7 + .../Entities/ProductPriceConfiguration.cs | 22 ++ .../Products/Models/CreateProductPrice.cs | 10 + .../Products/Models/Product.cs | 30 +++ .../Products/Models/ProductPrice.cs | 18 ++ .../Products/Models/Records.cs | 22 ++ .../Products/ProductService.cs | 252 ++++++++++++++++++ .../Customers/CustomerService.cs | 3 +- .../Products/Models/Product.cs | 4 +- LiteCharms.Features/Enums.cs | 13 + LiteCharms.Features/Models/PageReference.cs | 10 + LiteCharms.Features/Models/ProductFilter.cs | 18 ++ .../Models/ProductMetadata.cs | 2 +- LiteCharms.Features/Models/SocialMedia.cs | 13 + 39 files changed, 1383 insertions(+), 23 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs create mode 100644 LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs create mode 100644 LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs create mode 100644 LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs create mode 100644 LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs create mode 100644 LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs create mode 100644 LiteCharms.Features.MidrandBooks/Enums.cs create mode 100644 LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/Entities/BookPage.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs create mode 100644 LiteCharms.Features.MidrandBooks/Pages/PagesService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs rename LiteCharms.Features.MidrandBooks/Postgres/{MidrandShopDbContextFactory.cs => MidrandBooksDbContextFactory.cs} (58%) delete mode 100644 LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContext.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/Product.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/ProductPrice.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/Records.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/ProductService.cs create mode 100644 LiteCharms.Features/Models/PageReference.cs create mode 100644 LiteCharms.Features/Models/ProductFilter.cs rename {LiteCharms.Features.TechShop/Products => LiteCharms.Features}/Models/ProductMetadata.cs (79%) create mode 100644 LiteCharms.Features/Models/SocialMedia.cs diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs new file mode 100644 index 0000000..f3ba48b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -0,0 +1,104 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.AuthorBooks; + +public class BooksService(IDbContextFactory contextFactory) +{ + public async ValueTask UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var book = await context.Books.FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); + + if (book is null) + return Result.Fail(new Error($"Book with ID {bookId} not found")); + + book.UpdatedAt = DateTime.UtcNow; + book.Enabled = isEnabled; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to change status of book with ID {bookId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> PublishBookAsync(long authorId, long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken)) + return Result.Fail("Author not found."); + + if (!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken)) + return Result.Fail("Product not found."); + + var book = context.Books.Add(new Entities.AuthorBook + { + AuthorId = authorId, + ProductId = productId, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(book.Entity.Id) + : Result.Fail("Failed to create book."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetBookAsync(long bookId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var book = await context.Books + .AsNoTracking().FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); + + return book is null + ? Result.Fail(new Error($"Book with ID {bookId} not found")) + : Result.Ok(book.ToModel()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetBooksByAuthorAsync(long authorId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken)) + return Result.Fail(new Error($"Author with ID {authorId} not found")); + + var books = await context.Books + .AsNoTracking() + .OrderByDescending(b => b.CreatedAt) + .Where(b => b.AuthorId == authorId) + .ToListAsync(cancellationToken); + + return books?.Count > 0 + ? Result.Ok(books.Select(b => b.ToModel()).ToArray()) + : Result.Fail(new Error($"No books found for author with ID {authorId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs new file mode 100644 index 0000000..9c44f39 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs @@ -0,0 +1,14 @@ +using LiteCharms.Features.MidrandBooks.Authors.Entities; +using LiteCharms.Features.MidrandBooks.Pages.Entities; +using LiteCharms.Features.MidrandBooks.Products.Entities; + +namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +public class AuthorBook : Models.AuthorBook +{ + public virtual Author Author { get; set; } = new(); + + public virtual Product Book { get; set; } = new(); + + public virtual ICollection Pages { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs new file mode 100644 index 0000000..1f18c56 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs @@ -0,0 +1,28 @@ +namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +public class AuthorBookConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Books"); + + builder.HasKey(f => f.AuthorId); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.AuthorId).IsRequired(); + builder.Property(f => f.ProductId).IsRequired(); + builder.Property(f => f.Rating).IsRequired(false); + builder.Property(f => f.Ranking).IsRequired(false); + builder.Property(f => f.Enabled).HasDefaultValue(true); + + builder.HasOne(f => f.Author) + .WithMany(a => a.Books) + .HasForeignKey(f => f.AuthorId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Book) + .WithMany() + .HasForeignKey(f => f.ProductId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs new file mode 100644 index 0000000..5d77fe9 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models; + +public class AuthorBook +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long AuthorId { get; set; } + + public long ProductId { get; set; } + + public int Rating { get; set; } + + public int Ranking { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs new file mode 100644 index 0000000..b9cc707 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -0,0 +1,182 @@ +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.MidrandBooks.Products.Models; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Authors; + +public class AuthorService(IDbContextFactory contextFactory) +{ + public async ValueTask> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); + + if (author is null) + return Result.Fail(new Error($"Author with ID {authorId} not found")); + + var books = await context.Books.AsNoTracking() + .OrderByDescending(b => b.CreatedAt) + .Where(p => p.AuthorId == authorId) + .Select(p => p.Book.ToModel()) + .ToArrayAsync(cancellationToken); + + return books?.Length > 0 + ? Result.Ok(books) + : Result.Fail(new Error($"No books found for author with ID {authorId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); + + if (author is null) + return Result.Fail(new Error($"Author with ID {authorId} not found")); + + author.UpdatedAt = DateTime.UtcNow; + author.Enabled = isEnabled; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to change status of author with ID {authorId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAuthorAsync(long authorId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); + + return author is not null + ? Result.Ok(author.ToModel()) + : Result.Fail(new Error($"Author with ID {authorId} not found")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAuthors(DateRange range, CancellationToken cancellationToken) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var authors = await context.Authors.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .ThenByDescending(o => o.UpdatedAt) + .Where(a => a.CreatedAt >= fromDate && a.CreatedAt <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return authors?.Length > 0 + ? Result.Ok(authors.Select(a => a.ToModel()).ToArray()) + : Result.Fail(new Error("No authors found in the specified date range.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken)) + return Result.Fail(new Error($"An author with the name {request.Name} {request.LastName} already exists")); + + if (await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken)) + return Result.Fail(new Error($"An author with the email {request.Email} already exists")); + + var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); + + if (author is null) + return Result.Fail(new Error($"Author with ID {authorId} not found")); + + author.UpdatedAt = DateTime.UtcNow; + author.PublisherType = request.PublisherType; + author.Company = request.Company; + author.VatNumber = request.VatNumber; + author.Name = request.Name; + author.LastName = request.LastName; + author.Biography = request.Biography; + author.Email = request.Email; + author.Website = request.Website; + author.ImageUrl = request.ImageUrl; + author.ThumbnailImageUrl = request.ThumbnailImageUrl; + author.SocialMedia = request.SocialMedia; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update author with ID {authorId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateAuthorAsync(CreateAuthor request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken)) + return Result.Fail(new Error($"An author with the name {request.Name} {request.LastName} already exists")); + + if(await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken)) + return Result.Fail(new Error($"An author with the email {request.Email} already exists")); + + var newAuthor = context.Authors.Add(new Entities.Author + { + Company = request.Company, + VatNumber = request.VatNumber, + PublisherType = request.PublisherType, + Name = request.Name, + LastName = request.LastName, + Biography = request.Biography, + Email = request.Email, + Website = request.Website, + ImageUrl = request.ImageUrl, + ThumbnailImageUrl = request.ThumbnailImageUrl, + SocialMedia = request.SocialMedia + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newAuthor.Entity.Id) + : Result.Fail(new Error($"Failed to create author {request.Name} {request.LastName}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs new file mode 100644 index 0000000..60b6a2b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs @@ -0,0 +1,9 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +namespace LiteCharms.Features.MidrandBooks.Authors.Entities; + +[EntityTypeConfiguration] +public class Author : Models.Author +{ + public ICollection Books { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs new file mode 100644 index 0000000..cd6eda1 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs @@ -0,0 +1,24 @@ +namespace LiteCharms.Features.MidrandBooks.Authors.Entities; + +public class AuthorConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Authors"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.PublisherType).IsRequired(); + builder.Property(f => f.VatNumber).IsRequired(false).HasMaxLength(255); + builder.Property(f => f.Name).IsRequired().HasMaxLength(255); + builder.Property(f => f.LastName).IsRequired().HasMaxLength(255); + builder.Property(f => f.Biography).IsRequired(false).HasMaxLength(2048); + builder.Property(f => f.Email).IsRequired().HasMaxLength(512); + builder.Property(f => f.Website).IsRequired(false).HasMaxLength(1024); + builder.Property(f => f.ImageUrl).IsRequired().HasMaxLength(2048); + builder.Property(f => f.ThumbnailImageUrl).IsRequired(false).HasMaxLength(2048); + builder.Property(f => f.SocialMedia).IsRequired(false).HasColumnType("jsonb"); + builder.Property(f => f.Enabled).HasDefaultValue(true); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs b/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs new file mode 100644 index 0000000..0546a7b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs @@ -0,0 +1,36 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Authors.Models; + +public class Author +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public PublisherTypes PublisherType { get; set; } + + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public string? Name { get; set; } + + public string? LastName { get; set; } + + public string? Biography { get; set; } + + public string? Email { get; set; } + + public string? Website { get; set; } + + public string? ImageUrl { get; set; } + + public string? ThumbnailImageUrl { get; set; } + + public SocialMedia[]? SocialMedia { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs new file mode 100644 index 0000000..ba54191 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs @@ -0,0 +1,30 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Authors.Models; + +public record UpdateAuthor : CreateAuthor; + +public record CreateAuthor +{ + public required PublisherTypes PublisherType { get; set; } + + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public required string Name { get; set; } + + public required string LastName { get; set; } + + public string? Biography { get; set; } + + public required string Email { get; set; } + + public string? Website { get; set; } + + public required string ImageUrl { get; set; } + + public string? ThumbnailImageUrl { get; set; } + + public SocialMedia[]? SocialMedia { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Enums.cs b/LiteCharms.Features.MidrandBooks/Enums.cs new file mode 100644 index 0000000..b3325c2 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Enums.cs @@ -0,0 +1,62 @@ +namespace LiteCharms.Features.MidrandBooks; + +public enum PublisherTypes : int +{ + Individual = 0, + Company = 1, + Organization = 2, + SelfPublished = 3, + UniversityPress = 4, + GovernmentAgency = 5, + NonProfit = 6, + Independent = 7 +} + +public enum BookTypes : int +{ + Fiction = 0, + NonFiction = 1, + Academic = 2, + SelfHelp = 3, + Biography = 4, + Poetry = 5, + Children = 6, + YoungAdult = 7, + ScienceFiction = 8, + Fantasy = 9 +} + +public enum BookContentTypes : int +{ + Text = 0, + Image = 1, + Video = 2, + Audio = 3, + Interactive = 4, + Markdown = 5, + Html = 6, + Json = 7, + Yaml = 8 +} + +public enum BookPageTypes : int +{ + Cover = 0, + Preface = 1, + Introduction = 2, + Content = 3, + Closing = 4, + Referencer = 5, + Credits = 6, + BackCover = 7 +} + +public enum ProductTypes : int +{ + Book = 1, + Journal = 2, + Magazine = 3, + EBook = 4, + Audiobook = 5, + Accessory = 6 +} diff --git a/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs b/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs index fbc3e1f..bf35ee8 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs @@ -15,7 +15,7 @@ public static class HealthChecks public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services) { - services.AddHealthChecks().AddCheck(MidrandShopDbConfigName); + services.AddHealthChecks().AddCheck(MidrandBooksDbConfigName); return services; } diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs new file mode 100644 index 0000000..21607dc --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -0,0 +1,84 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Pages.Models; +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.Extensions; + +public static class Mappers +{ + public static BookPage ToModel(this Pages.Entities.BookPage entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + AuthorBookId = entity.AuthorBookId, + Content = entity.Content, + ContentType = entity.ContentType, + Number = entity.Number, + Enabled = entity.Enabled, + Notes = entity.Notes, + References = entity.References, + Type = entity.Type + }; + + public static AuthorBook ToModel(this AuthorBooks.Entities.AuthorBook entity) => new() + { + Id = entity.Id, + ProductId = entity.ProductId, + CreatedAt = entity.CreatedAt, + AuthorId = entity.AuthorId, + Ranking = entity.Ranking, + Rating = entity.Rating, + Enabled = entity.Enabled + }; + + public static ProductPrice ToModel(this Products.Entities.ProductPrice entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + ProductId = entity.ProductId, + Amount = entity.Amount, + Discount = entity.Discount, + Enabled = entity.Enabled + }; + + public static Product ToModel(this Products.Entities.Product entity) + { + return new Product + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Name = entity.Name, + Summary = entity.Summary, + Description = entity.Description, + Type = entity.Type, + ImageUrl = entity.ImageUrl, + ThumbnailUrls = entity.ThumbnailUrls, + Metadata = entity.Metadata, + Categories = entity.Categories, + Enabled = entity.Enabled + }; + } + + public static Author ToModel(this Authors.Entities.Author entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Name = entity.Name, + LastName = entity.LastName, + Biography = entity.Biography, + Email = entity.Email, + Website = entity.Website, + ImageUrl = entity.ImageUrl, + ThumbnailImageUrl = entity.ThumbnailImageUrl, + SocialMedia = entity.SocialMedia, + Enabled = entity.Enabled, + Company = entity.Company, + PublisherType = entity.PublisherType, + VatNumber = entity.VatNumber + }; +} diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs index 86173d9..228634d 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs @@ -4,12 +4,12 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Postgres { - public const string MidrandShopDbConfigName = "PostgresMidrandBooks"; + public const string MidrandBooksDbConfigName = "PostgresMidrandBooks"; public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) { - services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName))); + services.AddPooledDbContextFactory(options => + options.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName))); return services; } diff --git a/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs index e60ef2c..5a8b44e 100644 --- a/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs +++ b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs @@ -4,7 +4,7 @@ namespace LiteCharms.Features.MidrandBooks.HealthChecks; public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck { - private readonly string connectionString = configuration.GetConnectionString(MidrandShopDbConfigName)!; + private readonly string connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName)!; public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { @@ -18,11 +18,11 @@ public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHea await command.ExecuteScalarAsync(cancellationToken); - return HealthCheckResult.Healthy($"{MidrandShopDbConfigName} is responsive."); + return HealthCheckResult.Healthy($"{MidrandBooksDbConfigName} is responsive."); } catch (Exception ex) { - return HealthCheckResult.Unhealthy($"{MidrandShopDbConfigName} is unreachable.", ex); + return HealthCheckResult.Unhealthy($"{MidrandBooksDbConfigName} is unreachable.", ex); } } } \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPage.cs new file mode 100644 index 0000000..bba4436 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPage.cs @@ -0,0 +1,9 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +namespace LiteCharms.Features.MidrandBooks.Pages.Entities; + +[EntityTypeConfiguration] +public class BookPage : Models.BookPage +{ + public virtual AuthorBook Book { get; set; } = new(); +} diff --git a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs new file mode 100644 index 0000000..48c8196 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.MidrandBooks.Pages.Entities; + +public class BookPageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("BookPages"); + + builder.HasKey(bp => bp.Id); + builder.Property(bp => bp.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(bp => bp.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(bp => bp.Number).IsRequired().HasDefaultValue(0); + builder.Property(bp => bp.AuthorBookId).IsRequired(); + builder.Property(bp => bp.Content).IsRequired(); + builder.Property(bp => bp.Type).IsRequired(); + builder.Property(bp => bp.ContentType).IsRequired(); + builder.Property(bp => bp.Notes).IsRequired(false).HasColumnType("jsonb"); + builder.Property(bp => bp.References).IsRequired(false).HasColumnType("jsonb"); + builder.Property(bp => bp.Enabled).HasDefaultValue(true); + + builder.HasOne(f =>f.Book) + .WithMany(b => b.Pages) + .HasForeignKey(f => f.AuthorBookId) + .OnDelete(DeleteBehavior.NoAction); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs new file mode 100644 index 0000000..d95cfa1 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs @@ -0,0 +1,28 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Pages.Models; + +public class BookPage +{ + public long Id { get; set; } + + public long AuthorBookId { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public BookPageTypes Type { get; set; } + + public BookContentTypes ContentType { get; set; } + + public int Number { get; set; } + + public byte[]? Content { get; set; } + + public string[]? Notes { get; set; } + + public PageReference[]? References { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs new file mode 100644 index 0000000..d4af2e7 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs @@ -0,0 +1,18 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Pages.Models; + +public class CreateBookPage +{ + public BookPageTypes Type { get; set; } + + public BookContentTypes ContentType { get; set; } + + public int Number { get; set; } + + public byte[]? Content { get; set; } + + public string[]? Notes { get; set; } + + public PageReference[]? References { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs new file mode 100644 index 0000000..d78b732 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs @@ -0,0 +1,3 @@ +namespace LiteCharms.Features.MidrandBooks.Pages.Models; + +public class UpdateBookPage : CreateBookPage; diff --git a/LiteCharms.Features.MidrandBooks/Pages/PagesService.cs b/LiteCharms.Features.MidrandBooks/Pages/PagesService.cs new file mode 100644 index 0000000..18c822b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/PagesService.cs @@ -0,0 +1,225 @@ +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Pages.Models; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Pages; + +public class PagesService(IDbContextFactory contextFactory) +{ + public async ValueTask DeleteAllAsync(long authorBookId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken)) + return Result.Fail("Book not found"); + + var pages = await context.Pages.Where(p => p.AuthorBookId == authorBookId).ToListAsync(cancellationToken); + + if (pages.Count == 0) + return Result.Fail("No pages found for the specified book"); + + context.Pages.RemoveRange(pages); + + await context.SaveChangesAsync(cancellationToken); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeleteByPageTypeAsync(long authorBookId, int pageNumber, BookPageTypes pageType, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType, cancellationToken); + + if (page is null) + return Result.Fail("Page not found"); + + context.Pages.Remove(page); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to delete page"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdatePageStatusAsync(long bookPageId, bool enabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); + + if (page is null) + return Result.Fail("Page not found"); + + page.UpdatedAt = DateTime.UtcNow; + page.Enabled = enabled; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to update page status"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeletePageAsync(long bookPageId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); + + if (page is null) + return Result.Fail("Page not found"); + + context.Pages.Remove(page); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to delete page"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdatePageAsync(long bookPageId, UpdateBookPage request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); + + if (page is null) + return Result.Fail("Page not found"); + + page.UpdatedAt = DateTime.UtcNow; + page.Type = request.Type; + page.ContentType = request.ContentType; + page.Number = request.Number; + page.Content = request.Content; + page.Notes = request.Notes; + page.References = request.References; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to update page"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreatePageAsync(long authorBookId, CreateBookPage request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken)) + return Result.Fail("Book not found"); + + if (await context.Pages.AnyAsync(p => p.AuthorBookId == authorBookId && p.Number == request.Number && p.Type == request.Type, cancellationToken)) + return Result.Fail("A page with the same number already exists for this book"); + + var page = context.Pages.Add(new Entities.BookPage + { + AuthorBookId = authorBookId, + Type = request.Type, + ContentType = request.ContentType, + Number = request.Number, + Content = request.Content, + Notes = request.Notes, + References = request.References, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(page.Entity.Id) + : Result.Fail("Failed to create page"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPagesAsync(long authorBookId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken)) + return Result.Fail("Book not found"); + + var pages = await context.Pages.AsNoTracking() + .Where(p => p.AuthorBookId == authorBookId).ToArrayAsync(cancellationToken); + + return pages?.Length > 0 + ? Result.Ok(pages.Select(p => p.ToModel()).ToArray()) + : Result.Fail("No pages found for the specified book"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPageByNumberAsync(long pageId, int number, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == pageId && p.Number == number, cancellationToken); + + return page is not null + ? page.ToModel() + : Result.Fail("Page not found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPageAsync(long pageId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == pageId, cancellationToken); + + return page is not null + ? page.ToModel() + : Result.Fail("Page not found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs new file mode 100644 index 0000000..1bb907d --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -0,0 +1,19 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; +using LiteCharms.Features.MidrandBooks.Authors.Entities; +using LiteCharms.Features.MidrandBooks.Pages.Entities; +using LiteCharms.Features.MidrandBooks.Products.Entities; + +namespace LiteCharms.Features.MidrandBooks.Postgres; + +public class MidrandBooksDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Authors => Set(); + + public DbSet Products => Set(); + + public DbSet Prices => Set(); + + public DbSet Books => Set(); + + public DbSet Pages => Set(); +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContextFactory.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs similarity index 58% rename from LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContextFactory.cs rename to LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs index 6aca9e4..e518cfd 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContextFactory.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs @@ -2,20 +2,20 @@ namespace LiteCharms.Features.MidrandBooks.Postgres; -public class MidrandShopDbContextFactory : IDesignTimeDbContextFactory +public class MidrandBooksDbContextFactory : IDesignTimeDbContextFactory { - public MidrandShopDbContext CreateDbContext(string[] args) + public MidrandBooksDbContext CreateDbContext(string[] args) { var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) - .AddUserSecrets(typeof(MidrandShopDbContext).Assembly) + .AddUserSecrets(typeof(MidrandBooksDbContext).Assembly) .AddJsonFile("appsettings.json") .AddEnvironmentVariables() .Build(); - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName)); + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName)); - return new MidrandShopDbContext(optionsBuilder.Options); + return new MidrandBooksDbContext(optionsBuilder.Options); } } diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContext.cs deleted file mode 100644 index 19e6697..0000000 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandShopDbContext.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace LiteCharms.Features.MidrandBooks.Postgres; - -public class MidrandShopDbContext(DbContextOptions options) : DbContext(options) -{ - -} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs new file mode 100644 index 0000000..e7e82c6 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +[EntityTypeConfiguration] +public class Product : Models.Product +{ + public virtual ICollection Prices { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs new file mode 100644 index 0000000..71515b2 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public class ProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Products"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.Type).IsRequired(); + builder.Property(f => f.Name).IsRequired().HasMaxLength(255); + builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); + builder.Property(f => f.Description).HasMaxLength(1024); + builder.Property(f => f.ImageUrl).HasMaxLength(1024); + builder.Property(f => f.ThumbnailUrls).IsRequired(false).HasColumnType("jsonb"); + builder.Property(f => f.Metadata).IsRequired(false).HasColumnType("jsonb"); + builder.Property(f => f.Categories).IsRequired(false).HasColumnType("jsonb"); + builder.Property(f => f.Enabled).HasDefaultValue(false); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs new file mode 100644 index 0000000..e78c42b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +[EntityTypeConfiguration] +public class ProductPrice : Models.ProductPrice +{ + public virtual Product Product { get; set; } = new(); +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs new file mode 100644 index 0000000..4705ba5 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public class ProductPriceConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Prices"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.ProductId).IsRequired(); + builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Discount).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Enabled).HasDefaultValue(false); + + builder.HasOne(f => f.Product) + .WithMany(p => p.Prices) + .HasForeignKey(f => f.ProductId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs new file mode 100644 index 0000000..2fec81e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class CreateProductPrice +{ + public long ProductId { get; set; } + + public decimal Amount { get; set; } + + public decimal Discount { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs new file mode 100644 index 0000000..42a977f --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs @@ -0,0 +1,30 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class Product +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public ProductTypes Type { get; set; } + + public string? Name { get; set; } + + public string? Summary { get; set; } + + public string? Description { get; set; } + + public string? ImageUrl { get; set; } + + public string[]? ThumbnailUrls { get; set; } + + public string[]? Categories { get; set; } + + public ProductMetadata? Metadata { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/ProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Models/ProductPrice.cs new file mode 100644 index 0000000..a297068 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/ProductPrice.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class ProductPrice +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long ProductId { get; set; } + + public decimal Amount { get; set; } + + public decimal Discount { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs new file mode 100644 index 0000000..642a739 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs @@ -0,0 +1,22 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public record CreateProduct +{ + public required ProductTypes Type { get; set; } + + public required string Name { get; set; } + + public required string Summary { get; set; } + + public required string Description { get; set; } + + public required string ImageUrl { get; set; } + + public string[]? ThumbnailUrls { get; set; } + + public string[]? Categories { get; set; } + + public ProductMetadata? Metadata { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs new file mode 100644 index 0000000..5ee455f --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -0,0 +1,252 @@ +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.MidrandBooks.Products.Models; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Products; + +public class ProductService(IDbContextFactory contextFactory) +{ + public async ValueTask UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var productPrice = await context.Prices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + + if (productPrice is null) + return Result.Fail(new Error($"Product price with ID {productPriceId} not found")); + + productPrice.UpdatedAt = DateTime.UtcNow; + productPrice.Enabled = isEnabled; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to change status of product price with ID {productPriceId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateProductStatusAsync(long productId, bool isEnabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + + if (product is null) + return Result.Fail(new Error($"Product with ID {productId} not found")); + + product.UpdatedAt = DateTime.UtcNow; + product.Enabled = isEnabled; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to change status of product with ID {productId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> SearchProductsAsync(ProductFilter filter, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var query = context.Products.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(filter.Title)) + query = query.Where(p => p.Name!.Contains(filter.Title)); + + if (!string.IsNullOrWhiteSpace(filter.Category)) + query = query.Where(p => p.Categories!.Any(c => c == filter.Category)); + + if (!string.IsNullOrWhiteSpace(filter.Manufacturer)) + query = query.Where(p => p.Metadata!.Manufacturer == filter.Manufacturer); + + if (!string.IsNullOrWhiteSpace(filter.SerialNumber)) + query = query.Where(p => p.Metadata!.SerialNumber == filter.SerialNumber); + + if (filter.MinPrice > 0) + query = query.Where(p => p.Prices!.Any(pr => pr.Amount >= filter.MinPrice && pr.Amount <= filter.MaxPrice)); + + var products = await query.AsNoTracking().Where(p => p.Enabled).ToListAsync(cancellationToken); + + return products?.Count > 0 + ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) + : Result.Fail("No products found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateProductPriceAsync(long productId, CreateProductPrice request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken)) + return Result.Fail($"Product with ID {productId} does not exist."); + + var existingPrices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken); + + if (existingPrices.Count > 0) + foreach (var existingPrice in existingPrices) + { + existingPrice.Enabled = false; + existingPrice.UpdatedAt = DateTime.UtcNow; + context.Prices.Update(existingPrice); + } + + var price = context.Prices.Add(new Entities.ProductPrice + { + ProductId = productId, + Amount = request.Amount, + Discount = request.Discount, + Enabled = true, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(price.Entity.Id) + : Result.Fail("Failed to create product price."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateProductAsync(CreateProduct request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken)) + return Result.Fail("A product with the same name already exists."); + + if(request.Metadata is not null) + if (await context.Products.AnyAsync(p => p.Metadata!.SerialNumber == request.Metadata.SerialNumber, cancellationToken)) + return Result.Fail("A product with the same metadata already exists."); + + var product = context.Products.Add(new Entities.Product + { + UpdatedAt = DateTime.UtcNow, + Type = request.Type, + Name = request.Name, + Summary = request.Summary, + Description = request.Description, + ImageUrl = request.ImageUrl, + ThumbnailUrls = request.ThumbnailUrls, + Metadata = request.Metadata, + Categories = request.Categories, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(product.Entity.Id) + : Result.Fail("Failed to create product."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Prices + .AsNoTracking() + .OrderByDescending(p => p.CreatedAt) + .ThenBy(p => p.UpdatedAt) + .FirstOrDefaultAsync(p => p.ProductId == productId, cancellationToken); + + return product is not null + ? Result.Ok(new[] { product.ToModel() }) + : Result.Fail(new Error($"No price found for product with ID {productId}")); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductPricesAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var prices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken); + + return prices?.Count > 0 + ? prices.Select(p => p.ToModel()).ToArray() + : Result.Fail(new Error($"No prices found for product with ID {productId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + + return product is null + ? Result.Fail(new Error($"Product with ID {productId} not found.")) + : Result.Ok(product.ToModel()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductsAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var products = await context.Products + .AsNoTracking() + .OrderByDescending(p => p.CreatedAt) + .ThenBy(p => p.UpdatedAt) + .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) + : Result.Fail(new Error("Failed to retrieve products.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.TechShop/Customers/CustomerService.cs b/LiteCharms.Features.TechShop/Customers/CustomerService.cs index c8ea2cd..c4983c6 100644 --- a/LiteCharms.Features.TechShop/Customers/CustomerService.cs +++ b/LiteCharms.Features.TechShop/Customers/CustomerService.cs @@ -1,5 +1,4 @@ -using LiteCharms.Features.Extensions; -using LiteCharms.Features.Models; +using LiteCharms.Features.Models; using LiteCharms.Features.TechShop.Customers.Models; using LiteCharms.Features.TechShop.Extensions; using LiteCharms.Features.TechShop.Postgres; diff --git a/LiteCharms.Features.TechShop/Products/Models/Product.cs b/LiteCharms.Features.TechShop/Products/Models/Product.cs index b5fd71b..db9dba7 100644 --- a/LiteCharms.Features.TechShop/Products/Models/Product.cs +++ b/LiteCharms.Features.TechShop/Products/Models/Product.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.TechShop.Products.Models; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.TechShop.Products.Models; public class Product { diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs index 29a915f..510d826 100644 --- a/LiteCharms.Features/Enums.cs +++ b/LiteCharms.Features/Enums.cs @@ -1,5 +1,18 @@ namespace LiteCharms.Features; +public enum SocialMediaTypes : int +{ + Twitter = 0, + Facebook = 1, + Instagram = 2, + LinkedIn = 3, + TikTok = 4, + YouTube = 5, + Pinterest = 6, + Reddit = 7, + Tumblr = 8 +} + public enum EmailStatuses : int { GeneralError = 0, diff --git a/LiteCharms.Features/Models/PageReference.cs b/LiteCharms.Features/Models/PageReference.cs new file mode 100644 index 0000000..ab04eb9 --- /dev/null +++ b/LiteCharms.Features/Models/PageReference.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.Models; + +public class PageReference +{ + public string? Tag { get; set; } + + public string? Description { get; set; } + + public string? Url { get; set; } +} diff --git a/LiteCharms.Features/Models/ProductFilter.cs b/LiteCharms.Features/Models/ProductFilter.cs new file mode 100644 index 0000000..c6cc275 --- /dev/null +++ b/LiteCharms.Features/Models/ProductFilter.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.Models; + +public class ProductFilter +{ + public string? Name { get; set; } + + public string? Title { get; set; } + + public string? Category { get; set; } + + public string? Manufacturer { get; set; } + + public string? SerialNumber { get; set; } + + public decimal MinPrice { get; set; } + + public decimal MaxPrice { get; set; } +} diff --git a/LiteCharms.Features.TechShop/Products/Models/ProductMetadata.cs b/LiteCharms.Features/Models/ProductMetadata.cs similarity index 79% rename from LiteCharms.Features.TechShop/Products/Models/ProductMetadata.cs rename to LiteCharms.Features/Models/ProductMetadata.cs index 168f2bd..ee193fe 100644 --- a/LiteCharms.Features.TechShop/Products/Models/ProductMetadata.cs +++ b/LiteCharms.Features/Models/ProductMetadata.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.TechShop.Products.Models; +namespace LiteCharms.Features.Models; public class ProductMetadata { diff --git a/LiteCharms.Features/Models/SocialMedia.cs b/LiteCharms.Features/Models/SocialMedia.cs new file mode 100644 index 0000000..296d979 --- /dev/null +++ b/LiteCharms.Features/Models/SocialMedia.cs @@ -0,0 +1,13 @@ + +namespace LiteCharms.Features.Models; + +public class SocialMedia +{ + public SocialMediaTypes Type { get; set; } + + public string? Name { get; set; } + + public string? ImageUrl { get; set; } + + public string? Url { get; set; } +} From 4a85d01d1acf1633cbfbf8040763940586b5559a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 25 May 2026 23:00:17 +0200 Subject: [PATCH 054/153] Included navigation fields in get queries --- .../AuthorBooks/BooksService.cs | 40 ++++++++++++++++++- .../AuthorBooks/Entities/AuthorBook.cs | 2 +- .../Entities/AuthorBookConfiguration.cs | 2 +- .../AuthorBooks/Models/AuthorBook.cs | 6 ++- .../Authors/AuthorService.cs | 20 ++++++---- .../Extensions/Mappers.cs | 6 ++- .../Products/Models/Product.cs | 2 + .../Products/ProductService.cs | 17 ++++---- 8 files changed, 73 insertions(+), 22 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index f3ba48b..2fdfe2b 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -30,7 +30,7 @@ public class BooksService(IDbContextFactory contextFactor } } - public async ValueTask> PublishBookAsync(long authorId, long productId, CancellationToken cancellationToken = default) + public async ValueTask> CreateBookAsync(long authorId, long productId, CancellationToken cancellationToken = default) { try { @@ -65,7 +65,11 @@ public class BooksService(IDbContextFactory contextFactor await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var book = await context.Books - .AsNoTracking().FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); + .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product!.Price) + .Include(b => b.Pages) + .FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); return book is null ? Result.Fail(new Error($"Book with ID {bookId} not found")) @@ -88,6 +92,8 @@ public class BooksService(IDbContextFactory contextFactor var books = await context.Books .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product!.Price) .OrderByDescending(b => b.CreatedAt) .Where(b => b.AuthorId == authorId) .ToListAsync(cancellationToken); @@ -101,4 +107,34 @@ public class BooksService(IDbContextFactory contextFactor return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } + + public async ValueTask> GetPublishedBooksAsync(int offset, int limit, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var books = await context.Books + .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product!.Price) + .Include(b => b.Pages) + .Where(b => b.Enabled && b.Product!.Enabled && b.Author.Enabled) + .OrderByDescending(b => b.Ranking) + .ThenByDescending(b => b.Ranking) + .ThenByDescending(b => b.CreatedAt) + .ThenByDescending(b => b.UpdatedAt) + .Skip(offset).Take(limit) + .AsSplitQuery() + .ToArrayAsync(cancellationToken); + + return books?.Length > 0 + ? Result.Ok(books.Select(b => b.ToModel()).ToArray()) + : Result.Fail(new Error("No published books found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } } diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs index 9c44f39..c1282e8 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs @@ -8,7 +8,7 @@ public class AuthorBook : Models.AuthorBook { public virtual Author Author { get; set; } = new(); - public virtual Product Book { get; set; } = new(); + public new virtual Product? Product { get; set; } public virtual ICollection Pages { get; set; } = []; } diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs index 1f18c56..3f852ac 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs @@ -20,7 +20,7 @@ public class AuthorBookConfiguration : IEntityTypeConfiguration .HasForeignKey(f => f.AuthorId) .OnDelete(DeleteBehavior.Restrict); - builder.HasOne(f => f.Book) + builder.HasOne(f => f.Product) .WithMany() .HasForeignKey(f => f.ProductId) .OnDelete(DeleteBehavior.Restrict); diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs index 5d77fe9..6ac8dce 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models; public class AuthorBook { @@ -16,5 +18,7 @@ public class AuthorBook public int Ranking { get; set; } + public Product? Product { get; set; } + public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs index b9cc707..3897349 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -1,4 +1,5 @@ -using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Products.Models; @@ -8,7 +9,7 @@ namespace LiteCharms.Features.MidrandBooks.Authors; public class AuthorService(IDbContextFactory contextFactory) { - public async ValueTask> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) + public async ValueTask> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) { try { @@ -17,21 +18,24 @@ public class AuthorService(IDbContextFactory contextFacto var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); if (author is null) - return Result.Fail(new Error($"Author with ID {authorId} not found")); + return Result.Fail(new Error($"Author with ID {authorId} not found")); - var books = await context.Books.AsNoTracking() + var books = await context.Books + .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product!.Price) .OrderByDescending(b => b.CreatedAt) .Where(p => p.AuthorId == authorId) - .Select(p => p.Book.ToModel()) + .AsSplitQuery() .ToArrayAsync(cancellationToken); return books?.Length > 0 - ? Result.Ok(books) - : Result.Fail(new Error($"No books found for author with ID {authorId}")); + ? Result.Ok(books.Select(b => b.ToModel()).ToArray()) + : Result.Fail(new Error($"No books found for author with ID {authorId}")); } catch (Exception ex) { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); + return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 21607dc..4ec3897 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -30,7 +30,8 @@ public static class Mappers AuthorId = entity.AuthorId, Ranking = entity.Ranking, Rating = entity.Rating, - Enabled = entity.Enabled + Enabled = entity.Enabled, + Product = entity.Product?.ToModel(), }; public static ProductPrice ToModel(this Products.Entities.ProductPrice entity) => new() @@ -59,7 +60,8 @@ public static class Mappers ThumbnailUrls = entity.ThumbnailUrls, Metadata = entity.Metadata, Categories = entity.Categories, - Enabled = entity.Enabled + Enabled = entity.Enabled, + Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null, }; } diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs index 42a977f..1207c34 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs @@ -26,5 +26,7 @@ public class Product public ProductMetadata? Metadata { get; set; } + public ProductPrice? Price { get; set; } + public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 5ee455f..4cc30ab 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -74,7 +74,7 @@ public class ProductService(IDbContextFactory contextFact if (!string.IsNullOrWhiteSpace(filter.SerialNumber)) query = query.Where(p => p.Metadata!.SerialNumber == filter.SerialNumber); - + if (filter.MinPrice > 0) query = query.Where(p => p.Prices!.Any(pr => pr.Amount >= filter.MinPrice && pr.Amount <= filter.MaxPrice)); @@ -96,7 +96,7 @@ public class ProductService(IDbContextFactory contextFact { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if(!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken)) + if (!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken)) return Result.Fail($"Product with ID {productId} does not exist."); var existingPrices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken); @@ -133,10 +133,10 @@ public class ProductService(IDbContextFactory contextFact { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if(await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken)) + if (await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken)) return Result.Fail("A product with the same name already exists."); - if(request.Metadata is not null) + if (request.Metadata is not null) if (await context.Products.AnyAsync(p => p.Metadata!.SerialNumber == request.Metadata.SerialNumber, cancellationToken)) return Result.Fail("A product with the same metadata already exists."); @@ -223,7 +223,7 @@ public class ProductService(IDbContextFactory contextFact } } - public async ValueTask> GetProductsAsync(DateRange range, CancellationToken cancellationToken = default) + public async ValueTask> GetProductsAsync(int offset, DateRange range, CancellationToken cancellationToken = default) { try { @@ -234,11 +234,14 @@ public class ProductService(IDbContextFactory contextFact var products = await context.Products .AsNoTracking() + .Include(p => p.Prices) .OrderByDescending(p => p.CreatedAt) - .ThenBy(p => p.UpdatedAt) + .ThenByDescending(p => p.UpdatedAt) .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) + .Skip(offset) .Take(range.MaxRecords) - .ToArrayAsync(cancellationToken); + .AsSplitQuery() + .ToArrayAsync(cancellationToken); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) From 7136e4fc70f254077f9d15b88485b2b7ec367d3e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 26 May 2026 00:27:11 +0200 Subject: [PATCH 055/153] Added Customer, Contact and Address with Service Labeled all service to enable assembly scanning --- .../Abstractions/IService.cs | 3 + .../AuthorBooks/BooksService.cs | 5 +- .../Authors/AuthorService.cs | 6 +- .../Customers/CustomerService.cs | 408 ++++++++++++++++++ .../Customers/Entities/Address.cs | 7 + .../Entities/AddressConfiguration.cs | 29 ++ .../Customers/Entities/Contact.cs | 7 + .../Entities/ContactConfiguration.cs | 26 ++ .../Customers/Entities/Customer.cs | 9 + .../Entities/CustomerConfiguration.cs | 20 + .../Customers/Models/Address.cs | 32 ++ .../Customers/Models/Contact.cs | 26 ++ .../Customers/Models/Customer.cs | 26 ++ .../Customers/Models/Records.cs | 60 +++ .../Extensions/Mappers.cs | 47 ++ .../Pages/{PagesService.cs => PageService.cs} | 5 +- .../Postgres/MidrandBooksDbContext.cs | 7 + .../Products/ProductService.cs | 5 +- .../LiteCharms.Features.TechShop.csproj | 5 + LiteCharms.Features/Enums.cs | 25 ++ 20 files changed, 749 insertions(+), 9 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Abstractions/IService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/Address.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/Contact.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/Customer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Models/Address.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Models/Contact.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs rename LiteCharms.Features.MidrandBooks/Pages/{PagesService.cs => PageService.cs} (97%) diff --git a/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs b/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs new file mode 100644 index 0000000..6218faf --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs @@ -0,0 +1,3 @@ +namespace LiteCharms.Features.MidrandBooks.Abstractions; + +public interface IService; diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index 2fdfe2b..26a60ec 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -1,10 +1,11 @@ -using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.AuthorBooks; -public class BooksService(IDbContextFactory contextFactory) +public class BooksService(IDbContextFactory contextFactory) : IService { public async ValueTask UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs index 3897349..43ec4aa 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -1,13 +1,13 @@ -using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; -using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Authors; -public class AuthorService(IDbContextFactory contextFactory) +public class AuthorService(IDbContextFactory contextFactory) : IService { public async ValueTask> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs new file mode 100644 index 0000000..fdb7451 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs @@ -0,0 +1,408 @@ +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Customers; + +public class CustomerService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask> CreateCustomerAsync(CreateCustomer request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Customers.AnyAsync(c => c.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken)) + return Result.Fail(new Error($"Customer with email '{request.Email}' already exists.")); + + var customer = context.Customers.Add(new Entities.Customer + { + Company = request.Company, + VatNumber = request.VatNumber, + Email = request.Email, + Website = request.Website, + Phone = request.Phone, + SocialMedia = request.SocialMedia, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(customer.Entity.Id) + : Result.Fail(new Error("Failed to create customer.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateCustomerContactAsync(long customerId, CreateCustomerContact request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + if (await context.Contacts.AnyAsync(cc => cc.CustomerId == customerId && cc.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken)) + return Result.Fail(new Error($"Contact with email '{request.Email}' already exists for this customer.")); + + var contact = context.Contacts.Add(new Entities.Contact + { + CustomerId = customerId, + Name = request.Name, + Email = request.Email, + Phone = request.Phone, + LastName = request.LastName, + Type = request.Type, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(contact.Entity.Id) + : Result.Fail(new Error("Failed to create customer contact.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateCustomerAddressAsync(long customerId, CreateCustomerAddress request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + var address = context.Addresses.Add(new Entities.Address + { + CustomerId = customerId, + Street = request.Street, + City = request.City, + State = request.State, + PostalCode = request.PostalCode, + Country = request.Country, + Type = request.Type, + Enabled = true, + BuildingType = request.BuildingType, + IsPrimary = request.IsPrimary, + Name = request.Name + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(address.Entity.Id) + : Result.Fail(new Error("Failed to create customer address.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerAsync(long customerId, UpdateCustomer request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); + + if (customer is null) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + customer.UpdatedAt = DateTime.UtcNow; + customer.Company = request.Company; + customer.VatNumber = request.VatNumber; + customer.Email = request.Email; + customer.Website = request.Website; + customer.Phone = request.Phone; + customer.SocialMedia = request.SocialMedia; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerContactAsync(long contactId, UpdateCustomerContact request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken); + + if (contact is null) + return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); + + contact.UpdatedAt = DateTime.UtcNow; + contact.Name = request.Name; + contact.LastName = request.LastName; + contact.Email = request.Email; + contact.Phone = request.Phone; + contact.Type = request.Type; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer contact.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerAddressAsync(long addressId, UpdateCustomerAddress request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); + + if (address is null) + return Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); + + address.UpdatedAt = DateTime.UtcNow; + address.Street = request.Street; + address.City = request.City; + address.State = request.State; + address.PostalCode = request.PostalCode; + address.Country = request.Country; + address.Type = request.Type; + address.BuildingType = request.BuildingType; + address.IsPrimary = request.IsPrimary; + address.Name = request.Name; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer address.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerStatusAsync(long customerId, bool enabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); + + if (customer is null) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + customer.Enabled = enabled; + customer.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer status.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerContactStatusAsync(long contactId, bool enabled, bool isPrimary, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken); + + if (contact is null) + return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); + + contact.Enabled = enabled; + contact.IsPrimary = isPrimary; + contact.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer contact status.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerAddressStatusAsync(long addressId, bool enabled, bool isPrimary, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); + + if (address is null) + return Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); + + address.Enabled = enabled; + address.IsPrimary = isPrimary; + address.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer address status.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomersAsync(CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customers = await context.Customers + .AsNoTracking() + .Include(c => c.Contacts) + .Include(c => c.Addresses) + .OrderByDescending(c => c.CreatedAt) + .ThenByDescending(c => c.UpdatedAt) + .AsSplitQuery() + .ToListAsync(cancellationToken); + + return customers?.Count > 0 + ? Result.Ok(customers.Select(c => c.ToModel()).ToArray()) + : Result.Fail(new Error("No customers found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerContactsAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + var contacts = await context.Contacts + .AsNoTracking() + .Where(cc => cc.CustomerId == customerId) + .OrderByDescending(cc => cc.CreatedAt) + .ThenByDescending(cc => cc.UpdatedAt) + .ToListAsync(cancellationToken); + + return contacts?.Count > 0 + ? Result.Ok(contacts.Select(cc => cc.ToModel()).ToArray()) + : Result.Fail(new Error("No contacts found for the specified customer.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAddressesAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + var addresses = await context.Addresses + .AsNoTracking() + .Where(a => a.CustomerId == customerId) + .OrderByDescending(a => a.CreatedAt) + .ThenByDescending(a => a.UpdatedAt) + .ToListAsync(cancellationToken); + + return addresses?.Count > 0 + ? Result.Ok(addresses.Select(a => a.ToModel()).ToArray()) + : Result.Fail(new Error($"No addresses found for customer with ID '{customerId}'.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers + .AsNoTracking() + .Include(c => c.Contacts) + .Include(c => c.Addresses) + .FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); + + return customer is not null + ? Result.Ok(customer.ToModel()) + : Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerContactAsync(long contactId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var contact = await context.Contacts + .AsNoTracking() + .FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken); + + return contact is not null + ? Result.Ok(contact.ToModel()) + : Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAddressAsync(long addressId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var address = await context.Addresses + .AsNoTracking() + .FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); + + return address is not null + ? Result.Ok(address.ToModel()) + : Result.Fail
(new Error($"Address with ID '{addressId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail
(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/Address.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/Address.cs new file mode 100644 index 0000000..d54f7cd --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/Address.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +[EntityTypeConfiguration] +public class Address : Models.Address +{ + public virtual Customer? Customer { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs new file mode 100644 index 0000000..6b6997d --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs @@ -0,0 +1,29 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +public class AddressConfiguration : IEntityTypeConfiguration
+{ + public void Configure(EntityTypeBuilder
builder) + { + builder.ToTable("Addresses"); + + builder.HasKey(a => a.Id); + builder.Property(a => a.CustomerId).IsRequired(); + builder.Property(a => a.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(a => a.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(a => a.Name).IsRequired(); + builder.Property(a => a.Type).IsRequired(); + builder.Property(a => a.BuildingType).IsRequired(); + builder.Property(a => a.Street).IsRequired(); + builder.Property(a => a.City).IsRequired(); + builder.Property(a => a.State).IsRequired(); + builder.Property(a => a.PostalCode).IsRequired(); + builder.Property(a => a.Country).IsRequired(); + builder.Property(a => a.IsPrimary).HasDefaultValue(false); + builder.Property(a => a.Enabled).HasDefaultValue(true); + + builder.HasOne(a => a.Customer) + .WithMany(c => c.Addresses) + .HasForeignKey(a => a.CustomerId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/Contact.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/Contact.cs new file mode 100644 index 0000000..3d5509e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/Contact.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +[EntityTypeConfiguration] +public class Contact : Models.Contact +{ + public virtual Customer? Customer { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs new file mode 100644 index 0000000..8d6fac0 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +public class ContactConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Contacts"); + + builder.HasKey(c => c.Id); + builder.Property(c => c.CustomerId).IsRequired(); + builder.Property(c => c.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(c => c.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(c => c.Name).IsRequired(); + builder.Property(c => c.LastName).IsRequired(); + builder.Property(c => c.Type).IsRequired(); + builder.Property(c => c.Phone).IsRequired(); + builder.Property(c => c.Email).IsRequired(); + builder.Property(c => c.IsPrimary).HasDefaultValue(false); + builder.Property(c => c.Enabled).HasDefaultValue(true); + + builder.HasOne(c => c.Customer) + .WithMany(c => c.Contacts) + .HasForeignKey(c => c.CustomerId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/Customer.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/Customer.cs new file mode 100644 index 0000000..4443225 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/Customer.cs @@ -0,0 +1,9 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +[EntityTypeConfiguration] +public class Customer : Models.Customer +{ + public virtual ICollection Contacts { get; set; } = []; + + public virtual ICollection
Addresses { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs new file mode 100644 index 0000000..2b05c59 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +public class CustomerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Customers"); + + builder.HasKey(c => c.Id); + builder.Property(c => c.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(c => c.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(c => c.Company).IsRequired(false); + builder.Property(c => c.VatNumber).IsRequired(false); + builder.Property(c => c.Email).IsRequired(); + builder.Property(c => c.Phone).IsRequired(); + builder.Property(c => c.Website).IsRequired(); + builder.Property(c => c.SocialMedia).IsRequired(false).HasColumnType("jsonb"); + builder.Property(c => c.Enabled).HasDefaultValue(true); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Address.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Address.cs new file mode 100644 index 0000000..35c7816 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Address.cs @@ -0,0 +1,32 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public class Address +{ + public long Id { get; set; } + + public long CustomerId { get; set; } + + public string? Name { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public AddressType Type { get; set; } + + public AddressBuildingTypes BuildingType { get; set; } + + public string? Street { get; set; } + + public string? City { get; set; } + + public string? State { get; set; } + + public string? PostalCode { get; set; } + + public string? Country { get; set; } + + public bool IsPrimary { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Contact.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Contact.cs new file mode 100644 index 0000000..7eed90e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Contact.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public class Contact +{ + public long Id { get; set; } + + public long CustomerId { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public ContactTypes Type { get; set; } + + public string? Name { get; set; } + + public string? LastName { get; set; } + + public string? Email { get; set; } + + public string? Phone { get; set; } + + public bool IsPrimary { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs new file mode 100644 index 0000000..13204ce --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs @@ -0,0 +1,26 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public class Customer +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public string? Email { get; set; } + + public string? Website { get; set; } + + public string? Phone { get; set; } + + public SocialMedia[]? SocialMedia { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs new file mode 100644 index 0000000..ceb022c --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs @@ -0,0 +1,60 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public record CreateCustomer +{ + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public string? Email { get; set; } + + public string? Website { get; set; } + + public string? Phone { get; set; } + + public SocialMedia[]? SocialMedia { get; set; } +} + +public record UpdateCustomer : CreateCustomer; + +public record CreateCustomerContact +{ + public ContactTypes Type { get; set; } + + public string? Name { get; set; } + + public string? LastName { get; set; } + + public string? Email { get; set; } + + public string? Phone { get; set; } +} + +public record UpdateCustomerContact : CreateCustomerContact; + +public record CreateCustomerAddress +{ + public string? Name { get; set; } + + public AddressType Type { get; set; } + + public AddressBuildingTypes BuildingType { get; set; } + + public string? Street { get; set; } + + public string? City { get; set; } + + public string? State { get; set; } + + public string? PostalCode { get; set; } + + public string? Country { get; set; } + + public bool IsPrimary { get; set; } + + public bool Enabled { get; set; } +} + +public record UpdateCustomerAddress : CreateCustomerAddress; \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 4ec3897..1703710 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Customers.Models; using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Products.Models; @@ -7,6 +8,52 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Mappers { + public static Customer ToModel(this Customers.Entities.Customer entiry) => new() + { + Id = entiry.Id, + Company = entiry.Company, + CreatedAt = entiry.CreatedAt, + Email = entiry.Email, + Enabled = entiry.Enabled, + Phone = entiry.Phone, + SocialMedia = entiry.SocialMedia, + UpdatedAt = entiry.UpdatedAt, + VatNumber = entiry.VatNumber, + Website = entiry.Website + }; + + public static Address ToModel(this Customers.Entities.Address entity) => new() + { + Id = entity.Id, + BuildingType = entity.BuildingType, + CreatedAt = entity.CreatedAt, + CustomerId = entity.CustomerId, + Enabled = entity.Enabled, + IsPrimary = entity.IsPrimary, + Name = entity.Name, + PostalCode = entity.PostalCode, + Type = entity.Type, + UpdatedAt = entity.UpdatedAt, + Street = entity.Street, + City = entity.City, + State = entity.State, + Country = entity.Country + }; + + public static Contact ToModel(this Customers.Entities.Contact entity) => new() + { + Id = entity.Id, + Type = entity.Type, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + CustomerId = entity.CustomerId, + Email = entity.Email, + Enabled = entity.Enabled, + LastName = entity.LastName, + Name = entity.Name, + Phone = entity.Phone + }; + public static BookPage ToModel(this Pages.Entities.BookPage entity) => new() { Id = entity.Id, diff --git a/LiteCharms.Features.MidrandBooks/Pages/PagesService.cs b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs similarity index 97% rename from LiteCharms.Features.MidrandBooks/Pages/PagesService.cs rename to LiteCharms.Features.MidrandBooks/Pages/PageService.cs index 18c822b..5c6f98a 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/PagesService.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs @@ -1,10 +1,11 @@ -using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.Pages; -public class PagesService(IDbContextFactory contextFactory) +public class PageService(IDbContextFactory contextFactory) : IService { public async ValueTask DeleteAllAsync(long authorBookId, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index 1bb907d..d786b93 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; using LiteCharms.Features.MidrandBooks.Authors.Entities; +using LiteCharms.Features.MidrandBooks.Customers.Entities; using LiteCharms.Features.MidrandBooks.Pages.Entities; using LiteCharms.Features.MidrandBooks.Products.Entities; @@ -16,4 +17,10 @@ public class MidrandBooksDbContext(DbContextOptions optio public DbSet Books => Set(); public DbSet Pages => Set(); + + public DbSet Contacts => Set(); + + public DbSet
Addresses => Set
(); + + public DbSet Customers => Set(); } diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 4cc30ab..0232048 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -1,11 +1,12 @@ -using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Products; -public class ProductService(IDbContextFactory contextFactory) +public class ProductService(IDbContextFactory contextFactory) : IService { public async ValueTask UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj index a8478ad..0318fd4 100644 --- a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj +++ b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj @@ -23,6 +23,11 @@ utility;dotnet icon.png + + + + + diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs index 510d826..951c3ff 100644 --- a/LiteCharms.Features/Enums.cs +++ b/LiteCharms.Features/Enums.cs @@ -1,5 +1,30 @@ namespace LiteCharms.Features; +public enum ContactTypes : int +{ + Personal = 0, + Business = 1, + Other = 2 +} + +public enum AddressType +{ + Billing = 1, + Shipping = 2, + Other = 3 +} + +public enum AddressBuildingTypes : int +{ + Residential = 0, + Commercial = 1, + Industrial = 2, + MixedUse = 3, + Agricultural = 4, + Institutional = 5, + Recreational = 6, +} + public enum SocialMediaTypes : int { Twitter = 0, From 20b747e89c71fb4ee9962b1b226797fcdacd5d7e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 26 May 2026 00:47:07 +0200 Subject: [PATCH 056/153] Added Order models --- .../LiteCharms.Features.MidrandBooks.csproj | 4 ++ .../Order/Models/Order.cs | 22 +++++++ .../Order/Models/Refund.cs | 20 +++++++ .../Order/Models/Shipping.cs | 14 +++++ .../Order/Models/ShippingProvider.cs | 18 ++++++ .../Orders/Models/Order.cs | 4 +- LiteCharms.Features/Enums.cs | 58 +++++++++++++++++++ 7 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Order/Models/Order.cs create mode 100644 LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs create mode 100644 LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs create mode 100644 LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 552ce45..34c16e0 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -163,4 +163,8 @@ + + + + diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Order.cs b/LiteCharms.Features.MidrandBooks/Order/Models/Order.cs new file mode 100644 index 0000000..e0c1c73 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Order/Models/Order.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Order.Models; + +public class Order +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long CustomerId { get; set; } + + public OrderStatus Status { get; set; } + + public decimal Total { get; set; } + + public string[]? Notes { get; set; } + + public string[]? Terms { get; set; } + + public string? InvoiceUrl { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs b/LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs new file mode 100644 index 0000000..920145e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Order.Models; + +public class Refund +{ + public Guid Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public Guid OrderId { get; set; } + + public RefundTypes Type { get; set; } + + public RefundStatus Status { get; set; } + + public string? Reason { get; set; } + + public decimal Amount { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs b/LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs new file mode 100644 index 0000000..975ab91 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.MidrandBooks.Order.Models; + +public class Shipping +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long OrderId { get; set; } + + public ShippingStatuses Status { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs b/LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs new file mode 100644 index 0000000..8c129b4 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Order.Models; + +public class ShippingProvider +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public ShippingProviderTypes Type { get; set; } + + public string? Name { get; set; } + + public decimal? Price { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.TechShop/Orders/Models/Order.cs b/LiteCharms.Features.TechShop/Orders/Models/Order.cs index 2146423..b050337 100644 --- a/LiteCharms.Features.TechShop/Orders/Models/Order.cs +++ b/LiteCharms.Features.TechShop/Orders/Models/Order.cs @@ -1,6 +1,4 @@ -using LiteCharms.Features.TechShop; - -namespace LiteCharms.Features.TechShop.Orders.Models; +namespace LiteCharms.Features.TechShop.Orders.Models; public class Order { diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs index 951c3ff..9cff19c 100644 --- a/LiteCharms.Features/Enums.cs +++ b/LiteCharms.Features/Enums.cs @@ -1,5 +1,63 @@ namespace LiteCharms.Features; +public enum ShippingProviderTypes : int +{ + Dsv = 0, + Pargo = 1, + Ram = 2, + TheCourierGuy = 3, + Paxi = 4, + FastWay = 5, + MdsCollivery = 6, + PostNet = 7, + Aramex = 8, + DHL = 9, + FedEx = 10, + UPS = 11, + USPS = 12, + AmazonLogistics = 13, + LocalCourier = 14, + Other = 15 +} + +public enum ShippingStatuses : int +{ + Pending = 0, + Shipped = 1, + Delivered = 2, + Returned = 3, + Cancelled = 4, +} + +public enum RefundTypes : int +{ + Full = 0, + Partial = 1, + StoreCredit = 2, + Exchange = 3, + Other = 4 +} + +public enum RefundStatus : int +{ + Pending = 0, + Approved = 1, + Rejected = 2, + Completed = 3, + Failed = 4, +} + +public enum OrderStatus : int +{ + Pending = 0, + Completed = 1, + Cancelled = 2, + Failed = 3, + Refunded = 4, + Error = 5, + OnHold = 6, +} + public enum ContactTypes : int { Personal = 0, From 70860efcfb02289578908711c25b236e5e5eb788 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 26 May 2026 08:24:38 +0200 Subject: [PATCH 057/153] Created Order, Refund, Shipping --- .../LiteCharms.Features.MidrandBooks.csproj | 4 - .../Orders/Entities/Order.cs | 11 ++ .../Orders/Entities/OrderConfiguration.cs | 17 +++ .../Orders/Entities/OrderItem.cs | 14 +++ .../Orders/Entities/OrderItemConfiguration.cs | 31 +++++ .../Orders/Entities/Refund.cs | 7 ++ .../Orders/Entities/RefundConfiguration.cs | 22 ++++ .../Orders/Entities/Shipping.cs | 13 +++ .../Orders/Entities/ShippingConfiguration.cs | 32 +++++ .../Orders/Entities/ShippingProvider.cs | 6 + .../Entities/ShippingProviderConfiguration.cs | 17 +++ .../{Order => Orders}/Models/Order.cs | 6 +- .../Orders/Models/OrderItem.cs | 16 +++ .../Orders/Models/Records.cs | 5 + .../{Order => Orders}/Models/Refund.cs | 2 +- .../{Order => Orders}/Models/Shipping.cs | 6 +- .../Models/ShippingProvider.cs | 2 +- .../Orders/OrderService.cs | 109 ++++++++++++++++++ .../Postgres/MidrandBooksDbContext.cs | 11 ++ 19 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItem.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/Shipping.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProvider.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs rename LiteCharms.Features.MidrandBooks/{Order => Orders}/Models/Order.cs (69%) create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Models/OrderItem.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs rename LiteCharms.Features.MidrandBooks/{Order => Orders}/Models/Refund.cs (85%) rename LiteCharms.Features.MidrandBooks/{Order => Orders}/Models/Shipping.cs (61%) rename LiteCharms.Features.MidrandBooks/{Order => Orders}/Models/ShippingProvider.cs (84%) create mode 100644 LiteCharms.Features.MidrandBooks/Orders/OrderService.cs diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 34c16e0..552ce45 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -163,8 +163,4 @@ - - - - diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs new file mode 100644 index 0000000..ddc8468 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs @@ -0,0 +1,11 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +[EntityTypeConfiguration] +public class Order : Models.Order +{ + public virtual Shipping? Shipping { get; set; } + + public virtual ICollection OrderItems { get; set; } = []; + + public virtual ICollection Refunds { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs new file mode 100644 index 0000000..4063456 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs @@ -0,0 +1,17 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class OrderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Orders"); + + builder.HasKey(o => o.Id); + builder.Property(o => o.CustomerId).IsRequired(); + builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(o => o.Status).IsRequired(); + builder.Property(o => o.Total).IsRequired().HasColumnType("decimal(18,2)"); + builder.Property(o => o.Notes).HasMaxLength(1000); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItem.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItem.cs new file mode 100644 index 0000000..d255a04 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItem.cs @@ -0,0 +1,14 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; +using LiteCharms.Features.MidrandBooks.Products.Entities; + +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +[EntityTypeConfiguration] +public class OrderItem : Models.OrderItem +{ + public virtual Order? Order { get; set; } + + public virtual AuthorBook? AuthorBook { get; set; } + + public virtual ProductPrice? ProductPrice { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs new file mode 100644 index 0000000..50ac4da --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs @@ -0,0 +1,31 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class OrderItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("OrderItems"); + + builder.HasKey(oi => oi.Id); + builder.Property(oi => oi.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("new()"); + builder.Property(oi => oi.OrderId).IsRequired(); + builder.Property(oi => oi.AuthorBookId).IsRequired(); + builder.Property(oi => oi.ProductPriceId).IsRequired(); + builder.Property(oi => oi.Quantity).IsRequired(); + + builder.HasOne(oi => oi.Order) + .WithMany(o => o.OrderItems) + .HasForeignKey(oi => oi.OrderId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(oi => oi.AuthorBook) + .WithMany() + .HasForeignKey(oi => oi.AuthorBookId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(oi => oi.ProductPrice) + .WithMany() + .HasForeignKey(oi => oi.ProductPriceId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs new file mode 100644 index 0000000..3116896 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +[EntityTypeConfiguration] +public class Refund : Models.Refund +{ + public virtual Order? Order { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs new file mode 100644 index 0000000..566b779 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class RefundConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Refunds"); + + builder.HasKey(r => r.Id); + builder.Property(r => r.OrderId).IsRequired(); + builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(o => o.Status).IsRequired(); + builder.Property(r => r.Amount).IsRequired().HasPrecision(18, 2); + builder.Property(r => r.Reason).HasMaxLength(1000); + + builder.HasOne(r => r.Order) + .WithMany(o => o.Refunds) + .HasForeignKey(r => r.OrderId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Shipping.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/Shipping.cs new file mode 100644 index 0000000..d8f9e07 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/Shipping.cs @@ -0,0 +1,13 @@ +using LiteCharms.Features.MidrandBooks.Customers.Entities; + +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +[EntityTypeConfiguration] +public class Shipping : Models.Shipping +{ + public virtual Order? Order { get; set; } + + public virtual Address? Address { get; set; } + + public virtual ShippingProvider? ShippingProvider { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs new file mode 100644 index 0000000..2da9465 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs @@ -0,0 +1,32 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class ShippingConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Shippings"); + + builder.HasKey(s => s.Id); + builder.Property(s => s.OrderId).IsRequired(); + builder.Property(s => s.AddressId).IsRequired(); + builder.Property(s => s.ShippingProviderId).IsRequired(); + builder.Property(s => s.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(s => s.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(s => s.Status).IsRequired(); + + builder.HasOne(s => s.Order) + .WithOne(o => o.Shipping) + .HasForeignKey(s => s.OrderId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(s => s.Address) + .WithMany() + .HasForeignKey(s => s.AddressId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.ShippingProvider) + .WithMany(f => f.Shippings) + .HasForeignKey(f => f.ShippingProviderId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProvider.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProvider.cs new file mode 100644 index 0000000..1fc3127 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProvider.cs @@ -0,0 +1,6 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class ShippingProvider : Models.ShippingProvider +{ + public virtual ICollection Shippings { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs new file mode 100644 index 0000000..003ab98 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs @@ -0,0 +1,17 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class ShippingProviderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ShippingProviders"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.Type).IsRequired(); + builder.Property(f => f.Name).IsRequired().HasMaxLength(100); + builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Enabled).HasDefaultValue(true); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Order.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Order.cs similarity index 69% rename from LiteCharms.Features.MidrandBooks/Order/Models/Order.cs rename to LiteCharms.Features.MidrandBooks/Orders/Models/Order.cs index e0c1c73..a86c03e 100644 --- a/LiteCharms.Features.MidrandBooks/Order/Models/Order.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Order.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandBooks.Order.Models; +namespace LiteCharms.Features.MidrandBooks.Orders.Models; public class Order { @@ -14,9 +14,7 @@ public class Order public decimal Total { get; set; } - public string[]? Notes { get; set; } - - public string[]? Terms { get; set; } + public string? Notes { get; set; } public string? InvoiceUrl { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/OrderItem.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/OrderItem.cs new file mode 100644 index 0000000..424ad4f --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/OrderItem.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Models; + +public class OrderItem +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public long OrderId { get; set; } + + public long AuthorBookId { get; set; } + + public long ProductPriceId { get; set; } + + public int Quantity { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs new file mode 100644 index 0000000..a77d3aa --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs @@ -0,0 +1,5 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Models; + +public record CreateOrder(long CustomerId, decimal TotalPrice, string? Notes); + +public record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity); diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs similarity index 85% rename from LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs rename to LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs index 920145e..3f5eec4 100644 --- a/LiteCharms.Features.MidrandBooks/Order/Models/Refund.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandBooks.Order.Models; +namespace LiteCharms.Features.MidrandBooks.Orders.Models; public class Refund { diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs similarity index 61% rename from LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs rename to LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs index 975ab91..c43bc1f 100644 --- a/LiteCharms.Features.MidrandBooks/Order/Models/Shipping.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandBooks.Order.Models; +namespace LiteCharms.Features.MidrandBooks.Orders.Models; public class Shipping { @@ -10,5 +10,9 @@ public class Shipping public long OrderId { get; set; } + public long AddressId { get; set; } + + public long ShippingProviderId { get; set; } + public ShippingStatuses Status { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs similarity index 84% rename from LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs rename to LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs index 8c129b4..76a396e 100644 --- a/LiteCharms.Features.MidrandBooks/Order/Models/ShippingProvider.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandBooks.Order.Models; +namespace LiteCharms.Features.MidrandBooks.Orders.Models; public class ShippingProvider { diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs new file mode 100644 index 0000000..9e37122 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs @@ -0,0 +1,109 @@ +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Orders.Models; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Orders; + +public class OrderService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask> CreateOrderAsync(long customerId, CreateOrder request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail("Customer not found."); + + var order = context.Orders.Add(new Entities.Order + { + CustomerId = customerId, + Status = OrderStatus.Pending, + Total = request.TotalPrice + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(order.Entity.Id) + : Result.Fail("Failed to create order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> AddItemToOrderAsync(long orderId, CreateOrderItem request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) + return Result.Fail("Order not found."); + + if(!await context.Books.AnyAsync(ab => ab.Id == request.AuthorBookId, cancellationToken)) + return Result.Fail("Author book not found."); + + if (!await context.Prices.AnyAsync(pp => pp.Id == request.ProductPriceId, cancellationToken)) + return Result.Fail("Product price not found."); + + var orderItem = context.OrderItems.Add(new Entities.OrderItem + { + OrderId = orderId, + AuthorBookId = request.AuthorBookId, + ProductPriceId = request.ProductPriceId, + Quantity = request.Quantity + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(orderItem.Entity.Id) + : Result.Fail("Failed to add item to order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask AddItemsToOrderAsync(long orderId, CreateOrderItem[] items, CancellationToken cancellationToken = default) + { + try + { + if(items.Length == 0) + return Result.Fail("No items to add."); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) + return Result.Fail("Order not found."); + + var existingItems = context.OrderItems.Where(oi => oi.OrderId == orderId); + context.OrderItems.RemoveRange(existingItems); + + foreach (var item in items) + { + if (!await context.Books.AnyAsync(ab => ab.Id == item.AuthorBookId, cancellationToken)) + return Result.Fail($"Author book with ID {item.AuthorBookId} not found."); + + if (!await context.Prices.AnyAsync(pp => pp.Id == item.ProductPriceId, cancellationToken)) + return Result.Fail($"Product price with ID {item.ProductPriceId} not found."); + + context.OrderItems.Add(new Entities.OrderItem + { + OrderId = orderId, + AuthorBookId = item.AuthorBookId, + ProductPriceId = item.ProductPriceId, + Quantity = item.Quantity + }); + } + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to add items to order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index d786b93..901933e 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -1,6 +1,7 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; using LiteCharms.Features.MidrandBooks.Authors.Entities; using LiteCharms.Features.MidrandBooks.Customers.Entities; +using LiteCharms.Features.MidrandBooks.Orders.Entities; using LiteCharms.Features.MidrandBooks.Pages.Entities; using LiteCharms.Features.MidrandBooks.Products.Entities; @@ -23,4 +24,14 @@ public class MidrandBooksDbContext(DbContextOptions optio public DbSet
Addresses => Set
(); public DbSet Customers => Set(); + + public DbSet Orders => Set(); + + public DbSet OrderItems => Set(); + + public DbSet Refunds => Set(); + + public DbSet Shippings => Set(); + + public DbSet ShippingProviders => Set(); } From 902942eee672f7fab6aa8d3d6cb87769f665ca6a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 27 May 2026 09:12:04 +0200 Subject: [PATCH 058/153] Completed initial database design Sealed qualifying public classes Migrated database changes --- .editorconfig | 39 + .../AuthorBooks/BooksService.cs | 2 +- .../Entities/AuthorBookConfiguration.cs | 2 +- .../Authors/AuthorService.cs | 2 +- .../Authors/Entities/Author.cs | 2 +- .../Authors/Entities/AuthorConfiguration.cs | 2 +- .../Authors/Models/Records.cs | 2 +- .../Customers/CustomerService.cs | 2 +- .../Entities/AddressConfiguration.cs | 2 +- .../Entities/ContactConfiguration.cs | 2 +- .../Entities/CustomerConfiguration.cs | 2 +- .../Customers/Models/Records.cs | 6 +- .../Extensions/Mappers.cs | 45 + .../Extensions/Shop.cs | 23 + .../MidrandShopQuartzHealthCheck.cs | 2 +- .../PostgresMidrandShopHealthCheck.cs | 2 +- .../LiteCharms.Features.MidrandBooks.csproj | 12 +- .../Orders/Entities/Order.cs | 4 +- .../Orders/Entities/OrderConfiguration.cs | 2 +- .../Orders/Entities/OrderItemConfiguration.cs | 4 +- .../Orders/Entities/ShippingConfiguration.cs | 3 +- .../Entities/ShippingProviderConfiguration.cs | 3 +- .../Orders/Models/Records.cs | 10 +- .../Orders/Models/Shipping.cs | 2 + .../Orders/Models/ShippingProvider.cs | 2 + .../Orders/OrderService.cs | 356 ++++++- .../Pages/Entities/BookPageConfiguration.cs | 2 +- .../Pages/Models/UpdateBookPage.cs | 2 +- .../Pages/PageService.cs | 2 +- .../{Orders => Payments}/Entities/Refund.cs | 4 +- .../Entities/RefundConfiguration.cs | 4 +- .../{Orders => Payments}/Models/Refund.cs | 6 +- .../Payments/PaymentService.cs | 7 + .../Postgres/MidrandBooksDbContext.cs | 3 +- .../Postgres/MidrandBooksDbContextFactory.cs | 3 +- .../20260527070840_Init.Designer.cs | 875 ++++++++++++++++++ .../Migrations/20260527070840_Init.cs | 505 ++++++++++ .../MidrandBooksDbContextModelSnapshot.cs | 872 +++++++++++++++++ .../Products/Entities/ProductConfiguration.cs | 2 +- .../Entities/ProductPriceConfiguration.cs | 2 +- .../Products/Models/CreateProductPrice.cs | 2 +- .../Products/Models/Records.cs | 2 +- .../Products/ProductService.cs | 2 +- .../appsettings.json | 22 - ...520191059_AddedProductMetadata.Designer.cs | 1 + .../20260520191059_AddedProductMetadata.cs | 1 + .../Migrations/ShopDbContextModelSnapshot.cs | 1 + .../Products/Models/Records.cs | 4 +- .../Products/ProductService.cs | 3 +- LiteCharms.Features/Abstractions/EventBase.cs | 2 +- .../Email/Configuration/Account.cs | 2 +- .../Email/Configuration/SmtpSettings.cs | 2 +- LiteCharms.Features/Email/EmailService.cs | 77 +- .../Email/Models/Attachment.cs | 2 +- LiteCharms.Features/Email/Models/Body.cs | 2 +- .../Email/Models/BodyProperties.cs | 2 +- LiteCharms.Features/Email/Models/Message.cs | 2 +- LiteCharms.Features/Email/Models/Party.cs | 2 +- LiteCharms.Features/Email/Models/Response.cs | 2 +- LiteCharms.Features/Extensions/Hash.cs | 6 +- LiteCharms.Features/Extensions/Quartz.cs | 2 +- LiteCharms.Features/Extensions/Timezones.cs | 2 +- .../LiteCharms.Features.csproj | 5 + LiteCharms.Features/Models/DateRange.cs | 2 +- LiteCharms.Features/Models/PageReference.cs | 2 +- LiteCharms.Features/Models/ProductFilter.cs | 2 +- LiteCharms.Features/Models/ProductMetadata.cs | 2 +- LiteCharms.Features/Models/SocialMedia.cs | 2 +- LiteCharms.Features/Quartz/JobOrchestrator.cs | 8 +- LiteCharms.Features/Quartz/MediatorJob.cs | 2 +- .../Quartz/RetryJobListener.cs | 2 +- .../S3/Abstractions/S3ServiceBase.cs | 2 +- .../S3/BookshopInvoicesS3Service.cs | 2 +- .../S3/BookshopQuotesS3Service.cs | 2 +- LiteCharms.Features/S3/BookshopS3Service.cs | 2 +- .../S3/Configuration/S3Settings.cs | 2 +- .../ServiceBus/EmailServiceBus.cs | 2 +- .../ServiceBus/Exchanges/EmailExchange.cs | 2 +- .../ServiceBus/Exchanges/GeneralExchange.cs | 2 +- .../ServiceBus/Exchanges/SalesExchange.cs | 2 +- .../ServiceBus/GeneralServiceBus.cs | 2 +- .../ServiceBus/Queues/EmailQueue.cs | 2 +- .../ServiceBus/Queues/GeneralQueue.cs | 2 +- .../ServiceBus/Queues/SalesQueue.cs | 2 +- .../ServiceBus/SalesServiceBus.cs | 2 +- LiteCharmsShared.slnx | 1 + 86 files changed, 2883 insertions(+), 140 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Extensions/Shop.cs rename LiteCharms.Features.MidrandBooks/{Orders => Payments}/Entities/Refund.cs (53%) rename LiteCharms.Features.MidrandBooks/{Orders => Payments}/Entities/RefundConfiguration.cs (84%) rename LiteCharms.Features.MidrandBooks/{Orders => Payments}/Models/Refund.cs (68%) create mode 100644 LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs delete mode 100644 LiteCharms.Features.MidrandBooks/appsettings.json diff --git a/.editorconfig b/.editorconfig index 4a5bdbf..6ab8a85 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,6 +3,45 @@ 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 #### diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index 26a60ec..81ff39f 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.AuthorBooks; -public class BooksService(IDbContextFactory contextFactory) : IService +public sealed class BooksService(IDbContextFactory contextFactory) : IService { public async ValueTask UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs index 3f852ac..1bc1259 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; -public class AuthorBookConfiguration : IEntityTypeConfiguration +public sealed class AuthorBookConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs index 43ec4aa..2f71d02 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -7,7 +7,7 @@ using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Authors; -public class AuthorService(IDbContextFactory contextFactory) : IService +public sealed class AuthorService(IDbContextFactory contextFactory) : IService { public async ValueTask> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) { diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs index 60b6a2b..28ea8da 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs @@ -3,7 +3,7 @@ namespace LiteCharms.Features.MidrandBooks.Authors.Entities; [EntityTypeConfiguration] -public class Author : Models.Author +public sealed class Author : Models.Author { public ICollection Books { get; set; } = []; } diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs index cd6eda1..0c0af10 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Authors.Entities; -public class AuthorConfiguration : IEntityTypeConfiguration +public sealed class AuthorConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs index ba54191..2b6a055 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.MidrandBooks.Authors.Models; -public record UpdateAuthor : CreateAuthor; +public sealed record UpdateAuthor : CreateAuthor; public record CreateAuthor { diff --git a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs index fdb7451..d73292d 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.Customers; -public class CustomerService(IDbContextFactory contextFactory) : IService +public sealed class CustomerService(IDbContextFactory contextFactory) : IService { public async ValueTask> CreateCustomerAsync(CreateCustomer request, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs index 6b6997d..4934bac 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Customers.Entities; -public class AddressConfiguration : IEntityTypeConfiguration
+public sealed class AddressConfiguration : IEntityTypeConfiguration
{ public void Configure(EntityTypeBuilder
builder) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs index 8d6fac0..69c931c 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Customers.Entities; -public class ContactConfiguration : IEntityTypeConfiguration +public sealed class ContactConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs index 2b05c59..f5015a7 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Customers.Entities; -public class CustomerConfiguration : IEntityTypeConfiguration +public sealed class CustomerConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs index ceb022c..10c8dae 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs @@ -17,7 +17,7 @@ public record CreateCustomer public SocialMedia[]? SocialMedia { get; set; } } -public record UpdateCustomer : CreateCustomer; +public sealed record UpdateCustomer : CreateCustomer; public record CreateCustomerContact { @@ -32,7 +32,7 @@ public record CreateCustomerContact public string? Phone { get; set; } } -public record UpdateCustomerContact : CreateCustomerContact; +public sealed record UpdateCustomerContact : CreateCustomerContact; public record CreateCustomerAddress { @@ -57,4 +57,4 @@ public record CreateCustomerAddress public bool Enabled { get; set; } } -public record UpdateCustomerAddress : CreateCustomerAddress; \ No newline at end of file +public sealed record UpdateCustomerAddress : CreateCustomerAddress; \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 1703710..72e57d5 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -1,6 +1,7 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Products.Models; @@ -8,6 +9,50 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Mappers { + public static ShippingProvider ToModel(this Orders.Entities.ShippingProvider entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Name = entity.Name, + Type = entity.Type, + Price = entity.Price, + Enabled = entity.Enabled + }; + + public static Shipping ToModel(this Orders.Entities.Shipping entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + OrderId = entity.OrderId, + AddressId = entity.AddressId, + Status = entity.Status, + TrackingNumber = entity.TrackingNumber + }; + + public static OrderItem ToModel(this Orders.Entities.OrderItem entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + OrderId = entity.OrderId, + Quantity = entity.Quantity, + AuthorBookId = entity.AuthorBookId, + ProductPriceId = entity.ProductPriceId + }; + + public static Order ToModel(this Orders.Entities.Order entity) => new() + { + Id = entity.Id, + CustomerId = entity.CustomerId, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Status = entity.Status, + Total = entity.Total, + InvoiceUrl = entity.InvoiceUrl, + Notes = entity.Notes + }; + public static Customer ToModel(this Customers.Entities.Customer entiry) => new() { Id = entiry.Id, diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs new file mode 100644 index 0000000..43e407e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -0,0 +1,23 @@ +using LiteCharms.Features.MidrandBooks.Abstractions; + +namespace LiteCharms.Features.MidrandBooks.Extensions; + +public static class Shop +{ + public static IServiceCollection AddShopServices(this IServiceCollection services, Assembly assembly, ServiceLifetime serviceLifetime) + { + var serviceType = typeof(IService); + + var implementations = assembly.GetTypes() + .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); + + foreach (var implementation in implementations) + { + var descriptor = new ServiceDescriptor(serviceType, implementation, serviceLifetime); + + services.Add(descriptor); + } + + return services; + } +} diff --git a/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs b/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs index 23bdf5a..0de8562 100644 --- a/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs +++ b/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.MidrandBooks.HealthChecks; -public class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck +public sealed class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck { public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs index 5a8b44e..dba8e72 100644 --- a/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs +++ b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.MidrandBooks.HealthChecks; -public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck +public sealed class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck { private readonly string connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName)!; diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 552ce45..17db6dd 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -31,6 +31,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -143,6 +147,8 @@ + + @@ -157,10 +163,4 @@ - - - PreserveNewest - - - diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs index ddc8468..657af6f 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.MidrandBooks.Orders.Entities; +using LiteCharms.Features.MidrandBooks.Payments.Entities; + +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; [EntityTypeConfiguration] public class Order : Models.Order diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs index 4063456..c7ae590 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Entities; -public class OrderConfiguration : IEntityTypeConfiguration +public sealed class OrderConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs index 50ac4da..5a82a1a 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs @@ -1,13 +1,13 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Entities; -public class OrderItemConfiguration : IEntityTypeConfiguration +public sealed class OrderItemConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("OrderItems"); builder.HasKey(oi => oi.Id); - builder.Property(oi => oi.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("new()"); + builder.Property(oi => oi.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(oi => oi.OrderId).IsRequired(); builder.Property(oi => oi.AuthorBookId).IsRequired(); builder.Property(oi => oi.ProductPriceId).IsRequired(); diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs index 2da9465..5938ff2 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Entities; -public class ShippingConfiguration : IEntityTypeConfiguration +public sealed class ShippingConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { @@ -13,6 +13,7 @@ public class ShippingConfiguration : IEntityTypeConfiguration builder.Property(s => s.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(s => s.UpdatedAt).HasDefaultValueSql("now()"); builder.Property(s => s.Status).IsRequired(); + builder.Property(s => s.TrackingNumber).HasMaxLength(255); builder.HasOne(s => s.Order) .WithOne(o => o.Shipping) diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs index 003ab98..83b4755 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Entities; -public class ShippingProviderConfiguration : IEntityTypeConfiguration +public sealed class ShippingProviderConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { @@ -13,5 +13,6 @@ public class ShippingProviderConfiguration : IEntityTypeConfiguration f.Name).IsRequired().HasMaxLength(100); builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2); builder.Property(f => f.Enabled).HasDefaultValue(true); + builder.Property(f => f.TrackingUrl).HasMaxLength(200); } } diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs index a77d3aa..f5453bc 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs @@ -1,5 +1,11 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Models; -public record CreateOrder(long CustomerId, decimal TotalPrice, string? Notes); +public sealed record CreateOrder(long CustomerId, decimal TotalPrice, string? Notes); -public record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity); +public sealed record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity); + +public sealed record CreateShipping(long OrderId, long AddressId, long ShippingProviderId, string? TrackingNumber); + +public sealed record CreateShippingProvider(ShippingProviderTypes Type, string Name, decimal Price, string TrackingUrl); + +public sealed record UpdateShippingProvider(long ProviderId, bool Enabled, string Name, decimal Price, string TrackingUrl); diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs index c43bc1f..51491fe 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs @@ -14,5 +14,7 @@ public class Shipping public long ShippingProviderId { get; set; } + public string? TrackingNumber { get; set; } + public ShippingStatuses Status { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs index 76a396e..a26d934 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs @@ -14,5 +14,7 @@ public class ShippingProvider public decimal? Price { get; set; } + public string? TrackingUrl { get; set; } + public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs index 9e37122..c15b2bf 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs @@ -1,10 +1,12 @@ using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Orders; -public class OrderService(IDbContextFactory contextFactory) : IService +public sealed class OrderService(IDbContextFactory contextFactory) : IService { public async ValueTask> CreateOrderAsync(long customerId, CreateOrder request, CancellationToken cancellationToken = default) { @@ -106,4 +108,356 @@ public class OrderService(IDbContextFactory contextFactor return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } + + public async ValueTask RemoveItemFromOrderAsync(long orderId, long orderItemId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var orderItem = await context.OrderItems.FirstOrDefaultAsync(oi => oi.Id == orderItemId && oi.OrderId == orderId, cancellationToken); + + if (orderItem is null) + return Result.Fail("Order item not found."); + + context.OrderItems.Remove(orderItem); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to remove item from order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask ClearOrderItemasAsync(long orderId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var orderItems = context.OrderItems.Where(oi => oi.OrderId == orderId); + + context.OrderItems.RemoveRange(orderItems); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to clear order items."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask CancelOrderAsync(long orderId, CancellationToken cancellationToken = default) => + await UpdateOrderStatusAsync(orderId, OrderStatus.Cancelled, cancellationToken); + + public async ValueTask> GetOrderAsync(long orderId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.AsNoTracking().FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); + + return order is not null + ? Result.Ok(order.ToModel()) + : Result.Fail("Order not found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrdersByCustomerAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail("Customer not found."); + + var orders = await context.Orders + .AsNoTracking() + .Where(o => o.CustomerId == customerId) + .ToListAsync(cancellationToken); + + return Result.Ok(orders.Select(o => o.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrdersAsync(DateRange range, int index, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var orders = await context.Orders + .AsNoTracking() + .Where(o => o.CreatedAt >= range.From.ToDateTime(TimeOnly.MinValue) && o.CreatedAt <= range.To.ToDateTime(TimeOnly.MaxValue)) + .Skip(index * range.MaxRecords) + .ToListAsync(cancellationToken); + + return Result.Ok(orders.Select(o => o.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateOrderStatusAsync(long orderId, OrderStatus newStatus, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); + + if (order is null) + return Result.Fail("Order not found."); + + order.UpdatedAt = DateTime.UtcNow; + order.Status = newStatus; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to update order status."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) + return Result.Fail("Order not found."); + + if(!await context.Addresses.AnyAsync(a => a.Id == request.AddressId, cancellationToken)) + return Result.Fail("Address not found."); + + if(!await context.ShippingProviders.AnyAsync(sp => sp.Id == request.ShippingProviderId && sp.Enabled, cancellationToken)) + return Result.Fail("Shipping provider not found or disabled."); + + if(await context.Shippings.AnyAsync(s => s.OrderId == orderId, cancellationToken)) + return Result.Fail("Shipping already exists for this order."); + + var shipping = context.Shippings.Add(new Entities.Shipping + { + OrderId = orderId, + AddressId = request.AddressId, + ShippingProviderId = request.ShippingProviderId, + Status = ShippingStatuses.Pending, + TrackingNumber = request.TrackingNumber + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to add shipping to order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateShippingStatusAsync(long orderId, ShippingStatuses newStatus, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId, cancellationToken); + + if (shipping is null) + return Result.Fail("Shipping not found for this order."); + + shipping.UpdatedAt = DateTime.UtcNow; + shipping.Status = newStatus; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to update shipping status."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task> GetShippingByOrderIdAsync(long orderId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var shipping = await context.Shippings + .AsNoTracking() + .FirstOrDefaultAsync(s => s.OrderId == orderId, cancellationToken); + + return shipping is not null + ? Result.Ok(shipping.ToModel()) + : Result.Fail("Shipping not found for this order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task RemoveShippingFromOrderAsync(long orderId, long shippingId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) + return Result.Fail("Order not found."); + + var shipping = await context.Shippings.AsNoTracking() + .FirstOrDefaultAsync(s => s.OrderId == orderId && s.Id == shippingId, cancellationToken); + + if (shipping is null) + return Result.Fail("Shipping not found for this order."); + + context.Shippings.Remove(shipping); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to remove shipping from order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateShippingTrackingNumberAsync(long orderId, long shippingId, string trackingNumber, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId && s.Id == shippingId, cancellationToken); + + if (shipping is null) + return Result.Fail("Shipping not found for this order."); + + shipping.UpdatedAt = DateTime.UtcNow; + shipping.TrackingNumber = trackingNumber; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to update shipping tracking number."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(await context.ShippingProviders.AnyAsync(sp => sp.Type == request.Type, cancellationToken)) + return Result.Fail("Shipping provider with the same type already exists."); + + var shippingProvider = context.ShippingProviders.Add(new Entities.ShippingProvider + { + Name = request.Name, + Type = request.Type, + Price = request.Price, + TrackingUrl = request.TrackingUrl + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to create shipping provider."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShippingProvidersAsync(bool isEnabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var providers = await context.ShippingProviders.AsNoTracking().Where(sp => sp.Enabled == isEnabled) + .ToListAsync(cancellationToken); + + return Result.Ok(providers.Select(sp => sp.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShippingProviderAsync(long providerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var provider = await context.ShippingProviders.AsNoTracking() + .FirstOrDefaultAsync(sp => sp.Id == providerId, cancellationToken); + + return provider is not null + ? Result.Ok(provider.ToModel()) + : Result.Fail("Shipping provider not found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateShippingProviderAsync(UpdateShippingProvider request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var provider = await context.ShippingProviders.FirstOrDefaultAsync(sp => sp.Id == request.ProviderId, cancellationToken); + + if (provider is null) + return Result.Fail("Shipping provider not found."); + + provider.UpdatedAt = DateTime.UtcNow; + provider.Enabled = request.Enabled; + provider.Name = request.Name; + provider.Price = request.Price; + provider.TrackingUrl = request.TrackingUrl; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to update shipping provider status."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } } diff --git a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs index 48c8196..d6c9fed 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Pages.Entities; -public class BookPageConfiguration : IEntityTypeConfiguration +public sealed class BookPageConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs index d78b732..7016067 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs @@ -1,3 +1,3 @@ namespace LiteCharms.Features.MidrandBooks.Pages.Models; -public class UpdateBookPage : CreateBookPage; +public sealed class UpdateBookPage : CreateBookPage; diff --git a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs index 5c6f98a..80f3a5d 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.Pages; -public class PageService(IDbContextFactory contextFactory) : IService +public sealed class PageService(IDbContextFactory contextFactory) : IService { public async ValueTask DeleteAllAsync(long authorBookId, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/Refund.cs similarity index 53% rename from LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs rename to LiteCharms.Features.MidrandBooks/Payments/Entities/Refund.cs index 3116896..585ee1a 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/Refund.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/Refund.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.MidrandBooks.Orders.Entities; +using LiteCharms.Features.MidrandBooks.Orders.Entities; + +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; [EntityTypeConfiguration] public class Refund : Models.Refund diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/RefundConfiguration.cs similarity index 84% rename from LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs rename to LiteCharms.Features.MidrandBooks/Payments/Entities/RefundConfiguration.cs index 566b779..5227c6e 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/RefundConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/RefundConfiguration.cs @@ -1,6 +1,6 @@ -namespace LiteCharms.Features.MidrandBooks.Orders.Entities; +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; -public class RefundConfiguration : IEntityTypeConfiguration +public sealed class RefundConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Refund.cs similarity index 68% rename from LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs rename to LiteCharms.Features.MidrandBooks/Payments/Models/Refund.cs index 3f5eec4..5e81d3a 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/Refund.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Refund.cs @@ -1,14 +1,14 @@ -namespace LiteCharms.Features.MidrandBooks.Orders.Models; +namespace LiteCharms.Features.MidrandBooks.Payments.Models; public class Refund { - public Guid Id { get; set; } + public long Id { get; set; } public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } - public Guid OrderId { get; set; } + public long OrderId { get; set; } public RefundTypes Type { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs new file mode 100644 index 0000000..ce3668a --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -0,0 +1,7 @@ +using LiteCharms.Features.MidrandBooks.Abstractions; + +namespace LiteCharms.Features.MidrandBooks.Payments; + +public sealed class PaymentService : IService +{ +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index 901933e..a70e1cf 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -3,11 +3,12 @@ using LiteCharms.Features.MidrandBooks.Authors.Entities; using LiteCharms.Features.MidrandBooks.Customers.Entities; using LiteCharms.Features.MidrandBooks.Orders.Entities; using LiteCharms.Features.MidrandBooks.Pages.Entities; +using LiteCharms.Features.MidrandBooks.Payments.Entities; using LiteCharms.Features.MidrandBooks.Products.Entities; namespace LiteCharms.Features.MidrandBooks.Postgres; -public class MidrandBooksDbContext(DbContextOptions options) : DbContext(options) +public sealed class MidrandBooksDbContext(DbContextOptions options) : DbContext(options) { public DbSet Authors => Set(); diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs index e518cfd..fe25033 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs @@ -2,14 +2,13 @@ namespace LiteCharms.Features.MidrandBooks.Postgres; -public class MidrandBooksDbContextFactory : IDesignTimeDbContextFactory +public sealed class MidrandBooksDbContextFactory : IDesignTimeDbContextFactory { public MidrandBooksDbContext CreateDbContext(string[] args) { var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddUserSecrets(typeof(MidrandBooksDbContext).Assembly) - .AddJsonFile("appsettings.json") .AddEnvironmentVariables() .Build(); diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs new file mode 100644 index 0000000..856fd68 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs @@ -0,0 +1,875 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260527070840_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("SocialMedia") + .HasColumnType("jsonb"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("SocialMedia") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("References") + .HasColumnType("jsonb"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection("Categories") + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Metadata") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("jsonb"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discount") + .HasColumnType("numeric"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId") + .IsUnique(); + + b.ToTable("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Book"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", null) + .WithOne("Price") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", "ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Price"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs new file mode 100644 index 0000000..774c77a --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs @@ -0,0 +1,505 @@ +using System; +using LiteCharms.Features.Models; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Authors", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + PublisherType = table.Column(type: "integer", nullable: false), + Company = table.Column(type: "text", nullable: true), + VatNumber = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + LastName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Biography = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Email = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Website = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + ThumbnailImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + SocialMedia = table.Column(type: "jsonb", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Authors", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Company = table.Column(type: "text", nullable: true), + VatNumber = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: false), + Website = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + SocialMedia = table.Column(type: "jsonb", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + CustomerId = table.Column(type: "bigint", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Total = table.Column(type: "numeric(18,2)", nullable: false), + Notes = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + InvoiceUrl = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + ImageUrl = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + ThumbnailUrls = table.Column(type: "jsonb", nullable: true), + Categories = table.Column(type: "jsonb", nullable: true), + Metadata = table.Column(type: "jsonb", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ShippingProviders", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Type = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "text", nullable: true), + Price = table.Column(type: "numeric", nullable: true), + TrackingUrl = table.Column(type: "text", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShippingProviders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Addresses", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CustomerId = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + BuildingType = table.Column(type: "integer", nullable: false), + Street = table.Column(type: "text", nullable: false), + City = table.Column(type: "text", nullable: false), + State = table.Column(type: "text", nullable: false), + PostalCode = table.Column(type: "text", nullable: false), + Country = table.Column(type: "text", nullable: false), + IsPrimary = table.Column(type: "boolean", nullable: false, defaultValue: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Addresses", x => x.Id); + table.ForeignKey( + name: "FK_Addresses_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Contacts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CustomerId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + IsPrimary = table.Column(type: "boolean", nullable: false, defaultValue: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Contacts", x => x.Id); + table.ForeignKey( + name: "FK_Contacts_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Refunds", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + OrderId = table.Column(type: "bigint", nullable: false), + Type = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Reason = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Refunds", x => x.Id); + table.ForeignKey( + name: "FK_Refunds_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Books", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + AuthorId = table.Column(type: "bigint", nullable: false), + ProductId = table.Column(type: "bigint", nullable: false), + Rating = table.Column(type: "integer", nullable: false), + Ranking = table.Column(type: "integer", nullable: false), + Enabled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Books", x => x.Id); + table.ForeignKey( + name: "FK_Books_Authors_AuthorId", + column: x => x.AuthorId, + principalTable: "Authors", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Books_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Prices", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + ProductId = table.Column(type: "bigint", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Discount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Prices", x => x.Id); + table.ForeignKey( + name: "FK_Prices_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ProductPrice", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + ProductId = table.Column(type: "bigint", nullable: false), + Amount = table.Column(type: "numeric", nullable: false), + Discount = table.Column(type: "numeric", nullable: false), + Enabled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductPrice", x => x.Id); + table.ForeignKey( + name: "FK_ProductPrice_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Shippings", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + OrderId = table.Column(type: "bigint", nullable: false), + AddressId = table.Column(type: "bigint", nullable: false), + ShippingProviderId = table.Column(type: "bigint", nullable: false), + TrackingNumber = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + Status = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Shippings", x => x.Id); + table.ForeignKey( + name: "FK_Shippings_Addresses_AddressId", + column: x => x.AddressId, + principalTable: "Addresses", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Shippings_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Shippings_ShippingProviders_ShippingProviderId", + column: x => x.ShippingProviderId, + principalTable: "ShippingProviders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "BookPages", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AuthorBookId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + ContentType = table.Column(type: "integer", nullable: false), + Number = table.Column(type: "integer", nullable: false, defaultValue: 0), + Content = table.Column(type: "bytea", nullable: false), + Notes = table.Column(type: "jsonb", nullable: true), + References = table.Column(type: "jsonb", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BookPages", x => x.Id); + table.ForeignKey( + name: "FK_BookPages_Books_AuthorBookId", + column: x => x.AuthorBookId, + principalTable: "Books", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "OrderItems", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + OrderId = table.Column(type: "bigint", nullable: false), + AuthorBookId = table.Column(type: "bigint", nullable: false), + ProductPriceId = table.Column(type: "bigint", nullable: false), + Quantity = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderItems", x => x.Id); + table.ForeignKey( + name: "FK_OrderItems_Books_AuthorBookId", + column: x => x.AuthorBookId, + principalTable: "Books", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_OrderItems_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrderItems_Prices_ProductPriceId", + column: x => x.ProductPriceId, + principalTable: "Prices", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Addresses_CustomerId", + table: "Addresses", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_BookPages_AuthorBookId", + table: "BookPages", + column: "AuthorBookId"); + + migrationBuilder.CreateIndex( + name: "IX_Books_AuthorId", + table: "Books", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_Books_ProductId", + table: "Books", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Contacts_CustomerId", + table: "Contacts", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_AuthorBookId", + table: "OrderItems", + column: "AuthorBookId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_OrderId", + table: "OrderItems", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_ProductPriceId", + table: "OrderItems", + column: "ProductPriceId"); + + migrationBuilder.CreateIndex( + name: "IX_Prices_ProductId", + table: "Prices", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductPrice_ProductId", + table: "ProductPrice", + column: "ProductId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Refunds_OrderId", + table: "Refunds", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Shippings_AddressId", + table: "Shippings", + column: "AddressId"); + + migrationBuilder.CreateIndex( + name: "IX_Shippings_OrderId", + table: "Shippings", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Shippings_ShippingProviderId", + table: "Shippings", + column: "ShippingProviderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BookPages"); + + migrationBuilder.DropTable( + name: "Contacts"); + + migrationBuilder.DropTable( + name: "OrderItems"); + + migrationBuilder.DropTable( + name: "ProductPrice"); + + migrationBuilder.DropTable( + name: "Refunds"); + + migrationBuilder.DropTable( + name: "Shippings"); + + migrationBuilder.DropTable( + name: "Books"); + + migrationBuilder.DropTable( + name: "Prices"); + + migrationBuilder.DropTable( + name: "Addresses"); + + migrationBuilder.DropTable( + name: "Orders"); + + migrationBuilder.DropTable( + name: "ShippingProviders"); + + migrationBuilder.DropTable( + name: "Authors"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "Customers"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs new file mode 100644 index 0000000..04c9235 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -0,0 +1,872 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + partial class MidrandBooksDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("SocialMedia") + .HasColumnType("jsonb"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("SocialMedia") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("References") + .HasColumnType("jsonb"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection("Categories") + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Metadata") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("jsonb"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discount") + .HasColumnType("numeric"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId") + .IsUnique(); + + b.ToTable("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Book"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", null) + .WithOne("Price") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", "ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Price"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs index 71515b2..e360385 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Products.Entities; -public class ProductConfiguration : IEntityTypeConfiguration +public sealed class ProductConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs index 4705ba5..635ec8c 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Products.Entities; -public class ProductPriceConfiguration : IEntityTypeConfiguration +public sealed class ProductPriceConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs index 2fec81e..b237f96 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Products.Models; -public class CreateProductPrice +public sealed class CreateProductPrice { public long ProductId { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs index 642a739..44786dd 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.MidrandBooks.Products.Models; -public record CreateProduct +public sealed record CreateProduct { public required ProductTypes Type { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 0232048..99f1c45 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -6,7 +6,7 @@ using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Products; -public class ProductService(IDbContextFactory contextFactory) : IService +public sealed class ProductService(IDbContextFactory contextFactory) : IService { public async ValueTask UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) { diff --git a/LiteCharms.Features.MidrandBooks/appsettings.json b/LiteCharms.Features.MidrandBooks/appsettings.json deleted file mode 100644 index e3261e6..0000000 --- a/LiteCharms.Features.MidrandBooks/appsettings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "Email": { - "Credentials": { - "Username": "shop@litecharms.co.za" - }, - "Port": 465, - "Host": "mail.litecharms.co.za", - "UseSsl": true - }, - "Monitoring": { - "ApiKey": "", - "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", - "ServiceName": "MidrandBooks" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs index 82a7a0c..9013654 100644 --- a/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs @@ -1,5 +1,6 @@ // using System; +using LiteCharms.Features.Models; using LiteCharms.Features.TechShop.Postgres; using LiteCharms.Features.TechShop.Products.Models; using Microsoft.EntityFrameworkCore; diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs index 64abe3b..06d1891 100644 --- a/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs @@ -1,4 +1,5 @@ using System; +using LiteCharms.Features.Models; using LiteCharms.Features.TechShop.Products.Models; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs index 7fd9a00..8c1946e 100644 --- a/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using LiteCharms.Features.Models; using LiteCharms.Features.TechShop.Postgres; using LiteCharms.Features.TechShop.Products.Models; using Microsoft.EntityFrameworkCore; diff --git a/LiteCharms.Features.TechShop/Products/Models/Records.cs b/LiteCharms.Features.TechShop/Products/Models/Records.cs index 22b7dfb..8c7d274 100644 --- a/LiteCharms.Features.TechShop/Products/Models/Records.cs +++ b/LiteCharms.Features.TechShop/Products/Models/Records.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Features.TechShop.Products.Models; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.TechShop.Products.Models; public record CreateProduct { diff --git a/LiteCharms.Features.TechShop/Products/ProductService.cs b/LiteCharms.Features.TechShop/Products/ProductService.cs index 7c9eb83..c414262 100644 --- a/LiteCharms.Features.TechShop/Products/ProductService.cs +++ b/LiteCharms.Features.TechShop/Products/ProductService.cs @@ -1,4 +1,5 @@ -using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Extensions; using LiteCharms.Features.TechShop.Postgres; using LiteCharms.Features.TechShop.Products.Models; diff --git a/LiteCharms.Features/Abstractions/EventBase.cs b/LiteCharms.Features/Abstractions/EventBase.cs index 32def99..f64d71e 100644 --- a/LiteCharms.Features/Abstractions/EventBase.cs +++ b/LiteCharms.Features/Abstractions/EventBase.cs @@ -7,7 +7,7 @@ public abstract class EventBase { public Guid Id { get; set; } = Guid.CreateVersion7(); - public DateTimeOffset EnqueueAt { get; set; } = SouthAfricanTimeZone.UtcNow(); + public DateTimeOffset EnqueueAt { get; set; } = (DateTimeOffset)SouthAfricanTimeZone.UtcNow(); public string CorrelationId { get; set; } = Guid.CreateVersion7().ToString(); } diff --git a/LiteCharms.Features/Email/Configuration/Account.cs b/LiteCharms.Features/Email/Configuration/Account.cs index e0c65e7..02694d7 100644 --- a/LiteCharms.Features/Email/Configuration/Account.cs +++ b/LiteCharms.Features/Email/Configuration/Account.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Configuration; -public class Account +public sealed class Account { public string? Username { get; set; } diff --git a/LiteCharms.Features/Email/Configuration/SmtpSettings.cs b/LiteCharms.Features/Email/Configuration/SmtpSettings.cs index 78798f2..27ab574 100644 --- a/LiteCharms.Features/Email/Configuration/SmtpSettings.cs +++ b/LiteCharms.Features/Email/Configuration/SmtpSettings.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Configuration; -public class SmtpSettings +public sealed class SmtpSettings { public Account? Credentials { get; set; } diff --git a/LiteCharms.Features/Email/EmailService.cs b/LiteCharms.Features/Email/EmailService.cs index 28c38e0..00d5f63 100644 --- a/LiteCharms.Features/Email/EmailService.cs +++ b/LiteCharms.Features/Email/EmailService.cs @@ -4,7 +4,7 @@ using LiteCharms.Features.Email.Models; namespace LiteCharms.Features.Email; -public class EmailService(IOptions options) : IDisposable +public sealed class EmailService(IOptions options) : IDisposable { private readonly SmtpSettings settings = options.Value; private readonly SmtpClient client = new(); @@ -16,6 +16,7 @@ public class EmailService(IOptions options) : IDisposable public async ValueTask> SendEmailAsync(Message message, CancellationToken cancellationToken = default) { using var activity = EmailTelemetry.Source.StartActivity("Email Send"); + activity?.SetTag("email.recipient", message.Recipient?.Address); try @@ -27,21 +28,7 @@ public class EmailService(IOptions options) : IDisposable return Result.Fail("Smtp service is disconnected."); } - var email = new MimeMessage(); - email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!)); - email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!)); - email.Subject = message.Subject!; - - var bodyBuilder = new BodyBuilder(); - - if (message.Body!.Properties.HasAttachments) - foreach (var attachment in message.Body?.Attachments!) - bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken); - - if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message; - if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message; - - email.Body = bodyBuilder.ToMessageBody(); + var email = ConstructEmail(message, cancellationToken); var response = await client.SendAsync(email, cancellationToken); @@ -69,21 +56,9 @@ public class EmailService(IOptions options) : IDisposable await DisconnectAsync(cancellationToken); - if (response.Contains("421")) - { - Status = EmailStatuses.TooManyConnections; + var failCheckResult = HandleNegativeResponse(response); - return Result.Fail(response); - } - - if (response.Contains("451")) - { - Status = EmailStatuses.ConnectionAborted; - - return Result.Fail(response); - } - - EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } }); + if (failCheckResult.IsFailed) return failCheckResult; Status = EmailStatuses.Disconnected; @@ -100,6 +75,48 @@ public class EmailService(IOptions options) : IDisposable } } + private static MimeMessage ConstructEmail(Message message, CancellationToken cancellationToken) + { + var email = new MimeMessage(); + email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!)); + email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!)); + email.Subject = message.Subject!; + + var bodyBuilder = new BodyBuilder(); + + if (message.Body!.Properties.HasAttachments) + foreach (var attachment in message.Body?.Attachments!) + bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken); + + if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message; + if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message; + + email.Body = bodyBuilder.ToMessageBody(); + + return email; + } + + private Result HandleNegativeResponse(string response) + { + if (response.Contains("421", StringComparison.Ordinal)) + { + Status = EmailStatuses.TooManyConnections; + + return Result.Fail(response); + } + + if (response.Contains("451", StringComparison.Ordinal)) + { + Status = EmailStatuses.ConnectionAborted; + + return Result.Fail(response); + } + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } }); + + return Result.Fail(response); + } + public async ValueTask> ConnectAsync(CancellationToken cancellationToken = default) { using var activity = EmailTelemetry.Source.StartActivity("Email Connect"); diff --git a/LiteCharms.Features/Email/Models/Attachment.cs b/LiteCharms.Features/Email/Models/Attachment.cs index 6558058..09dbaf8 100644 --- a/LiteCharms.Features/Email/Models/Attachment.cs +++ b/LiteCharms.Features/Email/Models/Attachment.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class Attachment +public sealed class Attachment { public string? Name { get; set; } diff --git a/LiteCharms.Features/Email/Models/Body.cs b/LiteCharms.Features/Email/Models/Body.cs index 99156d8..42de5e3 100644 --- a/LiteCharms.Features/Email/Models/Body.cs +++ b/LiteCharms.Features/Email/Models/Body.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class Body : IDisposable +public sealed class Body : IDisposable { public string? Message { get; set; } diff --git a/LiteCharms.Features/Email/Models/BodyProperties.cs b/LiteCharms.Features/Email/Models/BodyProperties.cs index 7bdfa01..f3564c3 100644 --- a/LiteCharms.Features/Email/Models/BodyProperties.cs +++ b/LiteCharms.Features/Email/Models/BodyProperties.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class BodyProperties +public sealed class BodyProperties { public bool IsHtml { get; set; } diff --git a/LiteCharms.Features/Email/Models/Message.cs b/LiteCharms.Features/Email/Models/Message.cs index 7432545..44d7f3f 100644 --- a/LiteCharms.Features/Email/Models/Message.cs +++ b/LiteCharms.Features/Email/Models/Message.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class Message : IDisposable +public sealed class Message : IDisposable { public Party? Sender { get; set; } diff --git a/LiteCharms.Features/Email/Models/Party.cs b/LiteCharms.Features/Email/Models/Party.cs index 65c2e85..6aab9e3 100644 --- a/LiteCharms.Features/Email/Models/Party.cs +++ b/LiteCharms.Features/Email/Models/Party.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class Party +public sealed class Party { public string? Name { get; set; } diff --git a/LiteCharms.Features/Email/Models/Response.cs b/LiteCharms.Features/Email/Models/Response.cs index 6bc1c49..5557095 100644 --- a/LiteCharms.Features/Email/Models/Response.cs +++ b/LiteCharms.Features/Email/Models/Response.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Email.Models; -public class Response +public sealed class Response { public int Code { get; set; } diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs index d903299..ca24629 100644 --- a/LiteCharms.Features/Extensions/Hash.cs +++ b/LiteCharms.Features/Extensions/Hash.cs @@ -2,12 +2,12 @@ public static class Hash { - public static Func StringToSha256Hash = (input) => + public static readonly Func StringToSha256Hash = (input) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!))); - public static Func StreamToSha256Hash = (stream) => + public static readonly Func StreamToSha256Hash = (stream) => Convert.ToHexString(SHA256.HashData(stream)); - public static Func BytesToSha256Hash = (bytes) => + public static readonly Func BytesToSha256Hash = (bytes) => Convert.ToHexString(SHA256.HashData(bytes)); } diff --git a/LiteCharms.Features/Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs index 6c4c32e..315a973 100644 --- a/LiteCharms.Features/Extensions/Quartz.cs +++ b/LiteCharms.Features/Extensions/Quartz.cs @@ -64,7 +64,7 @@ public static class Quartz config.UseDefaultThreadPool(options => options.MaxConcurrency = 1); config.UseTimeZoneConverter(); - config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString()); + config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture)); config.UsePersistentStore(storage => { diff --git a/LiteCharms.Features/Extensions/Timezones.cs b/LiteCharms.Features/Extensions/Timezones.cs index 3976240..2c2a068 100644 --- a/LiteCharms.Features/Extensions/Timezones.cs +++ b/LiteCharms.Features/Extensions/Timezones.cs @@ -20,7 +20,7 @@ public static class Timezones ? new DateTimeOffset(sourceDateAdjusted.Ticks, SouthAfricanTimeZone.BaseUtcOffset).LocaliseDateTimeOffset(SouthAfricanTimeZone.BaseUtcOffset) : new DateTimeOffset(sourceDateAdjusted.Ticks, timezone!.BaseUtcOffset).LocaliseDateTimeOffset(timezone.BaseUtcOffset); - return DateTimeOffset.Parse(localised!); + return DateTimeOffset.Parse(localised!, CultureInfo.InvariantCulture); } public static DateTime UtcNow(this TimeZoneInfo timezone) => ToDateTimeWithTimeZone(DateTime.Now, timezone).UtcDateTime; diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index fab834b..552ee0a 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -31,6 +31,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -142,6 +146,7 @@ + diff --git a/LiteCharms.Features/Models/DateRange.cs b/LiteCharms.Features/Models/DateRange.cs index c82cba3..a5616b4 100644 --- a/LiteCharms.Features/Models/DateRange.cs +++ b/LiteCharms.Features/Models/DateRange.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Models; -public class DateRange +public sealed class DateRange { public DateOnly From { get; set; } diff --git a/LiteCharms.Features/Models/PageReference.cs b/LiteCharms.Features/Models/PageReference.cs index ab04eb9..12d53cb 100644 --- a/LiteCharms.Features/Models/PageReference.cs +++ b/LiteCharms.Features/Models/PageReference.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Models; -public class PageReference +public sealed class PageReference { public string? Tag { get; set; } diff --git a/LiteCharms.Features/Models/ProductFilter.cs b/LiteCharms.Features/Models/ProductFilter.cs index c6cc275..bfed022 100644 --- a/LiteCharms.Features/Models/ProductFilter.cs +++ b/LiteCharms.Features/Models/ProductFilter.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Models; -public class ProductFilter +public sealed class ProductFilter { public string? Name { get; set; } diff --git a/LiteCharms.Features/Models/ProductMetadata.cs b/LiteCharms.Features/Models/ProductMetadata.cs index ee193fe..d059f36 100644 --- a/LiteCharms.Features/Models/ProductMetadata.cs +++ b/LiteCharms.Features/Models/ProductMetadata.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Models; -public class ProductMetadata +public sealed class ProductMetadata { public string? Manufacturer { get; set; } diff --git a/LiteCharms.Features/Models/SocialMedia.cs b/LiteCharms.Features/Models/SocialMedia.cs index 296d979..b4f10c2 100644 --- a/LiteCharms.Features/Models/SocialMedia.cs +++ b/LiteCharms.Features/Models/SocialMedia.cs @@ -1,7 +1,7 @@  namespace LiteCharms.Features.Models; -public class SocialMedia +public sealed class SocialMedia { public SocialMediaTypes Type { get; set; } diff --git a/LiteCharms.Features/Quartz/JobOrchestrator.cs b/LiteCharms.Features/Quartz/JobOrchestrator.cs index 4929543..fae63c3 100644 --- a/LiteCharms.Features/Quartz/JobOrchestrator.cs +++ b/LiteCharms.Features/Quartz/JobOrchestrator.cs @@ -3,7 +3,7 @@ using LiteCharms.Features.Quartz.Abstractions; namespace LiteCharms.Features.Quartz; -public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator +public sealed class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator { public async Task SendAsync(TNotification notification, CancellationToken cancellationToken = default) where TNotification : IEvent @@ -11,7 +11,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra var chainedJobGroup = "onetime-jobs"; var scheduler = await schedulerFactory.GetScheduler(cancellationToken); - var jobKey = new JobKey($"{notification.Name.ToLower()}-{notification.CorrelationId.ToLower()}", chainedJobGroup); + var jobKey = new JobKey($"{notification.Name.ToLower(CultureInfo.InvariantCulture)}-{notification.CorrelationId.ToLower(CultureInfo.InvariantCulture)}", chainedJobGroup); var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup); var job = JobBuilder.Create>() @@ -35,7 +35,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra var chainedJobGroup = "scheduled-jobs"; var scheduler = await schedulerFactory.GetScheduler(cancellationToken); - var jobKey = new JobKey($"{notification.Name.ToLower()}", chainedJobGroup); + var jobKey = new JobKey($"{notification.Name.ToLower(CultureInfo.InvariantCulture)}", chainedJobGroup); var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup); var job = JobBuilder.Create>() @@ -53,7 +53,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra .WithDescription($"Scheduled via Main Job at {now:g}") .WithCronSchedule(cronExpression, cron => cron .WithMisfireHandlingInstructionIgnoreMisfires()) - .StartAt(now) + .StartAt((DateTimeOffset)now) .Build(); await scheduler.AddJob(job, replace: true, cancellationToken); diff --git a/LiteCharms.Features/Quartz/MediatorJob.cs b/LiteCharms.Features/Quartz/MediatorJob.cs index 9b52972..5fc7649 100644 --- a/LiteCharms.Features/Quartz/MediatorJob.cs +++ b/LiteCharms.Features/Quartz/MediatorJob.cs @@ -4,7 +4,7 @@ using LiteCharms.Features.Mediator; namespace LiteCharms.Features.Quartz; [DisallowConcurrentExecution] -public class MediatorJob(IMediator mediator) : IJob where TNotification : IEvent +public sealed class MediatorJob(IMediator mediator) : IJob where TNotification : IEvent { public async Task Execute(IJobExecutionContext context) { diff --git a/LiteCharms.Features/Quartz/RetryJobListener.cs b/LiteCharms.Features/Quartz/RetryJobListener.cs index 968b8bb..cc12662 100644 --- a/LiteCharms.Features/Quartz/RetryJobListener.cs +++ b/LiteCharms.Features/Quartz/RetryJobListener.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.Quartz; -public class RetryJobListener : IJobListener +public sealed class RetryJobListener : IJobListener { public string Name => "RetryJobListener"; diff --git a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs index 6679d5d..c1b9ae4 100644 --- a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs +++ b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs @@ -31,7 +31,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3) if(string.IsNullOrWhiteSpace(fileHash)) return Result.Fail("Failed to compute file hash."); - var fileKey = $"{fileHash.ToLower()}{Path.GetExtension(fileName)}"; + var fileKey = $"{fileHash.ToLower(CultureInfo.InvariantCulture)}{Path.GetExtension(fileName)}"; var putRequest = new PutObjectRequest { diff --git a/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs b/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs index 8093591..5f4ddac 100644 --- a/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs +++ b/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs @@ -3,7 +3,7 @@ using static LiteCharms.Features.S3.Constants; namespace LiteCharms.Features.S3; -public class BookshopInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopInvoicesBucketName)] IAmazonS3 amazonS3) : +public sealed class BookshopInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopInvoicesBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service { protected override string BucketName => configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:BucketName").Value ?? ""; diff --git a/LiteCharms.Features/S3/BookshopQuotesS3Service.cs b/LiteCharms.Features/S3/BookshopQuotesS3Service.cs index 0f87fa0..2362d66 100644 --- a/LiteCharms.Features/S3/BookshopQuotesS3Service.cs +++ b/LiteCharms.Features/S3/BookshopQuotesS3Service.cs @@ -3,7 +3,7 @@ using static LiteCharms.Features.S3.Constants; namespace LiteCharms.Features.S3; -public class BookshopQuotesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopQuotesBucketName)] IAmazonS3 amazonS3) : +public sealed class BookshopQuotesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopQuotesBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service { protected override string BucketName => configuration.GetSection($"{BookshopQuotesS3SettingsSection}:BucketName").Value ?? ""; diff --git a/LiteCharms.Features/S3/BookshopS3Service.cs b/LiteCharms.Features/S3/BookshopS3Service.cs index aff9cf5..024b829 100644 --- a/LiteCharms.Features/S3/BookshopS3Service.cs +++ b/LiteCharms.Features/S3/BookshopS3Service.cs @@ -3,7 +3,7 @@ using static LiteCharms.Features.S3.Constants; namespace LiteCharms.Features.S3; -public class BookshopS3Service(IConfiguration configuration, [FromKeyedServices(BookshopBucketName)] IAmazonS3 amazonS3) : +public sealed class BookshopS3Service(IConfiguration configuration, [FromKeyedServices(BookshopBucketName)] IAmazonS3 amazonS3) : S3ServiceBase(amazonS3), IS3Service { protected override string BucketName => configuration.GetSection($"{BookshopS3SettingsSection}:BucketName").Value ?? ""; diff --git a/LiteCharms.Features/S3/Configuration/S3Settings.cs b/LiteCharms.Features/S3/Configuration/S3Settings.cs index 6be7460..72984fa 100644 --- a/LiteCharms.Features/S3/Configuration/S3Settings.cs +++ b/LiteCharms.Features/S3/Configuration/S3Settings.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.S3.Configuration; -public class S3Settings +public sealed class S3Settings { public string? ServiceUrl { get; set; } diff --git a/LiteCharms.Features/ServiceBus/EmailServiceBus.cs b/LiteCharms.Features/ServiceBus/EmailServiceBus.cs index c79438e..f83ed8e 100644 --- a/LiteCharms.Features/ServiceBus/EmailServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/EmailServiceBus.cs @@ -4,7 +4,7 @@ using LiteCharms.Features.ServiceBus.Queues; namespace LiteCharms.Features.ServiceBus; -public class EmailServiceBus(EmailQueue messages) : IEventBus +public sealed class EmailServiceBus(EmailQueue messages) : IEventBus { public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) where TEvent : class, IEvent diff --git a/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs index 5ccae66..5a74145 100644 --- a/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs +++ b/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs @@ -3,7 +3,7 @@ using LiteCharms.Features.ServiceBus.Queues; namespace LiteCharms.Features.ServiceBus.Exchanges; -public class EmailExchange(EmailQueue messages, ILogger logger, IPublisher mediator) : BackgroundService +public sealed class EmailExchange(EmailQueue messages, ILogger logger, IPublisher mediator) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs index c94fb5d..32d84f1 100644 --- a/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs +++ b/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.ServiceBus.Exchanges; -public class GeneralExchange(GeneralQueue messages) : BackgroundService +public sealed class GeneralExchange(GeneralQueue messages) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs index 645ab49..3705993 100644 --- a/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs +++ b/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.ServiceBus.Exchanges; -public class SalesExchange(SalesQueue messages) : BackgroundService +public sealed class SalesExchange(SalesQueue messages) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs b/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs index 94edb37..0694c5c 100644 --- a/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs @@ -4,7 +4,7 @@ using LiteCharms.Features.ServiceBus.Queues; namespace LiteCharms.Features.ServiceBus; -public class GeneralServiceBus(GeneralQueue messages) : IEventBus +public sealed class GeneralServiceBus(GeneralQueue messages) : IEventBus { public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) where TEvent : class, IEvent diff --git a/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs b/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs index 508ad5f..700a1f1 100644 --- a/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs +++ b/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs @@ -2,4 +2,4 @@ namespace LiteCharms.Features.ServiceBus.Queues; -public class EmailQueue : EventBusQueueBase, IEventBusQueue; +public sealed class EmailQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs b/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs index 3d79a2f..ef155ec 100644 --- a/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs +++ b/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs @@ -2,4 +2,4 @@ namespace LiteCharms.Features.ServiceBus.Queues; -public class GeneralQueue : EventBusQueueBase, IEventBusQueue; +public sealed class GeneralQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs b/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs index 8dc5601..bb76de8 100644 --- a/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs +++ b/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs @@ -2,4 +2,4 @@ namespace LiteCharms.Features.ServiceBus.Queues; -public class SalesQueue : EventBusQueueBase, IEventBusQueue; +public sealed class SalesQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Features/ServiceBus/SalesServiceBus.cs b/LiteCharms.Features/ServiceBus/SalesServiceBus.cs index 853657b..d30050c 100644 --- a/LiteCharms.Features/ServiceBus/SalesServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/SalesServiceBus.cs @@ -4,7 +4,7 @@ using LiteCharms.Features.ServiceBus.Queues; namespace LiteCharms.Features.ServiceBus; -public class SalesServiceBus(SalesQueue messages) : IEventBus +public sealed class SalesServiceBus(SalesQueue messages) : IEventBus { public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) where TEvent : class, IEvent diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index 7b1a9f7..1945799 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -1,6 +1,7 @@ + From 2a0b34c73013a0908ffca825a882f8f77ac5fed8 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 28 May 2026 09:05:49 +0200 Subject: [PATCH 059/153] Refactored database registration to allow postgres to use internal representations for afster performance --- .../Fixture.cs | 7 +- .../IntegrationFactAttribute.cs | 10 ++ ...eCharms.Features.MidrandBooks.Tests.csproj | 1 + .../ProductServiceFeatureTest.cs | 90 +++++++++++ .../appsettings.json | 6 - .../Authors/Entities/AuthorConfiguration.cs | 3 +- .../Entities/CustomerConfiguration.cs | 3 +- .../Extensions/Postgres.cs | 12 +- .../Extensions/Shop.cs | 12 +- .../LiteCharms.Features.MidrandBooks.csproj | 3 + .../Orders/Entities/OrderConfiguration.cs | 2 +- .../Pages/Entities/BookPageConfiguration.cs | 7 +- ...ner.cs => 20260528052014_Init.Designer.cs} | 150 +++++++++++++++--- ...7070840_Init.cs => 20260528052014_Init.cs} | 71 ++++----- .../MidrandBooksDbContextModelSnapshot.cs | 148 ++++++++++++++--- .../Products/Entities/ProductConfiguration.cs | 7 +- .../Products/Entities/ProductPrice.cs | 2 +- .../Products/Models/CreateProductPrice.cs | 10 -- .../Products/Models/Records.cs | 7 + .../Products/ProductService.cs | 6 +- 20 files changed, 441 insertions(+), 116 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs create mode 100644 LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs rename LiteCharms.Features.MidrandBooks/Postgres/Migrations/{20260527070840_Init.Designer.cs => 20260528052014_Init.Designer.cs} (87%) rename LiteCharms.Features.MidrandBooks/Postgres/Migrations/{20260527070840_Init.cs => 20260528052014_Init.cs} (91%) delete mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs diff --git a/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs b/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs index 38c8bd7..d99ea23 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs @@ -1,4 +1,5 @@ using LiteCharms.Features.Extensions; +using LiteCharms.Features.MidrandBooks.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; namespace LiteCharms.Features.MidrandBooks.Tests; @@ -11,6 +12,10 @@ public class Fixture : IDisposable public IMediator Mediator { get; set; } + private readonly CancellationTokenSource cancellationTokenSource = new(); + + public CancellationToken CancellationToken => cancellationTokenSource.Token; + public Fixture() { Configuration = new ConfigurationBuilder() @@ -23,12 +28,12 @@ public class Fixture : IDisposable Services = new ServiceCollection() .AddMediator() .AddLogging() - //.AddMidrandShopServices() .AddEmailServiceBus() .AddGarageS3(Configuration) .AddMidrandShopDatabase(Configuration) .AddEmailServices(Configuration) .AddSingleton(Configuration) + .AddShopServices() .BuildServiceProvider(); Mediator = Services.GetRequiredService(); diff --git a/LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs b/LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs new file mode 100644 index 0000000..24194e6 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class IntegrationFactAttribute : FactAttribute +{ + public IntegrationFactAttribute() + { + if(!Debugger.IsAttached) + Skip = "This test requires the debugger to be attached."; + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj index 824c4ff..9eba9bd 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj +++ b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj @@ -39,6 +39,7 @@ + diff --git a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs new file mode 100644 index 0000000..3c41d55 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs @@ -0,0 +1,90 @@ +using LiteCharms.Features.MidrandBooks.Products; +using LiteCharms.Features.MidrandBooks.Products.Models; +using LiteCharms.Features.Models; +using System.Text.Json; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class ProductServiceFeatureTest(Fixture fixture, ITestOutputHelper output) : IClassFixture +{ + private readonly ProductService productService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task GetProductsAsync_ShouldReturn_RetultProducts() + { + var range = new DateRange + { + From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 1000 + }; + + var result = await productService.GetProductsAsync(0, range, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task UpdateProductStatusAsync_ShouldReturn_ResultTrue() + { + var result = await productService.UpdateProductStatusAsync(2, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateProductPriceStatusAsync_ShouldReturn_ResultTrue() + { + var result = await productService.UpdateProductPriceStatusAsync(2, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreateProductPriceAsync_Should_Return_NewProductPriceId() + { + var request = new CreateProductPrice + { + Amount = 29.99m, + Discount = 0.00m + }; + + var result = await productService.CreateProductPriceAsync(2, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess, "Product price creation should be successful."); + Assert.True(result.Value > 0, "New ProductPriceId should be greater than 0."); + + output.WriteLine($"Created ProductPriceId: {result.Value}"); + } + + [IntegrationFact] + public async Task CreateProductAsync_Result_Returns_ProductId() + { + var request = new CreateProduct + { + Name = "Systems Rewired", + Description = "[Design], , AND /CHAORS/ IN ***SYNC***", + Summary = "A comprehensive guide to systems thinking and design.", + ImageUrl = "https://bookshop.cdn.khongisa.co.za/design/2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp", + Type = ProductTypes.Book, + Categories = ["Systems Thinking", "Design", "Programming"], + Metadata = new ProductMetadata + { + CopyrightInfo = "© 2024 John Doe. All rights reserved.", + ManufactureDate = "2024-06-01", + Manufacturer = "TechWave Publishing", + SerialNumber = "SR-2024-0001" + } + }; + + var result = await productService.CreateProductAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess, "Product creation should be successful."); + Assert.True(result.Value > 0, "ProductId should be greater than 0."); + + output.WriteLine($"Created ProductId: {result.Value}"); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/appsettings.json b/LiteCharms.Features.MidrandBooks.Tests/appsettings.json index 1066af9..7f9a6b8 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/appsettings.json +++ b/LiteCharms.Features.MidrandBooks.Tests/appsettings.json @@ -5,12 +5,6 @@ "BucketName": "bookshop", "CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za" }, - "BookshopQuotesS3Settings": { - "ServiceUrl": "http://192.168.1.177:30900", - "Region": "garage", - "BucketName": "bookshop.quotes", - "CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za" - }, "Email": { "Credentials": { "Username": "shop@litecharms.co.za" diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs index 0c0af10..8a7de14 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs @@ -18,7 +18,8 @@ public sealed class AuthorConfiguration : IEntityTypeConfiguration builder.Property(f => f.Website).IsRequired(false).HasMaxLength(1024); builder.Property(f => f.ImageUrl).IsRequired().HasMaxLength(2048); builder.Property(f => f.ThumbnailImageUrl).IsRequired(false).HasMaxLength(2048); - builder.Property(f => f.SocialMedia).IsRequired(false).HasColumnType("jsonb"); builder.Property(f => f.Enabled).HasDefaultValue(true); + + builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); }); } } diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs index f5015a7..b68c086 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs @@ -14,7 +14,8 @@ public sealed class CustomerConfiguration : IEntityTypeConfiguration builder.Property(c => c.Email).IsRequired(); builder.Property(c => c.Phone).IsRequired(); builder.Property(c => c.Website).IsRequired(); - builder.Property(c => c.SocialMedia).IsRequired(false).HasColumnType("jsonb"); builder.Property(c => c.Enabled).HasDefaultValue(true); + + builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); }); } } diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs index 228634d..54307dc 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs @@ -8,8 +8,18 @@ public static class Postgres public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) { + var connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName); + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); + + dataSourceBuilder.ConfigureTypeLoading(options => { options.EnableTypeLoading(false); }); + + var dataSource = dataSourceBuilder.Build(); + + services.AddSingleton(dataSource); + services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName))); + options.UseNpgsql(dataSource)); return services; } diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs index 43e407e..f407c5b 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -4,19 +4,15 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Shop { - public static IServiceCollection AddShopServices(this IServiceCollection services, Assembly assembly, ServiceLifetime serviceLifetime) + public static IServiceCollection AddShopServices(this IServiceCollection services) { var serviceType = typeof(IService); - var implementations = assembly.GetTypes() + var implementations = Assembly.GetExecutingAssembly().GetTypes() .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); - foreach (var implementation in implementations) - { - var descriptor = new ServiceDescriptor(serviceType, implementation, serviceLifetime); - - services.Add(descriptor); - } + foreach (var implementation in implementations) + services.AddScoped(implementation); return services; } diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 17db6dd..82d3a1b 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -162,5 +162,8 @@ + + + diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs index c7ae590..629b030 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs @@ -11,7 +11,7 @@ public sealed class OrderConfiguration : IEntityTypeConfiguration builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()"); builder.Property(o => o.Status).IsRequired(); - builder.Property(o => o.Total).IsRequired().HasColumnType("decimal(18,2)"); + builder.Property(o => o.Total).IsRequired().HasPrecision(18, 2); builder.Property(o => o.Notes).HasMaxLength(1000); } } diff --git a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs index d6c9fed..eb33f55 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs @@ -13,10 +13,11 @@ public sealed class BookPageConfiguration : IEntityTypeConfiguration builder.Property(bp => bp.AuthorBookId).IsRequired(); builder.Property(bp => bp.Content).IsRequired(); builder.Property(bp => bp.Type).IsRequired(); - builder.Property(bp => bp.ContentType).IsRequired(); - builder.Property(bp => bp.Notes).IsRequired(false).HasColumnType("jsonb"); - builder.Property(bp => bp.References).IsRequired(false).HasColumnType("jsonb"); + builder.Property(bp => bp.ContentType).IsRequired(); builder.Property(bp => bp.Enabled).HasDefaultValue(true); + builder.Property(bp => bp.Notes).IsRequired(false); + + builder.OwnsMany(f => f.References, b => { b.ToJson(); }); builder.HasOne(f =>f.Book) .WithMany(b => b.Pages) diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs similarity index 87% rename from LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs rename to LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs index 856fd68..ed24e21 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs @@ -1,7 +1,6 @@ // using System; using LiteCharms.Features.MidrandBooks.Postgres; -using LiteCharms.Features.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -13,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations { [DbContext(typeof(MidrandBooksDbContext))] - [Migration("20260527070840_Init")] + [Migration("20260528052014_Init")] partial class Init { /// @@ -112,9 +111,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Property("PublisherType") .HasColumnType("integer"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("ThumbnailImageUrl") .HasMaxLength(2048) .HasColumnType("character varying(2048)"); @@ -291,9 +287,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .IsRequired() .HasColumnType("text"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("UpdatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -338,7 +331,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("integer"); b.Property("Total") - .HasColumnType("decimal(18,2)"); + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); b.Property("UpdatedAt") .ValueGeneratedOnAdd() @@ -494,17 +488,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("boolean") .HasDefaultValue(true); - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); b.Property("Number") .ValueGeneratedOnAdd() .HasColumnType("integer") .HasDefaultValue(0); - b.Property("References") - .HasColumnType("jsonb"); - b.Property("Type") .HasColumnType("integer"); @@ -570,8 +561,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.PrimitiveCollection("Categories") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Categories") + .HasColumnType("text[]"); b.Property("CreatedAt") .ValueGeneratedOnAdd() @@ -591,9 +582,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(1024) .HasColumnType("character varying(1024)"); - b.Property("Metadata") - .HasColumnType("jsonb"); - b.Property("Name") .IsRequired() .HasMaxLength(255) @@ -604,8 +592,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(512) .HasColumnType("character varying(512)"); - b.PrimitiveCollection("ThumbnailUrls") - .HasColumnType("jsonb"); + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); b.Property("Type") .HasColumnType("integer"); @@ -714,6 +702,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Product"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") @@ -736,6 +756,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Customer"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => { b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") @@ -798,7 +850,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .OnDelete(DeleteBehavior.NoAction) .IsRequired(); + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + b.Navigation("Book"); + + b.Navigation("References"); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => @@ -812,6 +891,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs similarity index 91% rename from LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs rename to LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs index 774c77a..1c0cb96 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs @@ -1,5 +1,4 @@ using System; -using LiteCharms.Features.Models; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; @@ -31,8 +30,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations Website = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), ThumbnailImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), - SocialMedia = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + SocialMedia = table.Column(type: "jsonb", nullable: true) }, constraints: table => { @@ -52,8 +51,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations Email = table.Column(type: "text", nullable: false), Website = table.Column(type: "text", nullable: false), Phone = table.Column(type: "text", nullable: false), - SocialMedia = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + SocialMedia = table.Column(type: "jsonb", nullable: true) }, constraints: table => { @@ -70,7 +69,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), CustomerId = table.Column(type: "bigint", nullable: false), Status = table.Column(type: "integer", nullable: false), - Total = table.Column(type: "numeric(18,2)", nullable: false), + Total = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), Notes = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), InvoiceUrl = table.Column(type: "text", nullable: true) }, @@ -92,10 +91,10 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), ImageUrl = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), - ThumbnailUrls = table.Column(type: "jsonb", nullable: true), - Categories = table.Column(type: "jsonb", nullable: true), - Metadata = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: false) + ThumbnailUrls = table.Column(type: "text[]", nullable: true), + Categories = table.Column(type: "text[]", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: false), + Metadata = table.Column(type: "jsonb", nullable: true) }, constraints: table => { @@ -260,29 +259,29 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations onDelete: ReferentialAction.Restrict); }); - migrationBuilder.CreateTable( - name: "ProductPrice", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - ProductId = table.Column(type: "bigint", nullable: false), - Amount = table.Column(type: "numeric", nullable: false), - Discount = table.Column(type: "numeric", nullable: false), - Enabled = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ProductPrice", x => x.Id); - table.ForeignKey( - name: "FK_ProductPrice_Products_ProductId", - column: x => x.ProductId, - principalTable: "Products", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); + //migrationBuilder.CreateTable( + // name: "ProductPrice", + // columns: table => new + // { + // Id = table.Column(type: "bigint", nullable: false) + // .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + // ProductId = table.Column(type: "bigint", nullable: false), + // Amount = table.Column(type: "numeric", nullable: false), + // Discount = table.Column(type: "numeric", nullable: false), + // Enabled = table.Column(type: "boolean", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_ProductPrice", x => x.Id); + // table.ForeignKey( + // name: "FK_ProductPrice_Products_ProductId", + // column: x => x.ProductId, + // principalTable: "Products", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); migrationBuilder.CreateTable( name: "Shippings", @@ -334,9 +333,9 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations ContentType = table.Column(type: "integer", nullable: false), Number = table.Column(type: "integer", nullable: false, defaultValue: 0), Content = table.Column(type: "bytea", nullable: false), - Notes = table.Column(type: "jsonb", nullable: true), - References = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + Notes = table.Column(type: "text[]", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + References = table.Column(type: "jsonb", nullable: true) }, constraints: table => { diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 04c9235..6340386 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -1,7 +1,6 @@ // using System; using LiteCharms.Features.MidrandBooks.Postgres; -using LiteCharms.Features.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -109,9 +108,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Property("PublisherType") .HasColumnType("integer"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("ThumbnailImageUrl") .HasMaxLength(2048) .HasColumnType("character varying(2048)"); @@ -288,9 +284,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .IsRequired() .HasColumnType("text"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("UpdatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -335,7 +328,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("integer"); b.Property("Total") - .HasColumnType("decimal(18,2)"); + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); b.Property("UpdatedAt") .ValueGeneratedOnAdd() @@ -491,17 +485,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("boolean") .HasDefaultValue(true); - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); b.Property("Number") .ValueGeneratedOnAdd() .HasColumnType("integer") .HasDefaultValue(0); - b.Property("References") - .HasColumnType("jsonb"); - b.Property("Type") .HasColumnType("integer"); @@ -567,8 +558,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.PrimitiveCollection("Categories") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Categories") + .HasColumnType("text[]"); b.Property("CreatedAt") .ValueGeneratedOnAdd() @@ -588,9 +579,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(1024) .HasColumnType("character varying(1024)"); - b.Property("Metadata") - .HasColumnType("jsonb"); - b.Property("Name") .IsRequired() .HasMaxLength(255) @@ -601,8 +589,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(512) .HasColumnType("character varying(512)"); - b.PrimitiveCollection("ThumbnailUrls") - .HasColumnType("jsonb"); + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); b.Property("Type") .HasColumnType("integer"); @@ -711,6 +699,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Product"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") @@ -733,6 +753,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Customer"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => { b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") @@ -795,7 +847,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .OnDelete(DeleteBehavior.NoAction) .IsRequired(); + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + b.Navigation("Book"); + + b.Navigation("References"); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => @@ -809,6 +888,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs index e360385..db18548 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs @@ -14,9 +14,10 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); builder.Property(f => f.Description).HasMaxLength(1024); builder.Property(f => f.ImageUrl).HasMaxLength(1024); - builder.Property(f => f.ThumbnailUrls).IsRequired(false).HasColumnType("jsonb"); - builder.Property(f => f.Metadata).IsRequired(false).HasColumnType("jsonb"); - builder.Property(f => f.Categories).IsRequired(false).HasColumnType("jsonb"); builder.Property(f => f.Enabled).HasDefaultValue(false); + builder.Property(f => f.Categories).IsRequired(false); + builder.Property(f => f.ThumbnailUrls).IsRequired(false); + + builder.OwnsOne(f => f.Metadata, b => { b.ToJson(); }); } } diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs index e78c42b..ba6a593 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs @@ -3,5 +3,5 @@ [EntityTypeConfiguration] public class ProductPrice : Models.ProductPrice { - public virtual Product Product { get; set; } = new(); + public virtual Product? Product { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs deleted file mode 100644 index b237f96..0000000 --- a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace LiteCharms.Features.MidrandBooks.Products.Models; - -public sealed class CreateProductPrice -{ - public long ProductId { get; set; } - - public decimal Amount { get; set; } - - public decimal Discount { get; set; } -} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs index 44786dd..9c09551 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs @@ -20,3 +20,10 @@ public sealed record CreateProduct public ProductMetadata? Metadata { get; set; } } + +public sealed class CreateProductPrice +{ + public decimal Amount { get; set; } + + public decimal Discount { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 99f1c45..a43e489 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -228,8 +228,8 @@ public sealed class ProductService(IDbContextFactory cont { try { - var fromDate = range.From.ToDateTime(TimeOnly.MinValue); - var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); @@ -244,7 +244,7 @@ public sealed class ProductService(IDbContextFactory cont .AsSplitQuery() .ToArrayAsync(cancellationToken); - return await context.SaveChangesAsync(cancellationToken) > 0 + return products?.Length > 0 ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) : Result.Fail(new Error("Failed to retrieve products.")); } From 4e53ff8a37ecad1edcd59e7ed8742303bca41736 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 28 May 2026 17:28:33 +0200 Subject: [PATCH 060/153] ProductService tested and stable --- ...eCharms.Features.MidrandBooks.Tests.csproj | 1 + .../ProductServiceFeatureTest.cs | 54 ++++++++++++++++++- .../Products/ProductService.cs | 23 ++++---- 3 files changed, 68 insertions(+), 10 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj index 9eba9bd..3b0e796 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj +++ b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj @@ -39,6 +39,7 @@ + diff --git a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs index 3c41d55..c498909 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs @@ -1,7 +1,6 @@ using LiteCharms.Features.MidrandBooks.Products; using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.Models; -using System.Text.Json; namespace LiteCharms.Features.MidrandBooks.Tests; @@ -9,6 +8,59 @@ public class ProductServiceFeatureTest(Fixture fixture, ITestOutputHelper output { private readonly ProductService productService = fixture.Services.GetRequiredService(); + [IntegrationFact] + public async Task GetProductPriceAsync_ShouldReturn_RetultOneProductPrice() + { + var result = await productService.GetProductPriceAsync(2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task GetProductPricesAsync_ShouldReturn_RetultProductPriceList() + { + var result = await productService.GetProductPricesAsync(2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task SearchProductsAsync_ShouldReturn_RetultMatchingProducts() + { + var filter = new ProductFilter + { + Name = "system", + Manufacturer = "techwave", + SerialNumber = "2024", + MinPrice = 10, + MaxPrice = 30 + }; + + var result = await productService.SearchProductsAsync(filter, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task GetProductAsync_ShouldReturn_RetultOneProduct() + { + var result = await productService.GetProductAsync(2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + [IntegrationFact] public async Task GetProductsAsync_ShouldReturn_RetultProducts() { diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index a43e489..aef7710 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -64,17 +64,22 @@ public sealed class ProductService(IDbContextFactory cont var query = context.Products.AsQueryable(); + var cultureInfo = CultureInfo.InvariantCulture; + + if (!string.IsNullOrWhiteSpace(filter.Name)) + query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Name}%")); + if (!string.IsNullOrWhiteSpace(filter.Title)) - query = query.Where(p => p.Name!.Contains(filter.Title)); + query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Title}%")); if (!string.IsNullOrWhiteSpace(filter.Category)) - query = query.Where(p => p.Categories!.Any(c => c == filter.Category)); + query = query.Where(p => p.Categories.Contains(filter.Category)); if (!string.IsNullOrWhiteSpace(filter.Manufacturer)) - query = query.Where(p => p.Metadata!.Manufacturer == filter.Manufacturer); + query = query.Where(p => EF.Functions.ILike(p.Metadata!.Manufacturer!, $"%{filter.Manufacturer}%")); if (!string.IsNullOrWhiteSpace(filter.SerialNumber)) - query = query.Where(p => p.Metadata!.SerialNumber == filter.SerialNumber); + query = query.Where(p => EF.Functions.ILike(p.Metadata!.SerialNumber!, $"%{filter.SerialNumber}%")); if (filter.MinPrice > 0) query = query.Where(p => p.Prices!.Any(pr => pr.Amount >= filter.MinPrice && pr.Amount <= filter.MaxPrice)); @@ -165,7 +170,7 @@ public sealed class ProductService(IDbContextFactory cont } } - public async ValueTask> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default) + public async ValueTask> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default) { try { @@ -175,16 +180,16 @@ public sealed class ProductService(IDbContextFactory cont .AsNoTracking() .OrderByDescending(p => p.CreatedAt) .ThenBy(p => p.UpdatedAt) - .FirstOrDefaultAsync(p => p.ProductId == productId, cancellationToken); + .FirstOrDefaultAsync(p => p.ProductId == productId && p.Enabled, cancellationToken); return product is not null - ? Result.Ok(new[] { product.ToModel() }) - : Result.Fail(new Error($"No price found for product with ID {productId}")); + ? Result.Ok(product.ToModel()) + : Result.Fail(new Error($"No price found for product with ID {productId}")); } catch (Exception ex) { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); + return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } From 2546c34ffc19499b0115cfacf21f6824d10dd6d4 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 29 May 2026 01:05:22 +0200 Subject: [PATCH 061/153] Wrote tests for most services, applied EF core optimisations --- .../AuthorServiceFeatureTests.cs | 81 +++++++ .../BooksServiceFeatureTests.cs | 52 +++++ .../{ => Common}/Fixture.cs | 2 +- .../{ => Common}/IntegrationFactAttribute.cs | 2 +- .../CustomerServiceFeatureTests.cs | 201 ++++++++++++++++++ .../OrderServiceFeatureTests.cs | 167 +++++++++++++++ .../PageServiceFeatureTests.cs | 63 ++++++ ...eTest.cs => ProductServiceFeatureTests.cs} | 3 +- .../AuthorBooks/BooksService.cs | 34 +-- .../AuthorBooks/Entities/AuthorBook.cs | 2 +- .../Authors/AuthorService.cs | 69 ++---- .../Authors/Models/Author.cs | 2 +- .../Customers/CustomerService.cs | 112 +++++----- .../Customers/Models/Customer.cs | 2 +- .../Customers/Models/Records.cs | 2 +- .../Orders/Models/Records.cs | 2 +- .../Orders/OrderService.cs | 158 +++++++------- .../Pages/Models/BookPage.cs | 2 +- .../Pages/Models/CreateBookPage.cs | 2 +- .../Pages/PageService.cs | 86 +++----- .../MidrandBooksDbContextModelSnapshot.cs | 14 +- .../Products/ProductService.cs | 32 ++- 22 files changed, 793 insertions(+), 297 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs create mode 100644 LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs rename LiteCharms.Features.MidrandBooks.Tests/{ => Common}/Fixture.cs (95%) rename LiteCharms.Features.MidrandBooks.Tests/{ => Common}/IntegrationFactAttribute.cs (77%) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs create mode 100644 LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs create mode 100644 LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs rename LiteCharms.Features.MidrandBooks.Tests/{ProductServiceFeatureTest.cs => ProductServiceFeatureTests.cs} (96%) diff --git a/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs new file mode 100644 index 0000000..1a0826a --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs @@ -0,0 +1,81 @@ +using LiteCharms.Features.MidrandBooks.Authors; +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Tests.Common; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class AuthorServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture +{ + private readonly AuthorService authorService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateAuthorAsync_ShouldReturn_ResultWithAuthorId() + { + var request = new CreateAuthor + { + Name = "John", + LastName = "Doe", + Company = "Solo Publishers", + Email = "solo@publishers.co.za", + PublisherType = PublisherTypes.Independent, + ImageUrl = "" + }; + + var result = await authorService.CreateAuthorAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task UpdateAuthorAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateAuthor + { + Name = "Jane", + LastName = "Doe", + Company = "Solo Publishers", + Email = "solo@publishers.co.za", + PublisherType = PublisherTypes.Independent, + ImageUrl = "" + }; + + var result = await authorService.UpdateAuthorAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetAuthors_ShouldReturn_ResultWithAuthorList() + { + var range = new DateRange + { + From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 1000 + }; + + var result = await authorService.GetAuthorsAsync(range, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetAuthorAsync_ShouldReturn_ResultWithAuthor() + { + var result = await authorService.GetAuthorAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task UpdateAuthorStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await authorService.UpdateAuthorStatusAsync(1, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs new file mode 100644 index 0000000..d65e0d6 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs @@ -0,0 +1,52 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks; +using LiteCharms.Features.MidrandBooks.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class BooksServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly BooksService bookService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateBookAsync_ShouldReturn_ResultWithBookId() + { + var result = await bookService.CreateBookAsync(1, 2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetBookAsync_ShouldReturn_ResultWithBook() + { + var result = await bookService.GetBookAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetBooksByAuthorAsync_ShouldReturn_ResultWithAuthorBooks() + { + var result = await bookService.GetBooksByAuthorAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetPublishedBooksAsync_ShouldReturn_ResultWithBublishedBooks() + { + var result = await bookService.GetPublishedBooksAsync(0, 1000, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task UpdateBookStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await bookService.UpdateBookStatusAsync(1, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs similarity index 95% rename from LiteCharms.Features.MidrandBooks.Tests/Fixture.cs rename to LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs index d99ea23..6948d48 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs @@ -2,7 +2,7 @@ using LiteCharms.Features.MidrandBooks.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; -namespace LiteCharms.Features.MidrandBooks.Tests; +namespace LiteCharms.Features.MidrandBooks.Tests.Common; public class Fixture : IDisposable { diff --git a/LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs b/LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs similarity index 77% rename from LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs rename to LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs index 24194e6..304f78e 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandBooks.Tests; +namespace LiteCharms.Features.MidrandBooks.Tests.Common; public class IntegrationFactAttribute : FactAttribute { diff --git a/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs new file mode 100644 index 0000000..5edea77 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs @@ -0,0 +1,201 @@ +using LiteCharms.Features.MidrandBooks.Customers; +using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.MidrandBooks.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class CustomerServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly CustomerService customerService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateCustomerAsync_ShouldReturn_ResultWithCustomerId() + { + var request = new CreateCustomer + { + Company = "Book Lovers", + Email = "hank@booklovers.com", + Phone = "555 1245 8577", + Website = "https://www.booklovers.com" + }; + + var result = await customerService.CreateCustomerAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task CreateCustomerContactAsync_ShouldReturn_ResultWithCustomerContactId() + { + var request = new CreateCustomerContact + { + Name = "Sipho", + LastName = "Madlanga", + Phone = "0710857365", + Email = "sipho@madlanga.africa", + Type = ContactTypes.Business + }; + + var result = await customerService.CreateCustomerContactAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task CreateCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddressId() + { + var request = new CreateCustomerAddress + { + Name = "Business", + BuildingType = AddressBuildingTypes.MixedUse, + Type = AddressType.Shipping, + Street = "123 Building 4, XYZ Suburb, Some Region", + City = "Johannesburg", + State = "Gauteng", + Country = "South Africa", + IsPrimary = true, + Enabled = true, + PostalCode = "12345" + }; + + var result = await customerService.CreateCustomerAddressAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task UpdateCustomerAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateCustomer + { + Company = "Book Lovers", + Email = "hank@booklovers.com", + Phone = "555 1245 8578", + Website = "https://www.booklovers.com" + }; + + var result = await customerService.UpdateCustomerAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerContactAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateCustomerContact + { + Name = "Sipho", + LastName = "Madlanga", + Phone = "0710857366", + Email = "sipho@madlanga.africa", + Type = ContactTypes.Business + }; + + var result = await customerService.UpdateCustomerContactAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerAddressAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateCustomerAddress + { + Name = "Business", + BuildingType = AddressBuildingTypes.MixedUse, + Type = AddressType.Shipping, + Street = "123 Building 4, XYZ Suburb, Some Region", + City = "Johannesburg", + State = "Gauteng", + Country = "South Africa", + IsPrimary = true, + Enabled = true, + PostalCode = "12346" + }; + + var result = await customerService.UpdateCustomerAddressAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await customerService.UpdateCustomerStatusAsync(1, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerContactStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await customerService.UpdateCustomerContactStatusAsync(1, true, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerAddressStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await customerService.UpdateCustomerAddressStatusAsync(1, true, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetCustomersAsync_ShouldReturn_ResultWithCustomerList() + { + var result = await customerService.GetCustomersAsync(fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerContactsAsync_ShouldReturn_ResultWithCustomerContactList() + { + var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerAddressesAsync_ShouldReturn_ResultWithCustomerAddressList() + { + var result = await customerService.GetCustomerAddressesAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerAsync_ShouldReturn_ResultWithCustomer() + { + var result = await customerService.GetCustomerAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerContactAsync_ShouldReturn_ResultWithCustomerContact() + { + var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddress() + { + var result = await customerService.GetCustomerAddressAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs new file mode 100644 index 0000000..e14e007 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs @@ -0,0 +1,167 @@ +using LiteCharms.Features.MidrandBooks.Orders; +using LiteCharms.Features.MidrandBooks.Orders.Models; +using LiteCharms.Features.MidrandBooks.Tests.Common; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class OrderServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly OrderService orderService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateOrderAsync_ShouldReturn_ResultWithOrderId() + { + var request = new CreateOrder(250, "At the intercomm, dial 1 then option 2"); + + var result = await orderService.CreateOrderAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task AddItemToOrderAsync_ShouldReturn_ResultWithOrderItemId() + { + var request = new CreateOrderItem(1, 1, 2); + + var result = await orderService.AddItemToOrderAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task AddItemsToOrderAsync_ShouldReturn_ResultWithSuccess() + { + var requests = new List + { + new(1, 1, 1), + new(1, 1, 3) + }; + + var result = await orderService.AddItemsToOrderAsync(1, [.. requests], fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task RemoveItemFromOrderAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.RemoveItemFromOrderAsync(1, 5, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task ClearOrderItemsAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.ClearOrderItemsAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CancelOrderAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.CancelOrderAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetOrderAsync_ShouldReturn_ResultWithOrder() + { + var result = await orderService.GetOrderAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetOrdersByCustomerAsync_ShouldReturn_ResultWithOrderList() + { + var result = await orderService.GetOrdersByCustomerAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetOrdersAsync_ShouldReturn_ResultWithOrderList() + { + var range = new DateRange + { + From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 1000 + }; + + var result = await orderService.GetOrdersAsync(range, 0, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task UpdateOrderStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.UpdateOrderStatusAsync(1, OrderStatus.Pending, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task AddShippingToOrderAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task UpdateShippingStatusAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task GetShippingByOrderIdAsync_ShouldReturn_ResultWithShipping() + { + + } + + [IntegrationFact] + public async Task RemoveShippingFromOrderAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task UpdateShippingTrackingNumberAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task CreateShippingProviderAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task GetShippingProvidersAsync_ShouldReturn_ResultWithShippingProviderList() + { + + } + + [IntegrationFact] + public async Task GetShippingProviderAsync_ShouldReturn_ResultWithShippingProvider() + { + + } + + [IntegrationFact] + public async Task UpdateShippingProviderAsync_ShouldReturn_ResultWithSuccess() + { + + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs new file mode 100644 index 0000000..a98c368 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs @@ -0,0 +1,63 @@ +using LiteCharms.Features.MidrandBooks.Pages; +using LiteCharms.Features.MidrandBooks.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class PageServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly PageService pageService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreatePageAsync_ShouldReturn_ResultWithPageId() + { + + } + + [IntegrationFact] + public async Task GetPagesAsync_ByBookId_ShouldReturn_ResultWithPageList() + { + + } + + [IntegrationFact] + public async Task GetPageAsync_ShouldReturn_ResultWithPage() + { + + } + + [IntegrationFact] + public async Task GetPageByNumberAsync_ById_And_BookPageNumber_ShouldReturn_ResultWithPage() + { + + } + + [IntegrationFact] + public async Task UpdatePageAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task DeletePageAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task DeleteByPageTypeAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task DeleteAllAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task UpdatePageStatusAsync_ShouldReturn_ResultWithSuccess() + { + + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs similarity index 96% rename from LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs rename to LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs index c498909..6dc7e88 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs @@ -1,10 +1,11 @@ using LiteCharms.Features.MidrandBooks.Products; using LiteCharms.Features.MidrandBooks.Products.Models; +using LiteCharms.Features.MidrandBooks.Tests.Common; using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Tests; -public class ProductServiceFeatureTest(Fixture fixture, ITestOutputHelper output) : IClassFixture +public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture { private readonly ProductService productService = fixture.Services.GetRequiredService(); diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index 81ff39f..4375d8e 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -7,23 +7,21 @@ namespace LiteCharms.Features.MidrandBooks.AuthorBooks; public sealed class BooksService(IDbContextFactory contextFactory) : IService { - public async ValueTask UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken) + public async ValueTask UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var book = await context.Books.FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); + var rowsUpdated = await context.Books + .Where(b => b.Id == bookId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(b => b.Enabled, isEnabled) + .SetProperty(b => b.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (book is null) - return Result.Fail(new Error($"Book with ID {bookId} not found")); - - book.UpdatedAt = DateTime.UtcNow; - book.Enabled = isEnabled; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error($"Failed to change status of book with ID {bookId}")); + : Result.Fail(new Error($"Book with ID {bookId} not found")); } catch (Exception ex) { @@ -45,8 +43,9 @@ public sealed class BooksService(IDbContextFactory contex var book = context.Books.Add(new Entities.AuthorBook { + CreatedAt = DateTime.UtcNow, AuthorId = authorId, - ProductId = productId, + ProductId = productId }); return await context.SaveChangesAsync(cancellationToken) > 0 @@ -68,7 +67,8 @@ public sealed class BooksService(IDbContextFactory contex var book = await context.Books .AsNoTracking() .Include(b => b.Author) - .Include(b => b.Product!.Price) + .Include(b => b.Product) + .ThenInclude(b => b!.Prices) .Include(b => b.Pages) .FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); @@ -86,7 +86,7 @@ public sealed class BooksService(IDbContextFactory contex { try { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken)) return Result.Fail(new Error($"Author with ID {authorId} not found")); @@ -94,7 +94,8 @@ public sealed class BooksService(IDbContextFactory contex var books = await context.Books .AsNoTracking() .Include(b => b.Author) - .Include(b => b.Product!.Price) + .Include(b => b.Product) + .ThenInclude(b => b.Prices) .OrderByDescending(b => b.CreatedAt) .Where(b => b.AuthorId == authorId) .ToListAsync(cancellationToken); @@ -118,9 +119,10 @@ public sealed class BooksService(IDbContextFactory contex var books = await context.Books .AsNoTracking() .Include(b => b.Author) - .Include(b => b.Product!.Price) + .Include(b => b.Product) + .ThenInclude(b => b!.Prices) .Include(b => b.Pages) - .Where(b => b.Enabled && b.Product!.Enabled && b.Author.Enabled) + .Where(b => b.Enabled && b.Product!.Enabled && b.Author!.Enabled) .OrderByDescending(b => b.Ranking) .ThenByDescending(b => b.Ranking) .ThenByDescending(b => b.CreatedAt) diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs index c1282e8..2b6b906 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs @@ -6,7 +6,7 @@ namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; public class AuthorBook : Models.AuthorBook { - public virtual Author Author { get; set; } = new(); + public virtual Author? Author { get; set; } public new virtual Product? Product { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs index 2f71d02..c1aa6b3 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -1,5 +1,4 @@ using LiteCharms.Features.MidrandBooks.Abstractions; -using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; @@ -9,53 +8,21 @@ namespace LiteCharms.Features.MidrandBooks.Authors; public sealed class AuthorService(IDbContextFactory contextFactory) : IService { - public async ValueTask> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) + public async ValueTask UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); + var rowsUpdated = await context.Authors + .Where(a => a.Id == authorId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(a => a.Enabled, isEnabled) + .SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (author is null) - return Result.Fail(new Error($"Author with ID {authorId} not found")); - - var books = await context.Books - .AsNoTracking() - .Include(b => b.Author) - .Include(b => b.Product!.Price) - .OrderByDescending(b => b.CreatedAt) - .Where(p => p.AuthorId == authorId) - .AsSplitQuery() - .ToArrayAsync(cancellationToken); - - return books?.Length > 0 - ? Result.Ok(books.Select(b => b.ToModel()).ToArray()) - : Result.Fail(new Error($"No books found for author with ID {authorId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } - - public async ValueTask UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken) - { - try - { - await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); - - if (author is null) - return Result.Fail(new Error($"Author with ID {authorId} not found")); - - author.UpdatedAt = DateTime.UtcNow; - author.Enabled = isEnabled; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error($"Failed to change status of author with ID {authorId}")); + : Result.Fail(new Error($"Author with ID {authorId} not found")); } catch (Exception ex) { @@ -81,12 +48,12 @@ public sealed class AuthorService(IDbContextFactory conte } } - public async ValueTask> GetAuthors(DateRange range, CancellationToken cancellationToken) + public async ValueTask> GetAuthorsAsync(DateRange range, CancellationToken cancellationToken = default) { try { - var fromDate = range.From.ToDateTime(TimeOnly.MinValue); - var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); @@ -107,18 +74,12 @@ public sealed class AuthorService(IDbContextFactory conte } } - public async ValueTask UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken) + public async ValueTask UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if (await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken)) - return Result.Fail(new Error($"An author with the name {request.Name} {request.LastName} already exists")); - - if (await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken)) - return Result.Fail(new Error($"An author with the email {request.Email} already exists")); - var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); if (author is null) @@ -151,12 +112,12 @@ public sealed class AuthorService(IDbContextFactory conte { try { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if(await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken)) + if (await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken)) return Result.Fail(new Error($"An author with the name {request.Name} {request.LastName} already exists")); - if(await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken)) + if (await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken)) return Result.Fail(new Error($"An author with the email {request.Email} already exists")); var newAuthor = context.Authors.Add(new Entities.Author diff --git a/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs b/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs index 0546a7b..c6080f1 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs @@ -30,7 +30,7 @@ public class Author public string? ThumbnailImageUrl { get; set; } - public SocialMedia[]? SocialMedia { get; set; } + public ICollection? SocialMedia { get; set; } public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs index d73292d..03f34c0 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs @@ -13,7 +13,7 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if (await context.Customers.AnyAsync(c => c.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken)) + if (await context.Customers.AnyAsync(c => EF.Functions.ILike(c.Email!, $"%{request.Email}%"), cancellationToken)) return Result.Fail(new Error($"Customer with email '{request.Email}' already exists.")); var customer = context.Customers.Add(new Entities.Customer @@ -46,7 +46,7 @@ public sealed class CustomerService(IDbContextFactory con if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); - if (await context.Contacts.AnyAsync(cc => cc.CustomerId == customerId && cc.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken)) + if (await context.Contacts.AnyAsync(cc => cc.CustomerId == customerId && EF.Functions.ILike(cc.Email!, $"%{request.Email}%"), cancellationToken)) return Result.Fail(new Error($"Contact with email '{request.Email}' already exists for this customer.")); var contact = context.Contacts.Add(new Entities.Contact @@ -139,21 +139,19 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken); + var rowsUpdated = await context.Contacts + .Where(cc => cc.Id == contactId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(cc => cc.Name, request.Name) + .SetProperty(cc => cc.LastName, request.LastName) + .SetProperty(cc => cc.Email, request.Email) + .SetProperty(cc => cc.Phone, request.Phone) + .SetProperty(cc => cc.Type, request.Type) + .SetProperty(cc => cc.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (contact is null) - return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); - - contact.UpdatedAt = DateTime.UtcNow; - contact.Name = request.Name; - contact.LastName = request.LastName; - contact.Email = request.Email; - contact.Phone = request.Phone; - contact.Type = request.Type; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error("Failed to update customer contact.")); + : Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); } catch (Exception ex) { @@ -167,25 +165,23 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); + var rowsUpdated = await context.Addresses + .Where(a => a.Id == addressId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(a => a.Street, request.Street) + .SetProperty(a => a.City, request.City) + .SetProperty(a => a.State, request.State) + .SetProperty(a => a.PostalCode, request.PostalCode) + .SetProperty(a => a.Country, request.Country) + .SetProperty(a => a.Type, request.Type) + .SetProperty(a => a.BuildingType, request.BuildingType) + .SetProperty(a => a.IsPrimary, request.IsPrimary) + .SetProperty(a => a.Name, request.Name) + .SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (address is null) - return Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); - - address.UpdatedAt = DateTime.UtcNow; - address.Street = request.Street; - address.City = request.City; - address.State = request.State; - address.PostalCode = request.PostalCode; - address.Country = request.Country; - address.Type = request.Type; - address.BuildingType = request.BuildingType; - address.IsPrimary = request.IsPrimary; - address.Name = request.Name; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error("Failed to update customer address.")); + : Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); } catch (Exception ex) { @@ -199,17 +195,15 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); + var rowsUpdated = await context.Customers + .Where(c => c.Id == customerId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(c => c.Enabled, enabled) + .SetProperty(c => c.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (customer is null) - return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); - - customer.Enabled = enabled; - customer.UpdatedAt = DateTime.UtcNow; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error("Failed to update customer status.")); + : Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); } catch (Exception ex) { @@ -223,18 +217,16 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken); + var rowsUpdated = await context.Contacts + .Where(cc => cc.Id == contactId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(cc => cc.Enabled, enabled) + .SetProperty(cc => cc.IsPrimary, isPrimary) + .SetProperty(cc => cc.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (contact is null) - return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); - - contact.Enabled = enabled; - contact.IsPrimary = isPrimary; - contact.UpdatedAt = DateTime.UtcNow; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error("Failed to update customer contact status.")); + : Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); } catch (Exception ex) { @@ -248,18 +240,16 @@ public sealed class CustomerService(IDbContextFactory con { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); + var rowsUpdated = await context.Addresses + .Where(a => a.Id == addressId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(a => a.Enabled, enabled) + .SetProperty(a => a.IsPrimary, isPrimary) + .SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (address is null) - return Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); - - address.Enabled = enabled; - address.IsPrimary = isPrimary; - address.UpdatedAt = DateTime.UtcNow; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error("Failed to update customer address status.")); + : Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); } catch (Exception ex) { diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs index 13204ce..9cf8f7e 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs @@ -20,7 +20,7 @@ public class Customer public string? Phone { get; set; } - public SocialMedia[]? SocialMedia { get; set; } + public ICollection? SocialMedia { get; set; } public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs index 10c8dae..0383187 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs @@ -14,7 +14,7 @@ public record CreateCustomer public string? Phone { get; set; } - public SocialMedia[]? SocialMedia { get; set; } + public ICollection? SocialMedia { get; set; } } public sealed record UpdateCustomer : CreateCustomer; diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs index f5453bc..c4c072a 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Orders.Models; -public sealed record CreateOrder(long CustomerId, decimal TotalPrice, string? Notes); +public sealed record CreateOrder(decimal TotalPrice, string? Notes); public sealed record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity); diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs index c15b2bf..84c7d86 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs @@ -49,6 +49,17 @@ public sealed class OrderService(IDbContextFactory contex if (!await context.Prices.AnyAsync(pp => pp.Id == request.ProductPriceId, cancellationToken)) return Result.Fail("Product price not found."); + var existingItem = await context.OrderItems.FirstOrDefaultAsync(i => i.ProductPriceId == request.ProductPriceId && i.OrderId == orderId, cancellationToken); + + if(existingItem is not null) + { + existingItem.Quantity += request.Quantity; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(existingItem.Id) + : Result.Fail("Update existing order item."); + } + var orderItem = context.OrderItems.Add(new Entities.OrderItem { OrderId = orderId, @@ -78,9 +89,6 @@ public sealed class OrderService(IDbContextFactory contex if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) return Result.Fail("Order not found."); - - var existingItems = context.OrderItems.Where(oi => oi.OrderId == orderId); - context.OrderItems.RemoveRange(existingItems); foreach (var item in items) { @@ -90,18 +98,23 @@ public sealed class OrderService(IDbContextFactory contex if (!await context.Prices.AnyAsync(pp => pp.Id == item.ProductPriceId, cancellationToken)) return Result.Fail($"Product price with ID {item.ProductPriceId} not found."); - context.OrderItems.Add(new Entities.OrderItem - { - OrderId = orderId, - AuthorBookId = item.AuthorBookId, - ProductPriceId = item.ProductPriceId, - Quantity = item.Quantity - }); + var existingItem = await context.OrderItems.FirstOrDefaultAsync(i => i.ProductPriceId == item.ProductPriceId && i.OrderId == orderId, cancellationToken); + + if (existingItem is not null) + existingItem.Quantity += item.Quantity; + else + context.OrderItems.Add(new Entities.OrderItem + { + OrderId = orderId, + AuthorBookId = item.AuthorBookId, + ProductPriceId = item.ProductPriceId, + Quantity = item.Quantity + }); + + await context.SaveChangesAsync(cancellationToken); } - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail("Failed to add items to order."); + return Result.Ok(); } catch (Exception ex) { @@ -115,16 +128,13 @@ public sealed class OrderService(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var orderItem = await context.OrderItems.FirstOrDefaultAsync(oi => oi.Id == orderItemId && oi.OrderId == orderId, cancellationToken); + var rowsDeleted = await context.OrderItems + .Where(oi => oi.Id == orderItemId && oi.OrderId == orderId) + .ExecuteDeleteAsync(cancellationToken); - if (orderItem is null) - return Result.Fail("Order item not found."); - - context.OrderItems.Remove(orderItem); - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsDeleted > 0 ? Result.Ok() - : Result.Fail("Failed to remove item from order."); + : Result.Fail("Order item not found or failed to remove."); } catch (Exception ex) { @@ -132,15 +142,14 @@ public sealed class OrderService(IDbContextFactory contex } } - public async ValueTask ClearOrderItemasAsync(long orderId, CancellationToken cancellationToken = default) + public async ValueTask ClearOrderItemsAsync(long orderId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var orderItems = context.OrderItems.Where(oi => oi.OrderId == orderId); - - context.OrderItems.RemoveRange(orderItems); + var deletedItems = await context.OrderItems.Where(oi => oi.OrderId == orderId) + .ExecuteDeleteAsync(cancellationToken); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() @@ -199,12 +208,15 @@ public sealed class OrderService(IDbContextFactory contex { try { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var orders = await context.Orders .AsNoTracking() - .Where(o => o.CreatedAt >= range.From.ToDateTime(TimeOnly.MinValue) && o.CreatedAt <= range.To.ToDateTime(TimeOnly.MaxValue)) - .Skip(index * range.MaxRecords) + .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) + .Skip(index).Take(range.MaxRecords) .ToListAsync(cancellationToken); return Result.Ok(orders.Select(o => o.ToModel()).ToArray()); @@ -221,17 +233,15 @@ public sealed class OrderService(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); + var rowsUpdated = await context.Orders + .Where(o => o.Id == orderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(o => o.Status, newStatus) + .SetProperty(o => o.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (order is null) - return Result.Fail("Order not found."); - - order.UpdatedAt = DateTime.UtcNow; - order.Status = newStatus; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update order status."); + : Result.Fail("Order not found or status update failed."); } catch (Exception ex) { @@ -282,17 +292,16 @@ public sealed class OrderService(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId, cancellationToken); + var rowsUpdated = await context.Shippings + .Where(s => s.OrderId == orderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(s => s.Status, newStatus) + .SetProperty(s => s.UpdatedAt, DateTime.UtcNow), + cancellationToken); - if (shipping is null) - return Result.Fail("Shipping not found for this order."); - - shipping.UpdatedAt = DateTime.UtcNow; - shipping.Status = newStatus; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update shipping status."); + : Result.Fail("Shipping not found for this order or status update failed."); } catch (Exception ex) { @@ -325,21 +334,14 @@ public sealed class OrderService(IDbContextFactory contex try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.Shippings + .Where(s => s.Id == shippingId && s.OrderId == orderId) + .ExecuteDeleteAsync(cancellationToken); - if(!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) - return Result.Fail("Order not found."); - - var shipping = await context.Shippings.AsNoTracking() - .FirstOrDefaultAsync(s => s.OrderId == orderId && s.Id == shippingId, cancellationToken); - - if (shipping is null) - return Result.Fail("Shipping not found for this order."); - - context.Shippings.Remove(shipping); - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsDeleted > 0 ? Result.Ok() - : Result.Fail("Failed to remove shipping from order."); + : Result.Fail("Shipping record not found for this order."); } catch (Exception ex) { @@ -353,17 +355,15 @@ public sealed class OrderService(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId && s.Id == shippingId, cancellationToken); + var rowsUpdated = await context.Shippings + .Where(s => s.Id == shippingId && s.OrderId == orderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(s => s.TrackingNumber, trackingNumber) + .SetProperty(s => s.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (shipping is null) - return Result.Fail("Shipping not found for this order."); - - shipping.UpdatedAt = DateTime.UtcNow; - shipping.TrackingNumber = trackingNumber; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update shipping tracking number."); + : Result.Fail("Shipping record not found for this order."); } catch (Exception ex) { @@ -440,24 +440,22 @@ public sealed class OrderService(IDbContextFactory contex { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var provider = await context.ShippingProviders.FirstOrDefaultAsync(sp => sp.Id == request.ProviderId, cancellationToken); + var rowsUpdated = await context.ShippingProviders + .Where(sp => sp.Id == request.ProviderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(sp => sp.Name, request.Name) + .SetProperty(sp => sp.Price, request.Price) + .SetProperty(sp => sp.TrackingUrl, request.TrackingUrl) + .SetProperty(sp => sp.Enabled, request.Enabled) + .SetProperty(sp => sp.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (provider is null) - return Result.Fail("Shipping provider not found."); - - provider.UpdatedAt = DateTime.UtcNow; - provider.Enabled = request.Enabled; - provider.Name = request.Name; - provider.Price = request.Price; - provider.TrackingUrl = request.TrackingUrl; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update shipping provider status."); + : Result.Fail("Shipping provider not found."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } - } + } } diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs index d95cfa1..c9d082d 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs @@ -22,7 +22,7 @@ public class BookPage public string[]? Notes { get; set; } - public PageReference[]? References { get; set; } + public ICollection? References { get; set; } public bool Enabled { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs index d4af2e7..5c691c9 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs @@ -14,5 +14,5 @@ public class CreateBookPage public string[]? Notes { get; set; } - public PageReference[]? References { get; set; } + public ICollection? References { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs index 80f3a5d..26569bc 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs @@ -13,19 +13,13 @@ public sealed class PageService(IDbContextFactory context { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken)) - return Result.Fail("Book not found"); + var rowsDeleted = await context.Pages + .Where(p => p.AuthorBookId == authorBookId) + .ExecuteDeleteAsync(cancellationToken); - var pages = await context.Pages.Where(p => p.AuthorBookId == authorBookId).ToListAsync(cancellationToken); - - if (pages.Count == 0) - return Result.Fail("No pages found for the specified book"); - - context.Pages.RemoveRange(pages); - - await context.SaveChangesAsync(cancellationToken); - - return Result.Ok(); + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("No pages found for the specified book"); } catch (Exception ex) { @@ -39,16 +33,13 @@ public sealed class PageService(IDbContextFactory context { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var page = await context.Pages.FirstOrDefaultAsync(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType, cancellationToken); + var rowsDeleted = await context.Pages + .Where(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType) + .ExecuteDeleteAsync(cancellationToken); - if (page is null) - return Result.Fail("Page not found"); - - context.Pages.Remove(page); - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsDeleted > 0 ? Result.Ok() - : Result.Fail("Failed to delete page"); + : Result.Fail("Page not found"); } catch (Exception ex) { @@ -62,17 +53,15 @@ public sealed class PageService(IDbContextFactory context { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); + var rowsUpdated = await context.Pages + .Where(p => p.Id == bookPageId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Enabled, enabled) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (page is null) - return Result.Fail("Page not found"); - - page.UpdatedAt = DateTime.UtcNow; - page.Enabled = enabled; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update page status"); + : Result.Fail("Page not found"); } catch (Exception ex) { @@ -86,16 +75,13 @@ public sealed class PageService(IDbContextFactory context { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); + var rowsDeleted = await context.Pages + .Where(p => p.Id == bookPageId) + .ExecuteDeleteAsync(cancellationToken); - if (page is null) - return Result.Fail("Page not found"); - - context.Pages.Remove(page); - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsDeleted > 0 ? Result.Ok() - : Result.Fail("Failed to delete page"); + : Result.Fail("Page not found"); } catch (Exception ex) { @@ -109,22 +95,20 @@ public sealed class PageService(IDbContextFactory context { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); + var rowsUpdated = await context.Pages + .Where(p => p.Id == bookPageId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Type, request.Type) + .SetProperty(p => p.ContentType, request.ContentType) + .SetProperty(p => p.Number, request.Number) + .SetProperty(p => p.Content, request.Content) + .SetProperty(p => p.Notes, request.Notes) + .SetProperty(p => p.References, request.References) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (page is null) - return Result.Fail("Page not found"); - - page.UpdatedAt = DateTime.UtcNow; - page.Type = request.Type; - page.ContentType = request.ContentType; - page.Number = request.Number; - page.Content = request.Content; - page.Notes = request.Notes; - page.References = request.References; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail("Failed to update page"); + : Result.Fail("Page not found"); } catch (Exception ex) { diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 6340386..634218e 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -57,7 +57,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.HasIndex("ProductId"); - b.ToTable("Books"); + b.ToTable("Books", (string)null); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => @@ -454,7 +454,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.HasKey("Id"); - b.ToTable("ShippingProviders"); + b.ToTable("ShippingProviders", (string)null); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => @@ -677,7 +677,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.HasIndex("ProductId") .IsUnique(); - b.ToTable("ProductPrice"); + b.ToTable("ProductPrice", (string)null); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => @@ -718,7 +718,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("AuthorId", "__synthesizedOrdinal"); - b1.ToTable("Authors"); + b1.ToTable("Authors", (string)null); b1 .ToJson("SocialMedia") @@ -772,7 +772,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("CustomerId", "__synthesizedOrdinal"); - b1.ToTable("Customers"); + b1.ToTable("Customers", (string)null); b1 .ToJson("SocialMedia") @@ -862,7 +862,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("BookPageId", "__synthesizedOrdinal"); - b1.ToTable("BookPages"); + b1.ToTable("BookPages", (string)null); b1 .ToJson("References") @@ -904,7 +904,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("ProductId"); - b1.ToTable("Products"); + b1.ToTable("Products", (string)null); b1 .ToJson("Metadata") diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index aef7710..4d1b0ab 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -14,17 +14,15 @@ public sealed class ProductService(IDbContextFactory cont { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var productPrice = await context.Prices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + var rowsUpdated = await context.Prices + .Where(p => p.Id == productPriceId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Enabled, isEnabled) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (productPrice is null) - return Result.Fail(new Error($"Product price with ID {productPriceId} not found")); - - productPrice.UpdatedAt = DateTime.UtcNow; - productPrice.Enabled = isEnabled; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error($"Failed to change status of product price with ID {productPriceId}")); + : Result.Fail(new Error($"Product price with ID {productPriceId} not found")); } catch (Exception ex) { @@ -38,17 +36,15 @@ public sealed class ProductService(IDbContextFactory cont { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + var rowsUpdated = await context.Products + .Where(p => p.Id == productId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Enabled, isEnabled) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); - if (product is null) - return Result.Fail(new Error($"Product with ID {productId} not found")); - - product.UpdatedAt = DateTime.UtcNow; - product.Enabled = isEnabled; - - return await context.SaveChangesAsync(cancellationToken) > 0 + return rowsUpdated > 0 ? Result.Ok() - : Result.Fail(new Error($"Failed to change status of product with ID {productId}")); + : Result.Fail(new Error($"Product with ID {productId} not found")); } catch (Exception ex) { From 4397976ed83301c88b4e8e3c5a73766806ebb1e5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 29 May 2026 08:18:29 +0200 Subject: [PATCH 062/153] Implemented order service tests --- .../OrderServiceFeatureTests.cs | 31 ++++++++++++++++++- .../Extensions/Mappers.cs | 3 +- .../Orders/Models/Records.cs | 2 +- .../Orders/OrderService.cs | 7 +++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs index e14e007..ef09044 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs @@ -114,54 +114,83 @@ public class OrderServiceFeatureTests(Fixture fixture) : IClassFixture [IntegrationFact] public async Task AddShippingToOrderAsync_ShouldReturn_ResultWithSuccess() { + var request = new CreateShipping(1, 2); + var result = await orderService.AddShippingToOrderAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); } [IntegrationFact] public async Task UpdateShippingStatusAsync_ShouldReturn_ResultWithSuccess() { + var result = await orderService.UpdateShippingStatusAsync(1, ShippingStatuses.Shipped, fixture.CancellationToken); + Assert.True(result.IsSuccess); } [IntegrationFact] public async Task GetShippingByOrderIdAsync_ShouldReturn_ResultWithShipping() { + var result = await orderService.GetShippingByOrderIdAsync(1, fixture.CancellationToken); + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); } [IntegrationFact] public async Task RemoveShippingFromOrderAsync_ShouldReturn_ResultWithSuccess() { + var result = await orderService.RemoveShippingFromOrderAsync(1, 1, fixture.CancellationToken); + Assert.True(result.IsSuccess); } [IntegrationFact] public async Task UpdateShippingTrackingNumberAsync_ShouldReturn_ResultWithSuccess() { + var result = await orderService.UpdateShippingTrackingNumberAsync(1, 2, "NA0009969397"); + Assert.True(result.IsSuccess); } [IntegrationFact] - public async Task CreateShippingProviderAsync_ShouldReturn_ResultWithSuccess() + public async Task CreateShippingProviderAsync_ShouldReturn_ResultWithShippingProviderId() { + var request = new CreateShippingProvider(ShippingProviderTypes.FastWay, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel"); + var result = await orderService.CreateShippingProviderAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); } [IntegrationFact] public async Task GetShippingProvidersAsync_ShouldReturn_ResultWithShippingProviderList() { + var result = await orderService.GetShippingProvidersAsync(true, fixture.CancellationToken); + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); } [IntegrationFact] public async Task GetShippingProviderAsync_ShouldReturn_ResultWithShippingProvider() { + var result = await orderService.GetShippingProviderAsync(2, fixture.CancellationToken); + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); } [IntegrationFact] public async Task UpdateShippingProviderAsync_ShouldReturn_ResultWithSuccess() { + var request = new UpdateShippingProvider(2,true, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel"); + var result = await orderService.UpdateShippingProviderAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); } } diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 72e57d5..753bcc7 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -17,7 +17,8 @@ public static class Mappers Name = entity.Name, Type = entity.Type, Price = entity.Price, - Enabled = entity.Enabled + Enabled = entity.Enabled, + TrackingUrl = entity.TrackingUrl, }; public static Shipping ToModel(this Orders.Entities.Shipping entity) => new() diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs index c4c072a..6ffccb4 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs @@ -4,7 +4,7 @@ public sealed record CreateOrder(decimal TotalPrice, string? Notes); public sealed record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity); -public sealed record CreateShipping(long OrderId, long AddressId, long ShippingProviderId, string? TrackingNumber); +public sealed record CreateShipping(long AddressId, long ShippingProviderId, string? TrackingNumber = null); public sealed record CreateShippingProvider(ShippingProviderTypes Type, string Name, decimal Price, string TrackingUrl); diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs index 84c7d86..290908b 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs @@ -249,7 +249,7 @@ public sealed class OrderService(IDbContextFactory contex } } - public async ValueTask AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default) + public async ValueTask> AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default) { try { @@ -371,7 +371,7 @@ public sealed class OrderService(IDbContextFactory contex } } - public async ValueTask CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default) + public async ValueTask> CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default) { try { @@ -382,6 +382,7 @@ public sealed class OrderService(IDbContextFactory contex var shippingProvider = context.ShippingProviders.Add(new Entities.ShippingProvider { + CreatedAt = DateTime.UtcNow, Name = request.Name, Type = request.Type, Price = request.Price, @@ -389,7 +390,7 @@ public sealed class OrderService(IDbContextFactory contex }); return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() + ? Result.Ok(shippingProvider.Entity.Id) : Result.Fail("Failed to create shipping provider."); } catch (Exception ex) From 9eb3526a2eb1178e45fd1dd99aa7d3416603d04b Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 29 May 2026 08:21:11 +0200 Subject: [PATCH 063/153] Moved Book Page DTOs --- .../Pages/Models/{CreateBookPage.cs => Records.cs} | 2 ++ .../Pages/Models/UpdateBookPage.cs | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) rename LiteCharms.Features.MidrandBooks/Pages/Models/{CreateBookPage.cs => Records.cs} (88%) delete mode 100644 LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/Records.cs similarity index 88% rename from LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs rename to LiteCharms.Features.MidrandBooks/Pages/Models/Records.cs index 5c691c9..5af145a 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Models/CreateBookPage.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/Records.cs @@ -16,3 +16,5 @@ public class CreateBookPage public ICollection? References { get; set; } } + +public sealed class UpdateBookPage : CreateBookPage; \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs deleted file mode 100644 index 7016067..0000000 --- a/LiteCharms.Features.MidrandBooks/Pages/Models/UpdateBookPage.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace LiteCharms.Features.MidrandBooks.Pages.Models; - -public sealed class UpdateBookPage : CreateBookPage; From 38e765203d6a6e435e4e80368ee3dbb6c4098d2a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 29 May 2026 09:02:47 +0200 Subject: [PATCH 064/153] Refactored migrations --- .../LiteCharms.Features.MidrandBooks.csproj | 3 - ...ner.cs => 20260529070104_Init.Designer.cs} | 47 +-------------- ...8052014_Init.cs => 20260529070104_Init.cs} | 33 ----------- .../MidrandBooksDbContextModelSnapshot.cs | 57 ++----------------- .../Products/Entities/ProductConfiguration.cs | 2 + 5 files changed, 9 insertions(+), 133 deletions(-) rename LiteCharms.Features.MidrandBooks/Postgres/Migrations/{20260528052014_Init.Designer.cs => 20260529070104_Init.Designer.cs} (95%) rename LiteCharms.Features.MidrandBooks/Postgres/Migrations/{20260528052014_Init.cs => 20260529070104_Init.cs} (93%) diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 82d3a1b..17db6dd 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -162,8 +162,5 @@ - - - diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.Designer.cs similarity index 95% rename from LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs rename to LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.Designer.cs index ed24e21..42297c0 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations { [DbContext(typeof(MidrandBooksDbContext))] - [Migration("20260528052014_Init")] + [Migration("20260529070104_Init")] partial class Init { /// @@ -649,40 +649,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.ToTable("Prices", (string)null); }); - modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Amount") - .HasColumnType("numeric"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasColumnType("numeric"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("ProductId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId") - .IsUnique(); - - b.ToTable("ProductPrice"); - }); - modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") @@ -931,15 +897,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Product"); }); - modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => - { - b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", null) - .WithOne("Price") - .HasForeignKey("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", "ProductId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => { b.Navigation("Pages"); @@ -973,8 +930,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => { - b.Navigation("Price"); - b.Navigation("Prices"); }); #pragma warning restore 612, 618 diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.cs similarity index 93% rename from LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs rename to LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.cs index 1c0cb96..d960313 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.cs @@ -259,30 +259,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations onDelete: ReferentialAction.Restrict); }); - //migrationBuilder.CreateTable( - // name: "ProductPrice", - // columns: table => new - // { - // Id = table.Column(type: "bigint", nullable: false) - // .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - // ProductId = table.Column(type: "bigint", nullable: false), - // Amount = table.Column(type: "numeric", nullable: false), - // Discount = table.Column(type: "numeric", nullable: false), - // Enabled = table.Column(type: "boolean", nullable: false) - // }, - // constraints: table => - // { - // table.PrimaryKey("PK_ProductPrice", x => x.Id); - // table.ForeignKey( - // name: "FK_ProductPrice_Products_ProductId", - // column: x => x.ProductId, - // principalTable: "Products", - // principalColumn: "Id", - // onDelete: ReferentialAction.Cascade); - // }); - migrationBuilder.CreateTable( name: "Shippings", columns: table => new @@ -427,12 +403,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations table: "Prices", column: "ProductId"); - migrationBuilder.CreateIndex( - name: "IX_ProductPrice_ProductId", - table: "ProductPrice", - column: "ProductId", - unique: true); - migrationBuilder.CreateIndex( name: "IX_Refunds_OrderId", table: "Refunds", @@ -467,9 +437,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations migrationBuilder.DropTable( name: "OrderItems"); - migrationBuilder.DropTable( - name: "ProductPrice"); - migrationBuilder.DropTable( name: "Refunds"); diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 634218e..6045ed4 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -57,7 +57,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.HasIndex("ProductId"); - b.ToTable("Books", (string)null); + b.ToTable("Books"); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => @@ -454,7 +454,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.HasKey("Id"); - b.ToTable("ShippingProviders", (string)null); + b.ToTable("ShippingProviders"); }); modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => @@ -646,40 +646,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.ToTable("Prices", (string)null); }); - modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Amount") - .HasColumnType("numeric"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasColumnType("numeric"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("ProductId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId") - .IsUnique(); - - b.ToTable("ProductPrice", (string)null); - }); - modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") @@ -718,7 +684,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("AuthorId", "__synthesizedOrdinal"); - b1.ToTable("Authors", (string)null); + b1.ToTable("Authors"); b1 .ToJson("SocialMedia") @@ -772,7 +738,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("CustomerId", "__synthesizedOrdinal"); - b1.ToTable("Customers", (string)null); + b1.ToTable("Customers"); b1 .ToJson("SocialMedia") @@ -862,7 +828,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("BookPageId", "__synthesizedOrdinal"); - b1.ToTable("BookPages", (string)null); + b1.ToTable("BookPages"); b1 .ToJson("References") @@ -904,7 +870,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b1.HasKey("ProductId"); - b1.ToTable("Products", (string)null); + b1.ToTable("Products"); b1 .ToJson("Metadata") @@ -928,15 +894,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Product"); }); - modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b => - { - b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", null) - .WithOne("Price") - .HasForeignKey("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", "ProductId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => { b.Navigation("Pages"); @@ -970,8 +927,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => { - b.Navigation("Price"); - b.Navigation("Prices"); }); #pragma warning restore 612, 618 diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs index db18548..795097d 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs @@ -4,6 +4,8 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { + builder.Ignore(p => p.Price); + builder.ToTable("Products"); builder.HasKey(f => f.Id); From 60fcc70e9831ab5b41a2dd7f151a36fc37bda7d7 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 29 May 2026 18:56:08 +0200 Subject: [PATCH 065/153] Implemented Product Data Seeder --- .../Configuration/CdnSettings.cs | 8 + ...teCharms.Features.MidrandBooks.Seed.csproj | 158 +++++++++++++++ .../ProductsSeederService.cs | 190 ++++++++++++++++++ .../Program.cs | 21 ++ .../appsettings.json | 28 +++ LiteCharms.Features/Enums.cs | 3 +- LiteCharmsShared.slnx | 1 + 7 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs create mode 100644 LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj create mode 100644 LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs create mode 100644 LiteCharms.Features.MidrandBooks.Seed/Program.cs create mode 100644 LiteCharms.Features.MidrandBooks.Seed/appsettings.json diff --git a/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs new file mode 100644 index 0000000..af16232 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.MidrandBooks.Seed.Configuration; + +public class CdnSettings +{ + public string? BaseCdn { get; set; } + + public string[]? BookCovers { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj new file mode 100644 index 0000000..14e2ffd --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj @@ -0,0 +1,158 @@ + + + + Exe + net10.0 + enable + enable + 5c3bc894-8654-4691-99e8-f90d3414843f + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs new file mode 100644 index 0000000..a506bf3 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs @@ -0,0 +1,190 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks; +using LiteCharms.Features.MidrandBooks.Authors; +using LiteCharms.Features.MidrandBooks.Products; +using LiteCharms.Features.MidrandBooks.Seed.Configuration; + +namespace LiteCharms.Features.MidrandBooks.Seed; + +public class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService, + IOptions options, ILogger logger) : BackgroundService +{ + private readonly CdnSettings cdnSettings = options.Value; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation("Product Seeding started"); + + if (cdnSettings.BookCovers is null || cdnSettings.BookCovers.Length == 0) + { + logger.LogWarning("No book covers found in CDN settings. Seeding aborted."); + return; + } + + // Initialize Bogus Faker engine + var faker = new Faker(); + var culture = CultureInfo.InvariantCulture; + + // Ensure repeatable data sets if run multiple times by anchoring the seed + Randomizer.Seed = new Random(42); + + foreach (var bookCover in cdnSettings.BookCovers) + { + if (stoppingToken.IsCancellationRequested) break; + + // Generate beautifully mixed eclectic topics on the fly + var bookTopic = faker.PickRandom( + // --- Tech & IT --- + "C# 12 & Modern .NET Architecture", + "PostgreSQL Database Optimization", + "Docker & Kubernetes in Production", + "Domain-Driven Design Paradigms", + "Artificial Intelligence with Python", + + // --- Sci-Fi & Fantasy --- + "The Chronicles of the Quantum Nebula", + "Legends of the Lost Cybernetic Kingdom", + "Parallel Dimensions and Rogue Time Streams", + "The Last Android in Neo-Johannesburg", + + // --- Thrillers, Mystery & Crime --- + "The Midnight Code Cryptograph", + "Shadows in the Highveld", + "The Silent Witness of Midrand", + "Deception on the 14th Floor", + + // --- Business, Finance & Wealth --- + "Mastering the South African Tech Market", + "The Modern Entrepreneur's Blueprint", + "Generational Wealth and Venture Capital", + "Negotiation Tactics for High-Stakes Deals", + + // --- Self-Help & Personal Growth --- + "The Art of Relentless Focus", + "Building High-Performance Habits", + "The Mindfulness Guide for Software Engineers", + "Unlocking Creative Flow Under Pressure" + ); + + // Defensive Length Processing to avoid Entity Framework / Postgres string truncation crashes + var rawTitle = $"{faker.Company.CatchPhrase()} with {bookTopic}"; + var bookTitle = rawTitle.Length > 255 ? rawTitle[..252] + "..." : rawTitle; + + var rawSummary = $"A comprehensive guide to mastering {bookTopic}. Learn modern implementation techniques through real-world software engineering paradigms."; + var bookSummary = rawSummary.Length > 512 ? rawSummary[..509] + "..." : rawSummary; + + // Generating a single concise paragraph ensures a rich text description falling safely well under 1024 + var rawDescription = faker.Lorem.Paragraph(3); + var bookDescription = rawDescription.Length > 1024 ? rawDescription[..1021] + "..." : rawDescription; + + var authorFirstName = faker.Name.FirstName(); + var authorLastName = faker.Name.LastName(); + var publisherCompany = faker.Company.CompanyName(); + + // Step 1: Add Product + var productCreateResult = await productService.CreateProductAsync(new Products.Models.CreateProduct + { + Name = bookTitle, + Summary = bookSummary, + Description = bookDescription, + ImageUrl = $"{cdnSettings.BaseCdn}{bookCover}", + Type = ProductTypes.Book, + Metadata = new Models.ProductMetadata + { + CopyrightInfo = $"© {DateTime.UtcNow.Year} {publisherCompany}. All rights reserved.", + ManufactureDate = faker.Date.Past(3).ToString("yyyy-MM-dd", culture), + Manufacturer = $"{authorFirstName} {authorLastName} / {publisherCompany}", + SerialNumber = faker.Phone.PhoneNumber("978-##########") + }, + Categories = ["Coding", "Computers", "IT"] + }, stoppingToken); + + if (productCreateResult.IsFailed) + { + logger.LogError("Failed to create product: {Error}", productCreateResult.Errors[0].Message); + break; + } + + // Step 2: Enable product so it can show on the shop + var enableProductResult = await productService.UpdateProductStatusAsync(productId: productCreateResult.Value, isEnabled: true, stoppingToken); + + if (enableProductResult.IsFailed) + { + logger.LogError("Failed to enable created product: {Error}", enableProductResult.Errors[0].Message); + break; + } + + // Step 3: Create Product Price + var productPriceCreateResult = await productService.CreateProductPriceAsync(productId: productCreateResult.Value, request: new Products.Models.CreateProductPrice + { + // Generates fair, dynamic prices in Rands between R150 and R650, snapped neatly to integers + Amount = Math.Round(faker.Random.Decimal(150m, 650m), 2), + Discount = 0.0m + }, stoppingToken); + + if (productPriceCreateResult.IsFailed) + { + logger.LogError("Failed to create product price: {Error}", productPriceCreateResult.Errors[0].Message); + break; + } + + // Step 4: Create Author + var authorCreateResult = await authorService.CreateAuthorAsync(request: new Authors.Models.CreateAuthor + { + Name = authorFirstName, + LastName = authorLastName, + Company = publisherCompany, + VatNumber = faker.Random.Bool() ? faker.Phone.PhoneNumber("4#########") : "", + PublisherType = faker.PickRandom(), + Email = faker.Internet.Email(authorFirstName, authorLastName), + Website = faker.Internet.Url(), + ImageUrl = faker.Internet.Avatar(), + SocialMedia = + [ + new Models.SocialMedia + { + Name = "LinkedIn", + ImageUrl = "https://cdn.example.com/icons/linkedin.png", + Type = SocialMediaTypes.LinkedIn, + Url = $"https://linkedin.com/in/{authorFirstName.ToLower(culture)}-{authorLastName.ToLower(culture)}" + }, + new Models.SocialMedia + { + Name = "GitHub", + ImageUrl = "https://cdn.example.com/icons/github.png", + Type = SocialMediaTypes.GitHub, + Url = $"https://github.com/tech-{authorFirstName.ToLower(culture)}" + } + ], + Biography = $"{authorFirstName} {authorLastName} is a veteran technologist and systems architect with over a decade of domain expertise. " + faker.Lorem.Paragraph(2), + ThumbnailImageUrl = null + }, stoppingToken); + + if (authorCreateResult.IsFailed) + { + logger.LogError("Failed to create author: {Error}", authorCreateResult.Errors[0].Message); + break; + } + + // Step 5: Create Author-Book link (product linkage) + var authorBookCreateResult = await booksService.CreateBookAsync(authorId: authorCreateResult.Value, productId: productCreateResult.Value, stoppingToken); + + if (authorBookCreateResult.IsFailed) + { + logger.LogError("Failed to create author-book linkage: {Error}", authorBookCreateResult.Errors[0].Message); + break; + } + + var enableAuthorBookResult = await booksService.UpdateBookStatusAsync(bookId: authorBookCreateResult.Value, isEnabled: true, stoppingToken); + + if (enableAuthorBookResult.IsFailed) + { + logger.LogError("Failed to enable author-book link: {Error}", enableAuthorBookResult.Errors[0].Message); + break; + } + + logger.LogInformation("Successfully seeded book product: {Title}", bookTitle); + } + + logger.LogInformation("Product Seeding completed successfully."); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Seed/Program.cs b/LiteCharms.Features.MidrandBooks.Seed/Program.cs new file mode 100644 index 0000000..f31cdd3 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/Program.cs @@ -0,0 +1,21 @@ +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Seed; +using LiteCharms.Features.MidrandBooks.Seed.Configuration; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Configuration + .AddJsonFile("appsettings.json") + .AddUserSecrets(typeof(Program).Assembly); + +builder.Services + .AddLogging() + .AddShopServices() + .AddHostedService() + .AddMidrandShopDatabase(builder.Configuration); + +builder.Services.Configure(options => builder.Configuration.GetSection(nameof(CdnSettings)).Bind(options)); + +using var host = builder.Build(); + +await host.RunAsync(); \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json new file mode 100644 index 0000000..1dc7baa --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json @@ -0,0 +1,28 @@ +{ + "CdnSettings": { + "BaseCdn": "https://bookshop.cdn.khongisa.co.za/design/", + "BookCovers": [ + "144e0314-0bd8-4e2c-8814-2b34608c0600_1764780116467.webp", + "2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp", + "3cd172b9-416e-4b3a-b613-7ff0a3aecc4c_1764780129459.webp", + "762947b6-63ac-436e-98fb-91dd94de1d82_1764780126141.webp", + "7b42f23f-666c-4dd9-82e1-9b928e1b4db7_1764780186376.webp", + "8e281eea-8910-473d-b650-43f539995d9e_1764780152316.webp", + "91d18e2c-6ee6-44b4-84a5-8692db5dbfe4_1764780114484.webp", + "94fdf403-4d10-4cb4-a537-fc914a1f5ba8_1764780198423.webp", + "9b881786-75e1-47f7-9bc9-27188d46d1ec_1764780122458.webp", + "c417236d-a628-45eb-82c2-ec1cb0326e4f_1765006317965.webp", + "clpqjsgi71jpr1i6392e449l2.webp", + "clps1mk9f0m1z1i32grvq17tg.webp", + "clq550xxy2dab1ywb6mdv4acv.webp", + "clq5535o52dj51yw557ml49c1.webp", + "clq55455w2dcm1yvd8d8258d8.webp", + "clq558fkh2djy1ywbghwcawnh.webp", + "clq55cvqh2dqi1yyratl123wq.webp", + "clrhnk94e115k1y00bg8h9ixb.webp", + "d44a3c04-f124-4f0b-8301-3841ae2fd439_1764780121224.webp", + "e6ba52f208914285bcdf1966cfb08f6f.jpg", + "fa9cbbe6-f947-4f83-8e98-61d2661f43e0_1764841636705.webp" + ] + } +} diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs index 9cff19c..40aabdc 100644 --- a/LiteCharms.Features/Enums.cs +++ b/LiteCharms.Features/Enums.cs @@ -93,7 +93,8 @@ public enum SocialMediaTypes : int YouTube = 5, Pinterest = 6, Reddit = 7, - Tumblr = 8 + Tumblr = 8, + GitHub = 9 } public enum EmailStatuses : int diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index 1945799..be7cb5c 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -9,6 +9,7 @@ + From 50eee03dbec0a60ad47d732841d4988dbe4a52b3 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 29 May 2026 23:02:06 +0200 Subject: [PATCH 066/153] Product seeding completed --- .../Configuration/CdnSettings.cs | 6 + .../ProductsSeederService.cs | 68 ++++- .../appsettings.json | 237 +++++++++++++++++- 3 files changed, 302 insertions(+), 9 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs index af16232..6eb1107 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs @@ -5,4 +5,10 @@ public class CdnSettings public string? BaseCdn { get; set; } public string[]? BookCovers { get; set; } + + public string[]? Authors { get; set; } + + public string[]? AuthorThumbnails { get; set; } + + public string[]? BookThumbnails { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs index a506bf3..9dfc582 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs @@ -65,8 +65,19 @@ public class ProductsSeederService(ProductService productService, AuthorService "Unlocking Creative Flow Under Pressure" ); - // Defensive Length Processing to avoid Entity Framework / Postgres string truncation crashes - var rawTitle = $"{faker.Company.CatchPhrase()} with {bookTopic}"; + // Dynamic raw title generation formulas executed via random function picker + var titlePatterns = new Func[] + { + () => $"{faker.Company.CatchPhrase()} with {bookTopic}", + () => $"The {faker.Commerce.ProductAdjective()} Guide to {bookTopic}", + () => $"Mastering {bookTopic}: A {faker.Company.Bs()} Blueprint", + () => $"{bookTopic} for the Modern {faker.Name.JobTitle()}", + () => $"Advanced {bookTopic}: Demystifying the {faker.Company.CatchPhrase()}", + () => $"{faker.Random.Replace("###")} Blueprints for {bookTopic}" + }; + + // Pick a format template and resolve it down to raw string text + var rawTitle = faker.PickRandom(titlePatterns)(); var bookTitle = rawTitle.Length > 255 ? rawTitle[..252] + "..." : rawTitle; var rawSummary = $"A comprehensive guide to mastering {bookTopic}. Learn modern implementation techniques through real-world software engineering paradigms."; @@ -80,6 +91,21 @@ public class ProductsSeederService(ProductService productService, AuthorService var authorLastName = faker.Name.LastName(); var publisherCompany = faker.Company.CompanyName(); + // Safe bounded random picking for book thumbnails + string? pickedBookThumbnail = null; + string? pickedBookThumbnail1 = null; + string? pickedBookThumbnail2 = null; + string? pickedBookThumbnail3 = null; + string? pickedBookThumbnail4 = null; + if (cdnSettings.BookThumbnails is not null && cdnSettings.BookThumbnails.Length > 0) + { + pickedBookThumbnail = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}"; + pickedBookThumbnail1 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}"; + pickedBookThumbnail2 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}"; + pickedBookThumbnail3 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}"; + pickedBookThumbnail4 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}"; + } + // Step 1: Add Product var productCreateResult = await productService.CreateProductAsync(new Products.Models.CreateProduct { @@ -95,7 +121,8 @@ public class ProductsSeederService(ProductService productService, AuthorService Manufacturer = $"{authorFirstName} {authorLastName} / {publisherCompany}", SerialNumber = faker.Phone.PhoneNumber("978-##########") }, - Categories = ["Coding", "Computers", "IT"] + Categories = ["Coding", "Computers", "IT"], + ThumbnailUrls = pickedBookThumbnail is not null ? [pickedBookThumbnail, pickedBookThumbnail1!, pickedBookThumbnail2!, pickedBookThumbnail3!, pickedBookThumbnail4!] : null }, stoppingToken); if (productCreateResult.IsFailed) @@ -116,7 +143,6 @@ public class ProductsSeederService(ProductService productService, AuthorService // Step 3: Create Product Price var productPriceCreateResult = await productService.CreateProductPriceAsync(productId: productCreateResult.Value, request: new Products.Models.CreateProductPrice { - // Generates fair, dynamic prices in Rands between R150 and R650, snapped neatly to integers Amount = Math.Round(faker.Random.Decimal(150m, 650m), 2), Discount = 0.0m }, stoppingToken); @@ -127,6 +153,34 @@ public class ProductsSeederService(ProductService productService, AuthorService break; } + // Safe bounded picking for Authors (Real Avatars) + string authorAvatarUrl = faker.Internet.Avatar(); // Fallback + if (cdnSettings.Authors is not null && cdnSettings.Authors.Length > 0) + { + authorAvatarUrl = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.Authors)}"; + } + + // Safe bounded picking for Author Thumbnails (Cartoon Avatars) + string? authorThumbnailUrl = null; + if (cdnSettings.AuthorThumbnails is not null && cdnSettings.AuthorThumbnails.Length > 0) + { + var selectedThumb = faker.PickRandom(cdnSettings.AuthorThumbnails); + authorThumbnailUrl = $"{cdnSettings.BaseCdn}{selectedThumb}.jpg"; + } + + // Synthesize a highly dynamic, organic opening bio statement + var professionalBackgrounds = new[] + { + $"{authorFirstName} {authorLastName} is an award-winning {faker.Name.JobDescriptor()} {faker.Name.JobTitle()} with over {faker.Random.Number(5, 25)} years of core engineering domain expertise.", + $"As a veteran systems consultant and practicing {faker.Name.JobTitle()}, {authorFirstName} has spent decades leading digital infrastructure transformations and managing complex topologies.", + $"Operating from modern innovation hubs, {authorFirstName} {authorLastName} specializes in global product strategies and serves as an authority in {faker.Name.JobDescriptor()} computing.", + $"With a rich professional background as a principal {faker.Name.JobTitle()} at {publisherCompany}, {authorFirstName} has spent a lifetime refining the system workflows highlighted here." + }; + + // Pick a randomized context hook and append a 2-paragraph contextual narrative block + var biographyPrefix = faker.PickRandom(professionalBackgrounds); + var authorBiography = $"{biographyPrefix} {faker.Lorem.Paragraph(2)}"; + // Step 4: Create Author var authorCreateResult = await authorService.CreateAuthorAsync(request: new Authors.Models.CreateAuthor { @@ -137,7 +191,7 @@ public class ProductsSeederService(ProductService productService, AuthorService PublisherType = faker.PickRandom(), Email = faker.Internet.Email(authorFirstName, authorLastName), Website = faker.Internet.Url(), - ImageUrl = faker.Internet.Avatar(), + ImageUrl = authorAvatarUrl, SocialMedia = [ new Models.SocialMedia @@ -155,8 +209,8 @@ public class ProductsSeederService(ProductService productService, AuthorService Url = $"https://github.com/tech-{authorFirstName.ToLower(culture)}" } ], - Biography = $"{authorFirstName} {authorLastName} is a veteran technologist and systems architect with over a decade of domain expertise. " + faker.Lorem.Paragraph(2), - ThumbnailImageUrl = null + Biography = authorBiography, + ThumbnailImageUrl = authorThumbnailUrl }, stoppingToken); if (authorCreateResult.IsFailed) diff --git a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json index 1dc7baa..ce89459 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json +++ b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json @@ -23,6 +23,239 @@ "d44a3c04-f124-4f0b-8301-3841ae2fd439_1764780121224.webp", "e6ba52f208914285bcdf1966cfb08f6f.jpg", "fa9cbbe6-f947-4f83-8e98-61d2661f43e0_1764841636705.webp" - ] + ], + "Authors": [ + "authors/uifaces-human-avatar.jpg", + "authors/uifaces-human-avatar-1.jpg", + "authors/uifaces-human-avatar-2.jpg", + "authors/uifaces-human-avatar-3.jpg", + "authors/uifaces-human-avatar-4.jpg", + "authors/uifaces-human-avatar-5.jpg", + "authors/uifaces-human-avatar-6.jpg", + "authors/uifaces-human-avatar-7.jpg", + "authors/uifaces-human-avatar-8.jpg", + "authors/uifaces-human-avatar-9.jpg", + "authors/uifaces-human-avatar-10.jpg", + "authors/uifaces-human-avatar-11.jpg", + "authors/uifaces-human-avatar-12.jpg", + "authors/uifaces-human-avatar-13.jpg", + "authors/uifaces-human-avatar-14.jpg", + "authors/uifaces-human-avatar-15.jpg", + "authors/uifaces-human-avatar-16.jpg" + ], + "AuthorThumbnails": [ + "authors/thumbnails/uifaces-cartoon-avatar-1", + "authors/thumbnails/uifaces-cartoon-avatar-2", + "authors/thumbnails/uifaces-cartoon-avatar-3", + "authors/thumbnails/uifaces-cartoon-avatar-4", + "authors/thumbnails/uifaces-cartoon-avatar-5", + "authors/thumbnails/uifaces-cartoon-avatar-6", + "authors/thumbnails/uifaces-cartoon-avatar-7", + "authors/thumbnails/uifaces-cartoon-avatar-8", + "authors/thumbnails/uifaces-cartoon-avatar-9", + "authors/thumbnails/uifaces-cartoon-avatar-10" + ], + "BookThumbnails": [ + "thumbnails/book_thumbnail_001.jpg", + "thumbnails/book_thumbnail_002.jpg", + "thumbnails/book_thumbnail_003.jpg", + "thumbnails/book_thumbnail_004.jpg", + "thumbnails/book_thumbnail_005.jpg", + "thumbnails/book_thumbnail_006.jpg", + "thumbnails/book_thumbnail_007.jpg", + "thumbnails/book_thumbnail_008.jpg", + "thumbnails/book_thumbnail_009.jpg", + "thumbnails/book_thumbnail_010.jpg", + "thumbnails/book_thumbnail_011.jpg", + "thumbnails/book_thumbnail_012.jpg", + "thumbnails/book_thumbnail_013.jpg", + "thumbnails/book_thumbnail_014.jpg", + "thumbnails/book_thumbnail_015.jpg", + "thumbnails/book_thumbnail_016.jpg", + "thumbnails/book_thumbnail_017.jpg", + "thumbnails/book_thumbnail_018.jpg", + "thumbnails/book_thumbnail_019.jpg", + "thumbnails/book_thumbnail_020.jpg", + "thumbnails/book_thumbnail_021.jpg", + "thumbnails/book_thumbnail_022.jpg", + "thumbnails/book_thumbnail_023.jpg", + "thumbnails/book_thumbnail_024.jpg", + "thumbnails/book_thumbnail_025.jpg", + "thumbnails/book_thumbnail_026.jpg", + "thumbnails/book_thumbnail_027.jpg", + "thumbnails/book_thumbnail_028.jpg", + "thumbnails/book_thumbnail_029.jpg", + "thumbnails/book_thumbnail_030.jpg", + "thumbnails/book_thumbnail_031.jpg", + "thumbnails/book_thumbnail_032.jpg", + "thumbnails/book_thumbnail_033.jpg", + "thumbnails/book_thumbnail_034.jpg", + "thumbnails/book_thumbnail_035.jpg", + "thumbnails/book_thumbnail_036.jpg", + "thumbnails/book_thumbnail_037.jpg", + "thumbnails/book_thumbnail_038.jpg", + "thumbnails/book_thumbnail_039.jpg", + "thumbnails/book_thumbnail_040.jpg", + "thumbnails/book_thumbnail_041.jpg", + "thumbnails/book_thumbnail_042.jpg", + "thumbnails/book_thumbnail_043.jpg", + "thumbnails/book_thumbnail_044.jpg", + "thumbnails/book_thumbnail_045.jpg", + "thumbnails/book_thumbnail_046.jpg", + "thumbnails/book_thumbnail_047.jpg", + "thumbnails/book_thumbnail_048.jpg", + "thumbnails/book_thumbnail_049.jpg", + "thumbnails/book_thumbnail_050.jpg", + "thumbnails/book_thumbnail_051.jpg", + "thumbnails/book_thumbnail_052.jpg", + "thumbnails/book_thumbnail_053.jpg", + "thumbnails/book_thumbnail_054.jpg", + "thumbnails/book_thumbnail_055.jpg", + "thumbnails/book_thumbnail_056.jpg", + "thumbnails/book_thumbnail_057.jpg", + "thumbnails/book_thumbnail_058.jpg", + "thumbnails/book_thumbnail_059.jpg", + "thumbnails/book_thumbnail_060.jpg", + "thumbnails/book_thumbnail_061.jpg", + "thumbnails/book_thumbnail_062.jpg", + "thumbnails/book_thumbnail_063.jpg", + "thumbnails/book_thumbnail_064.jpg", + "thumbnails/book_thumbnail_065.jpg", + "thumbnails/book_thumbnail_066.jpg", + "thumbnails/book_thumbnail_067.jpg", + "thumbnails/book_thumbnail_068.jpg", + "thumbnails/book_thumbnail_069.jpg", + "thumbnails/book_thumbnail_070.jpg", + "thumbnails/book_thumbnail_071.jpg", + "thumbnails/book_thumbnail_072.jpg", + "thumbnails/book_thumbnail_073.jpg", + "thumbnails/book_thumbnail_074.jpg", + "thumbnails/book_thumbnail_075.jpg", + "thumbnails/book_thumbnail_076.jpg", + "thumbnails/book_thumbnail_077.jpg", + "thumbnails/book_thumbnail_078.jpg", + "thumbnails/book_thumbnail_079.jpg", + "thumbnails/book_thumbnail_080.jpg", + "thumbnails/book_thumbnail_081.jpg", + "thumbnails/book_thumbnail_082.jpg", + "thumbnails/book_thumbnail_083.jpg", + "thumbnails/book_thumbnail_084.jpg", + "thumbnails/book_thumbnail_085.jpg", + "thumbnails/book_thumbnail_086.jpg", + "thumbnails/book_thumbnail_087.jpg", + "thumbnails/book_thumbnail_088.jpg", + "thumbnails/book_thumbnail_089.jpg", + "thumbnails/book_thumbnail_090.jpg", + "thumbnails/book_thumbnail_091.jpg", + "thumbnails/book_thumbnail_092.jpg", + "thumbnails/book_thumbnail_093.jpg", + "thumbnails/book_thumbnail_094.jpg", + "thumbnails/book_thumbnail_095.jpg", + "thumbnails/book_thumbnail_096.jpg", + "thumbnails/book_thumbnail_097.jpg", + "thumbnails/book_thumbnail_098.jpg", + "thumbnails/book_thumbnail_099.jpg", + "thumbnails/book_thumbnail_100.jpg", + "thumbnails/book_thumbnail_101.jpg", + "thumbnails/book_thumbnail_102.jpg", + "thumbnails/book_thumbnail_103.jpg", + "thumbnails/book_thumbnail_104.jpg", + "thumbnails/book_thumbnail_105.jpg", + "thumbnails/book_thumbnail_106.jpg", + "thumbnails/book_thumbnail_107.jpg", + "thumbnails/book_thumbnail_108.jpg", + "thumbnails/book_thumbnail_109.jpg", + "thumbnails/book_thumbnail_110.jpg", + "thumbnails/book_thumbnail_111.jpg", + "thumbnails/book_thumbnail_112.jpg", + "thumbnails/book_thumbnail_113.jpg", + "thumbnails/book_thumbnail_114.jpg", + "thumbnails/book_thumbnail_115.jpg", + "thumbnails/book_thumbnail_116.jpg", + "thumbnails/book_thumbnail_117.jpg", + "thumbnails/book_thumbnail_118.jpg", + "thumbnails/book_thumbnail_119.jpg", + "thumbnails/book_thumbnail_120.jpg", + "thumbnails/book_thumbnail_121.jpg", + "thumbnails/book_thumbnail_122.jpg", + "thumbnails/book_thumbnail_123.jpg", + "thumbnails/book_thumbnail_124.jpg", + "thumbnails/book_thumbnail_125.jpg", + "thumbnails/book_thumbnail_126.jpg", + "thumbnails/book_thumbnail_127.jpg", + "thumbnails/book_thumbnail_128.jpg", + "thumbnails/book_thumbnail_129.jpg", + "thumbnails/book_thumbnail_130.jpg", + "thumbnails/book_thumbnail_131.jpg", + "thumbnails/book_thumbnail_132.jpg", + "thumbnails/book_thumbnail_133.jpg", + "thumbnails/book_thumbnail_134.jpg", + "thumbnails/book_thumbnail_135.jpg", + "thumbnails/book_thumbnail_136.jpg", + "thumbnails/book_thumbnail_137.jpg", + "thumbnails/book_thumbnail_138.jpg", + "thumbnails/book_thumbnail_139.jpg", + "thumbnails/book_thumbnail_140.jpg", + "thumbnails/book_thumbnail_141.jpg", + "thumbnails/book_thumbnail_142.jpg", + "thumbnails/book_thumbnail_143.jpg", + "thumbnails/book_thumbnail_144.jpg", + "thumbnails/book_thumbnail_145.jpg", + "thumbnails/book_thumbnail_146.jpg", + "thumbnails/book_thumbnail_147.jpg", + "thumbnails/book_thumbnail_148.jpg", + "thumbnails/book_thumbnail_149.jpg", + "thumbnails/book_thumbnail_150.jpg", + "thumbnails/book_thumbnail_151.jpg", + "thumbnails/book_thumbnail_152.jpg", + "thumbnails/book_thumbnail_153.jpg", + "thumbnails/book_thumbnail_154.jpg", + "thumbnails/book_thumbnail_155.jpg", + "thumbnails/book_thumbnail_156.jpg", + "thumbnails/book_thumbnail_157.jpg", + "thumbnails/book_thumbnail_158.jpg", + "thumbnails/book_thumbnail_159.jpg", + "thumbnails/book_thumbnail_160.jpg", + "thumbnails/book_thumbnail_161.jpg", + "thumbnails/book_thumbnail_162.jpg", + "thumbnails/book_thumbnail_163.jpg", + "thumbnails/book_thumbnail_164.jpg", + "thumbnails/book_thumbnail_165.jpg", + "thumbnails/book_thumbnail_166.jpg", + "thumbnails/book_thumbnail_167.jpg", + "thumbnails/book_thumbnail_168.jpg", + "thumbnails/book_thumbnail_169.jpg", + "thumbnails/book_thumbnail_170.jpg", + "thumbnails/book_thumbnail_171.jpg", + "thumbnails/book_thumbnail_172.jpg", + "thumbnails/book_thumbnail_173.jpg", + "thumbnails/book_thumbnail_174.jpg", + "thumbnails/book_thumbnail_175.jpg", + "thumbnails/book_thumbnail_176.jpg", + "thumbnails/book_thumbnail_177.jpg", + "thumbnails/book_thumbnail_178.jpg", + "thumbnails/book_thumbnail_179.jpg", + "thumbnails/book_thumbnail_180.jpg", + "thumbnails/book_thumbnail_181.jpg", + "thumbnails/book_thumbnail_182.jpg", + "thumbnails/book_thumbnail_183.jpg", + "thumbnails/book_thumbnail_184.jpg", + "thumbnails/book_thumbnail_185.jpg", + "thumbnails/book_thumbnail_186.jpg", + "thumbnails/book_thumbnail_187.jpg", + "thumbnails/book_thumbnail_188.jpg", + "thumbnails/book_thumbnail_189.jpg", + "thumbnails/book_thumbnail_190.jpg", + "thumbnails/book_thumbnail_191.jpg", + "thumbnails/book_thumbnail_192.jpg", + "thumbnails/book_thumbnail_193.jpg", + "thumbnails/book_thumbnail_194.jpg", + "thumbnails/book_thumbnail_195.jpg", + "thumbnails/book_thumbnail_196.jpg", + "thumbnails/book_thumbnail_197.jpg", + "thumbnails/book_thumbnail_198.jpg", + "thumbnails/book_thumbnail_199.jpg", + "thumbnails/book_thumbnail_200.jpg" + ] } -} +} \ No newline at end of file From 2db3b3d293a037d5ca3ebf2a26e4fc13f305f3b6 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 00:11:19 +0200 Subject: [PATCH 067/153] Added customer seeder with order data --- .../CustomerSeederService.cs | 275 ++++++++++++++++++ ...teCharms.Features.MidrandBooks.Seed.csproj | 10 +- .../ProductsSeederService.cs | 4 +- .../Program.cs | 9 +- .../appsettings.json | 6 +- 5 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs diff --git a/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs new file mode 100644 index 0000000..6814a6d --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs @@ -0,0 +1,275 @@ +using LiteCharms.Features.MidrandBooks.Customers; +using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.MidrandBooks.Orders; +using LiteCharms.Features.MidrandBooks.Orders.Models; + +namespace LiteCharms.Features.MidrandBooks.Seed; + +public class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!await features.IsEnabledAsync("CustomerSeederService")) return; + + logger.LogInformation("Customer Seeding started"); + + // 1: Add shipping providers (shippingProvider IDs will be created in sequence: 1, 2, 3) + await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.FastWay, "FastWay Couriers", 39, "https://www.fastway.co.za/our-services/track-your-parcel"), stoppingToken); + await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.DHL, "DHL Couriers", 60, "https://www.dhl.com/za-en/home/tracking.html"), stoppingToken); + await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.PostNet, "Postnet Overnight Mail", 45, "https://www.postnet.co.za/tracker"), stoppingToken); + + // Initialize Bogus Faker engine + var faker = new Faker(); + var culture = CultureInfo.InvariantCulture; + + // Ensure repeatable datasets across executions + Randomizer.Seed = new Random(84); + + // South African Provinces array lookup helper + var southAfricanProvinces = new[] + { + "Gauteng", "Western Cape", "KwaZulu-Natal", "Eastern Cape", + "Free State", "Limpopo", "Mpumalanga", "North West", "Northern Cape" + }; + + // South African major towns matching geographic boundaries roughly + var southAfricanCities = new[] { "Midrand", "Johannesburg", "Pretoria", "Cape Town", "Durban", "Gqeberha", "Polokwane", "Nelspruit", "Bloemfontein" }; + + // Tracks sequential Address IDs added globally to the system across all loops + long addressSequenceCounter = 0; + + // 2: Create 15 customers with resources sequentially + for (int c = 0; c < 15; c++) + { + if (stoppingToken.IsCancellationRequested) break; + + // Determine if this specific iteration represents a Corporate Client or an Individual Consumer + bool isCompanyCustomer = faker.Random.Bool(0.4f); // 40% chance of seeding a corporate entity + + string customerFirstName = faker.Name.FirstName(); + string customerLastName = faker.Name.LastName(); + + string companyName = isCompanyCustomer ? faker.Company.CompanyName() : ""; + string companySuffix = isCompanyCustomer ? faker.Company.CompanySuffix() : ""; + string fullCompanyName = isCompanyCustomer ? $"{companyName} {companySuffix}" : ""; + + string customerEmail = isCompanyCustomer + ? faker.Internet.Email(firstName: companyName, provider: "co.za").ToLower(culture) + : faker.Internet.Email(customerFirstName, customerLastName).ToLower(culture); + + string customerPhone = faker.Phone.PhoneNumber("087#######"); // Corporate VOIP / Personal South African cell line format + string customerWebsite = isCompanyCustomer ? faker.Internet.Url().Replace("www.", $"www.{companyName.ToLower(culture)}.") : ""; + string customerVat = isCompanyCustomer ? faker.Phone.PhoneNumber("4#########") : ""; // SA VAT registration starts with a 4 + + // Randomly select distinct Social Media channels + var chosenSocialType = faker.PickRandom(); + string socialMediaUrl = chosenSocialType switch + { + SocialMediaTypes.LinkedIn => isCompanyCustomer ? $"https://linkedin.com/company/{companyName.ToLower(culture)}" : $"https://linkedin.com/in/{customerFirstName.ToLower(culture)}-{customerLastName.ToLower(culture)}", + SocialMediaTypes.GitHub => $"https://github.com/{(isCompanyCustomer ? "orgs/" + companyName.ToLower(culture) : customerFirstName.ToLower(culture))}", + _ => $"https://x.com/{(isCompanyCustomer ? companyName.ToLower(culture) : customerFirstName.ToLower(culture))}" + }; + + // 3: Create customer + var createCustomerResult = await customerService.CreateCustomerAsync(new CreateCustomer + { + Company = fullCompanyName, + Email = customerEmail, + Phone = customerPhone, + Website = customerWebsite, + SocialMedia = + [ + new Models.SocialMedia + { + Name = chosenSocialType.ToString(), + Type = chosenSocialType, + ImageUrl = $"https://cdn.example.com/icons/{chosenSocialType.ToString().ToLower(culture)}.png", + Url = socialMediaUrl + } + ], + VatNumber = customerVat + }, stoppingToken); + + if (createCustomerResult.IsFailed) + { + logger.LogError("Failed to create customer record at index {Index}: {Error}", c, createCustomerResult.Errors[0].Message); + break; + } + + var assignedCustomerId = createCustomerResult.Value; + + // 4: Create customer contact (only if customer is a company entity) + if (isCompanyCustomer) + { + var contactFirstName = faker.Name.FirstName(); + var contactLastName = faker.Name.LastName(); + + var createContactResult = await customerService.CreateCustomerContactAsync(assignedCustomerId, new CreateCustomerContact + { + Name = contactFirstName, + LastName = contactLastName, + Phone = faker.Phone.PhoneNumber("082#######"), // Typical South African mobile prefix format + Email = faker.Internet.Email(contactFirstName, contactLastName, provider: "company.co.za").ToLower(culture), + Type = ContactTypes.Business + }, stoppingToken); + + if (createContactResult.IsFailed) + { + logger.LogError("Failed to create company customer contact relation: {Error}", createContactResult.Errors[0].Message); + break; + } + } + + // Shared Randomizations for Regional Postal/Building details + var primaryState = faker.PickRandom(southAfricanProvinces); + var primaryCity = faker.PickRandom(southAfricanCities); + var shippingPostalCode = faker.Random.Replace("####"); + + var billingState = faker.PickRandom(southAfricanProvinces); + var billingCity = faker.PickRandom(southAfricanCities); + var billingPostalCode = faker.Random.Replace("####"); + + // 5: Create customer address - SHIPPING + var createShippingAddressResult = await customerService.CreateCustomerAddressAsync(assignedCustomerId, new CreateCustomerAddress + { + Name = isCompanyCustomer ? "Head Office Distribution" : "My Home Residence", + BuildingType = faker.PickRandom(), + Type = AddressType.Shipping, + Street = $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Street", + City = primaryCity, + State = primaryState, + Country = "South Africa", + IsPrimary = true, + Enabled = true, + PostalCode = shippingPostalCode + }, stoppingToken); + + long currentCustomerShippingAddressId = 0; + if (createShippingAddressResult.IsSuccess) + { + addressSequenceCounter++; + currentCustomerShippingAddressId = addressSequenceCounter; + } + else + { + logger.LogWarning("Failed to attach Shipping address profile: {Error}", createShippingAddressResult.Errors[0].Message); + } + + // 6: Create customer address - BILLING + var createBillingAddressResult = await customerService.CreateCustomerAddressAsync(assignedCustomerId, new CreateCustomerAddress + { + Name = isCompanyCustomer ? "Accounts Payable Department" : "Billing Address", + BuildingType = faker.PickRandom(), + Type = AddressType.Billing, + Street = isCompanyCustomer ? $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Boulevard" : $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Street", + City = billingCity, + State = billingState, + Country = "South Africa", + IsPrimary = false, + Enabled = true, + PostalCode = billingPostalCode + }, stoppingToken); + + long currentCustomerBillingAddressId = 0; + if (createBillingAddressResult.IsSuccess) + { + addressSequenceCounter++; + currentCustomerBillingAddressId = addressSequenceCounter; + } + else + { + logger.LogError("Failed to attach Billing address profile: {Error}", createBillingAddressResult.Errors[0].Message); + break; + } + + // 7: Challenge Extrapolation — Create a random number of orders (0 to 4 orders) per customer + int ordersToGenerate = faker.Random.Number(0, 4); + for (int o = 0; o < ordersToGenerate; o++) + { + var deliveryInstructions = faker.PickRandom( + "Leave at reception desk", + "Please call before delivery", + "At the intercom, dial 1 then option 2", + "Leave with security guard at front gate", + "Deliver to back delivery bay" + ); + + // Use the calculated sequential Billing Address Id for order creation + var orderResult = await orderService.CreateOrderAsync( + assignedCustomerId, + new CreateOrder(currentCustomerBillingAddressId, deliveryInstructions), + stoppingToken + ); + + if (orderResult.IsFailed) + { + logger.LogWarning("Failed to create purchase order shell context: {Error}", orderResult.Errors[0].Message); + continue; + } + + long seededOrderId = orderResult.Value; + + // Build a varying array of items using valid product bounds (IDs: 0 to 21) + int lineItemsCount = faker.Random.Number(1, 5); + var itemsList = new List(); + + for (int i = 0; i < lineItemsCount; i++) + { + long randomProductId = faker.Random.Number(0, 21); + long randomProductPriceId = faker.Random.Number(0, 21); + int itemQuantity = faker.Random.Number(1, 3); + + itemsList.Add(new CreateOrderItem(randomProductId, randomProductPriceId, itemQuantity)); + } + + // Push bulk items payload into order via matching test framework signatures + var addItemsResult = await orderService.AddItemsToOrderAsync(seededOrderId, [.. itemsList], stoppingToken); + if (addItemsResult.IsFailed) + { + logger.LogWarning("Failed to link item collections to Order Id {Id}", seededOrderId); + continue; + } + + // Randomly select an order status matrix pathing + var targetedOrderStatus = faker.PickRandom(); + await orderService.UpdateOrderStatusAsync(seededOrderId, targetedOrderStatus, stoppingToken); + + // Check lifecycle workflow criteria: Attach dynamic shipping if status warrants it + if (targetedOrderStatus != OrderStatus.Pending && + targetedOrderStatus != OrderStatus.Cancelled && + targetedOrderStatus != OrderStatus.Failed && + currentCustomerShippingAddressId > 0) + { + // Select from seeded Shipping Providers in step 1 (IDs: 1, 2, or 3) + long randomShippingProviderId = faker.Random.Number(1, 3); + + var addShippingResult = await orderService.AddShippingToOrderAsync( + seededOrderId, + new CreateShipping(currentCustomerShippingAddressId, randomShippingProviderId), + stoppingToken + ); + + if (addShippingResult.IsSuccess) + { + long assignedShippingId = addShippingResult.Value; + + // Transition logistics flags matching delivery metrics + var shippingStatus = faker.PickRandom(); + await orderService.UpdateShippingStatusAsync(seededOrderId, shippingStatus, stoppingToken); + + if (shippingStatus == ShippingStatuses.Shipped || shippingStatus == ShippingStatuses.Delivered) + { + string rawTrackingCode = $"ZA{faker.Random.Replace("#########")}NV"; + await orderService.UpdateShippingTrackingNumberAsync(seededOrderId, assignedShippingId, rawTrackingCode); + } + } + } + } + + logger.LogInformation("Successfully seeded customer profile #{Index}: {Name} alongside {Count} orders.", c, isCompanyCustomer ? fullCompanyName : $"{customerFirstName} {customerLastName}", ordersToGenerate); + } + + logger.LogInformation("Customer Seeding completed successfully."); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj index 14e2ffd..b9be4ff 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj +++ b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj @@ -11,10 +11,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -84,7 +85,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -95,8 +96,8 @@ - - + + @@ -128,6 +129,7 @@ + diff --git a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs index 9dfc582..868a454 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs @@ -6,12 +6,14 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration; namespace LiteCharms.Features.MidrandBooks.Seed; public class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService, - IOptions options, ILogger logger) : BackgroundService + IFeatureManager features, IOptions options, ILogger logger) : BackgroundService { private readonly CdnSettings cdnSettings = options.Value; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + if (await features.IsEnabledAsync("ProductsSeederService") is not true) return; + logger.LogInformation("Product Seeding started"); if (cdnSettings.BookCovers is null || cdnSettings.BookCovers.Length == 0) diff --git a/LiteCharms.Features.MidrandBooks.Seed/Program.cs b/LiteCharms.Features.MidrandBooks.Seed/Program.cs index f31cdd3..01b4fbf 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/Program.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/Program.cs @@ -5,13 +5,18 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration; var builder = Host.CreateApplicationBuilder(args); builder.Configuration - .AddJsonFile("appsettings.json") - .AddUserSecrets(typeof(Program).Assembly); + .AddCommandLine(args) + .AddUserSecrets(typeof(Program).Assembly) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables(); + +builder.Services.AddScopedFeatureManagement(); builder.Services .AddLogging() .AddShopServices() .AddHostedService() + .AddHostedService() .AddMidrandShopDatabase(builder.Configuration); builder.Services.Configure(options => builder.Configuration.GetSection(nameof(CdnSettings)).Bind(options)); diff --git a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json index ce89459..b710f54 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json +++ b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json @@ -1,4 +1,8 @@ { + "FeatureManagement": { + "CustomerSeederService": false, + "ProductsSeederService": false + }, "CdnSettings": { "BaseCdn": "https://bookshop.cdn.khongisa.co.za/design/", "BookCovers": [ @@ -257,5 +261,5 @@ "thumbnails/book_thumbnail_199.jpg", "thumbnails/book_thumbnail_200.jpg" ] - } + } } \ No newline at end of file From e40c958066811111708d7458f41c54201a4e03d3 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 14:22:00 +0200 Subject: [PATCH 068/153] Implemented category feature --- .../CategoryServiceFeatureTests.cs | 70 ++ .../Categories/CategoryService.cs | 132 +++ .../Categories/Entities/Category.cs | 4 + .../Entities/CategoryConfiguration.cs | 14 + .../Categories/Models/Category.cs | 12 + .../Extensions/Mappers.cs | 15 +- .../LiteCharms.Features.MidrandBooks.csproj | 10 +- .../Postgres/MidrandBooksDbContext.cs | 3 + ...20260530104851_AddedCategories.Designer.cs | 966 ++++++++++++++++++ .../20260530104851_AddedCategories.cs | 37 + .../MidrandBooksDbContextModelSnapshot.cs | 28 + 11 files changed, 1284 insertions(+), 7 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs create mode 100644 LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Categories/Entities/Category.cs create mode 100644 LiteCharms.Features.MidrandBooks/Categories/Entities/CategoryConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Categories/Models/Category.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.cs diff --git a/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs new file mode 100644 index 0000000..7c8fbea --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs @@ -0,0 +1,70 @@ +using LiteCharms.Features.MidrandBooks.Categories; +using LiteCharms.Features.MidrandBooks.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class CategoryServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly CategoryService categoryService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task UpdateCategoryStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await categoryService.UpdateCategoryStatusAsync(3, false, false, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetCategoryAsync_ShouldReturn_ResultWithCategory() + { + var result = await categoryService.GetCategoryAsync(3, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetCategoriesAsync_ShouldReturn_All_ResultWithCategoryList() + { + var result = await categoryService.GetCategoriesAsync(isMain: null,fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCategoriesAsync_ShouldReturn_MainCategory_ResultWithCategoryList() + { + var result = await categoryService.GetCategoriesAsync(true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCategoriesAsync_ShouldReturn_SubMainCategory_ResultWithCategoryList() + { + var result = await categoryService.GetCategoriesAsync(false, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task CreateCategoriesAsync_ShouldReturn_ResultWithSuccess() + { + var result = await categoryService.CreateCategoriesAsync(fixture.CancellationToken, "Test", "Test 1", "Test 2"); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreateCategoryAsync_ShouldReturn_ResultWithCategoryId() + { + var result = await categoryService.CreateCategoryAsync("Test", false, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs new file mode 100644 index 0000000..dd6c557 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs @@ -0,0 +1,132 @@ +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Categories.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Categories; + +public sealed class CategoryService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask UpdateCategoryStatusAsync(long categoryId, bool enabled, bool isMain, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Categories + .Where(c => c.Id == categoryId && c.Enabled) + .ExecuteUpdateAsync(setters => setters + .SetProperty(c => c.Enabled, enabled) + .SetProperty(c => c.IsMain, isMain), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update category")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCategoryAsync(long categoryId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var category = await context.Categories.AsNoTracking().FirstOrDefaultAsync(c => c.Id == categoryId, cancellationToken); + + return category is not null + ? Result.Ok(category.ToModel()) + : Result.Fail("Failed to create new category"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCategoriesAsync(bool? isMain = null, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var query = context.Categories.AsNoTracking() + .OrderByDescending(o => o.IsMain) + .ThenByDescending(o => o.Id) + .ThenBy(o => o.IsMain) + .AsQueryable(); + + query = isMain is null + ? query.Where(c => c.Enabled).AsQueryable() + : query.Where(c => c.Enabled && c.IsMain == isMain.Value); + + var categories = await query.ToListAsync(cancellationToken); + + return categories?.Count > 0 + ? Result.Ok(categories.Select(c => c.ToModel()).ToArray()) + : Result.Fail("No categories found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask CreateCategoriesAsync(CancellationToken cancellationToken = default, params string[] categories) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + foreach (var category in categories) + { + if (await context.Categories.AnyAsync(c => EF.Functions.ILike(c.Name!, category!), cancellationToken)) + continue; + + context.Categories.Add(new Entities.Category + { + Name = category.Humanize(LetterCasing.Title), + IsMain = false, + Enabled = true, + }); + } + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to add any category in the list"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateCategoryAsync(string category, bool isMain, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Categories.AnyAsync(c => EF.Functions.ILike(c.Name!, category!), cancellationToken)) + return Result.Fail($"Category '{category}' already exists"); + + var newCategory = context.Categories.Add(new Entities.Category + { + Name = StringHumanizeExtensions.Humanize(category, LetterCasing.Title), + IsMain = isMain, + Enabled = true, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newCategory.Entity.Id) + : Result.Fail("Failed to create new category"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Categories/Entities/Category.cs b/LiteCharms.Features.MidrandBooks/Categories/Entities/Category.cs new file mode 100644 index 0000000..5aac931 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/Entities/Category.cs @@ -0,0 +1,4 @@ +namespace LiteCharms.Features.MidrandBooks.Categories.Entities; + +[EntityTypeConfiguration] +public sealed class Category : Models.Category; diff --git a/LiteCharms.Features.MidrandBooks/Categories/Entities/CategoryConfiguration.cs b/LiteCharms.Features.MidrandBooks/Categories/Entities/CategoryConfiguration.cs new file mode 100644 index 0000000..bc5a7bc --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/Entities/CategoryConfiguration.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.MidrandBooks.Categories.Entities; + +public sealed class CategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Categories"); + + builder.HasKey(c => c.Id); + builder.Property(c => c.Name).IsRequired().HasMaxLength(15); + builder.Property(c => c.IsMain).HasDefaultValue(false); + builder.Property(c => c.Enabled).HasDefaultValue(true); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Categories/Models/Category.cs b/LiteCharms.Features.MidrandBooks/Categories/Models/Category.cs new file mode 100644 index 0000000..b793204 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/Models/Category.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.MidrandBooks.Categories.Models; + +public class Category +{ + public long Id { get; set; } + + public string? Name { get; set; } + + public bool IsMain { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 753bcc7..3c3790d 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Categories.Models; using LiteCharms.Features.MidrandBooks.Customers.Models; using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Pages.Models; @@ -9,6 +10,14 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Mappers { + public static Category ToModel(this Categories.Entities.Category entity) => new() + { + Id = entity.Id, + Name = entity.Name, + IsMain = entity.IsMain, + Enabled = entity.Enabled, + }; + public static ShippingProvider ToModel(this Orders.Entities.ShippingProvider entity) => new() { Id = entity.Id, @@ -65,7 +74,7 @@ public static class Mappers SocialMedia = entiry.SocialMedia, UpdatedAt = entiry.UpdatedAt, VatNumber = entiry.VatNumber, - Website = entiry.Website + Website = entiry.Website }; public static Address ToModel(this Customers.Entities.Address entity) => new() @@ -83,7 +92,7 @@ public static class Mappers Street = entity.Street, City = entity.City, State = entity.State, - Country = entity.Country + Country = entity.Country }; public static Contact ToModel(this Customers.Entities.Contact entity) => new() @@ -112,7 +121,7 @@ public static class Mappers Enabled = entity.Enabled, Notes = entity.Notes, References = entity.References, - Type = entity.Type + Type = entity.Type }; public static AuthorBook ToModel(this AuthorBooks.Entities.AuthorBook entity) => new() diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 17db6dd..e2e58d3 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -31,7 +31,8 @@ - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -104,7 +105,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -115,8 +116,8 @@ - - + + @@ -147,6 +148,7 @@ + diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index a70e1cf..30af0d7 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; using LiteCharms.Features.MidrandBooks.Authors.Entities; +using LiteCharms.Features.MidrandBooks.Categories.Entities; using LiteCharms.Features.MidrandBooks.Customers.Entities; using LiteCharms.Features.MidrandBooks.Orders.Entities; using LiteCharms.Features.MidrandBooks.Pages.Entities; @@ -35,4 +36,6 @@ public sealed class MidrandBooksDbContext(DbContextOptions Shippings => Set(); public DbSet ShippingProviders => Set(); + + public DbSet Categories => Set(); } diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.Designer.cs new file mode 100644 index 0000000..e007a80 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.Designer.cs @@ -0,0 +1,966 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260530104851_AddedCategories")] + partial class AddedCategories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection("Categories") + .HasColumnType("text[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.cs new file mode 100644 index 0000000..96d30de --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(15)", maxLength: 15, nullable: false), + IsMain = table.Column(type: "boolean", nullable: false, defaultValue: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Categories"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 6045ed4..ec18d1c 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -130,6 +130,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.ToTable("Authors", (string)null); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => { b.Property("Id") From 18d1640808a4542e3204b9ca9b70c86189932b59 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 15:35:35 +0200 Subject: [PATCH 069/153] Added product categories --- .../CategoryServiceFeatureTests.cs | 2 +- .../ProductServiceFeatureTests.cs | 45 +- .../Categories/CategoryService.cs | 4 +- .../Extensions/Mappers.cs | 32 +- .../Postgres/MidrandBooksDbContext.cs | 2 + ...0125323_AddedProductCategories.Designer.cs | 1007 +++++++++++++++++ .../20260530125323_AddedProductCategories.cs | 68 ++ .../MidrandBooksDbContextModelSnapshot.cs | 47 +- .../Products/Entities/Product.cs | 2 + .../Products/Entities/ProductCategory.cs | 11 + .../Entities/ProductCategoryConfiguration.cs | 23 + .../Products/Entities/ProductConfiguration.cs | 1 - .../Products/Models/Product.cs | 2 - .../Products/Models/ProductCategory.cs | 10 + .../Products/ProductService.cs | 99 +- LiteCharms.Features/Models/ProductFilter.cs | 2 - 16 files changed, 1318 insertions(+), 39 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategory.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategoryConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/ProductCategory.cs diff --git a/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs index 7c8fbea..c1f103c 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs @@ -62,7 +62,7 @@ public class CategoryServiceFeatureTests(Fixture fixture) : IClassFixture 0); diff --git a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs index 6dc7e88..d4e6e4c 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs @@ -10,7 +10,40 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu private readonly ProductService productService = fixture.Services.GetRequiredService(); [IntegrationFact] - public async Task GetProductPriceAsync_ShouldReturn_RetultOneProductPrice() + public async Task AddProductCategoryAsync_ShouldReturn_ResultWithId() + { + var result = await productService.AddProductCategoryAsync(1, 2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetProductCategoriesAsync_ShouldReturn_ResultWithCategoryList() + { + var result = await productService.GetProductCategoriesAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task DeleteProductCategoryAsync_ShouldReturn_ResultWithSuccess() + { + var result = await productService.DeleteProductCategoryAsync(1, 1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task DeleteAllProductCategoriesAsync_ShouldReturn_ResultWithSuccess() + { + var result = await productService.DeleteAllProductCategoriesAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetProductPriceAsync_ShouldReturn_ResultOneProductPrice() { var result = await productService.GetProductPriceAsync(2, fixture.CancellationToken); @@ -21,7 +54,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu } [IntegrationFact] - public async Task GetProductPricesAsync_ShouldReturn_RetultProductPriceList() + public async Task GetProductPricesAsync_ShouldReturn_ResultProductPriceList() { var result = await productService.GetProductPricesAsync(2, fixture.CancellationToken); @@ -32,7 +65,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu } [IntegrationFact] - public async Task SearchProductsAsync_ShouldReturn_RetultMatchingProducts() + public async Task SearchProductsAsync_ShouldReturn_ResultMatchingProducts() { var filter = new ProductFilter { @@ -52,7 +85,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu } [IntegrationFact] - public async Task GetProductAsync_ShouldReturn_RetultOneProduct() + public async Task GetProductAsync_ShouldReturn_ResultOneProduct() { var result = await productService.GetProductAsync(2, fixture.CancellationToken); @@ -63,7 +96,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu } [IntegrationFact] - public async Task GetProductsAsync_ShouldReturn_RetultProducts() + public async Task GetProductsAsync_ShouldReturn_ResultProducts() { var range = new DateRange { @@ -81,7 +114,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu } [IntegrationFact] - public async Task UpdateProductStatusAsync_ShouldReturn_ResultTrue() + public async Task UpdateProductStatusAsync_ShouldResurn_ResultTrue() { var result = await productService.UpdateProductStatusAsync(2, true, fixture.CancellationToken); diff --git a/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs index dd6c557..2a5b9f4 100644 --- a/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs +++ b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs @@ -55,8 +55,8 @@ public sealed class CategoryService(IDbContextFactory con var query = context.Categories.AsNoTracking() .OrderByDescending(o => o.IsMain) - .ThenByDescending(o => o.Id) - .ThenBy(o => o.IsMain) + .ThenByDescending(o => o.Id) + .ThenBy(o => o.Name) .AsQueryable(); query = isMain is null diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 3c3790d..1a81b74 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -147,25 +147,21 @@ public static class Mappers Enabled = entity.Enabled }; - public static Product ToModel(this Products.Entities.Product entity) + public static Product ToModel(this Products.Entities.Product entity) => new Product { - return new Product - { - Id = entity.Id, - CreatedAt = entity.CreatedAt, - UpdatedAt = entity.UpdatedAt, - Name = entity.Name, - Summary = entity.Summary, - Description = entity.Description, - Type = entity.Type, - ImageUrl = entity.ImageUrl, - ThumbnailUrls = entity.ThumbnailUrls, - Metadata = entity.Metadata, - Categories = entity.Categories, - Enabled = entity.Enabled, - Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null, - }; - } + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Name = entity.Name, + Summary = entity.Summary, + Description = entity.Description, + Type = entity.Type, + ImageUrl = entity.ImageUrl, + ThumbnailUrls = entity.ThumbnailUrls, + Metadata = entity.Metadata, + Enabled = entity.Enabled, + Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null, + }; public static Author ToModel(this Authors.Entities.Author entity) => new() { diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index 30af0d7..782bb1f 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -38,4 +38,6 @@ public sealed class MidrandBooksDbContext(DbContextOptions ShippingProviders => Set(); public DbSet Categories => Set(); + + public DbSet ProductCategories => Set(); } diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.Designer.cs new file mode 100644 index 0000000..af132c2 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.Designer.cs @@ -0,0 +1,1007 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260530125323_AddedProductCategories")] + partial class AddedProductCategories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.cs new file mode 100644 index 0000000..4f35ac7 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.cs @@ -0,0 +1,68 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedProductCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Categories", + table: "Products"); + + migrationBuilder.CreateTable( + name: "ProductCategories", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProductId = table.Column(type: "bigint", nullable: false), + CategoryId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductCategories", x => x.Id); + table.ForeignKey( + name: "FK_ProductCategories_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProductCategories_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ProductCategories_CategoryId", + table: "ProductCategories", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductCategories_ProductId", + table: "ProductCategories", + column: "ProductId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ProductCategories"); + + migrationBuilder.AddColumn( + name: "Categories", + table: "Products", + type: "text[]", + nullable: true); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index ec18d1c..8af6b80 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -586,9 +586,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.PrimitiveCollection("Categories") - .HasColumnType("text[]"); - b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -633,6 +630,29 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.ToTable("Products", (string)null); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.Property("Id") @@ -911,6 +931,25 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Metadata"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") @@ -955,6 +994,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => { + b.Navigation("Categories"); + b.Navigation("Prices"); }); #pragma warning restore 612, 618 diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs index e7e82c6..265f787 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs @@ -3,5 +3,7 @@ [EntityTypeConfiguration] public class Product : Models.Product { + public virtual ICollection Categories { get; set; } = []; + public virtual ICollection Prices { get; set; } = []; } diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategory.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategory.cs new file mode 100644 index 0000000..6b84876 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategory.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.MidrandBooks.Categories.Entities; + +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +[EntityTypeConfiguration] +public class ProductCategory : Models.ProductCategory +{ + public virtual Product? Product { get; set; } + + public virtual Category? Category { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategoryConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategoryConfiguration.cs new file mode 100644 index 0000000..768926d --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategoryConfiguration.cs @@ -0,0 +1,23 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public sealed class ProductCategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ProductCategories"); + + builder.HasKey(p => p.Id); + builder.Property(p => p.ProductId).IsRequired(); + builder.Property(p => p.CategoryId).IsRequired(); + + builder.HasOne(p => p.Product) + .WithMany(p => p.Categories) + .HasForeignKey(p => p.ProductId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(c => c.Category) + .WithMany() + .HasForeignKey(c => c.CategoryId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs index 795097d..daf7f19 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs @@ -17,7 +17,6 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration builder.Property(f => f.Description).HasMaxLength(1024); builder.Property(f => f.ImageUrl).HasMaxLength(1024); builder.Property(f => f.Enabled).HasDefaultValue(false); - builder.Property(f => f.Categories).IsRequired(false); builder.Property(f => f.ThumbnailUrls).IsRequired(false); builder.OwnsOne(f => f.Metadata, b => { b.ToJson(); }); diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs index 1207c34..bf425e4 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs @@ -22,8 +22,6 @@ public class Product public string[]? ThumbnailUrls { get; set; } - public string[]? Categories { get; set; } - public ProductMetadata? Metadata { get; set; } public ProductPrice? Price { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/ProductCategory.cs b/LiteCharms.Features.MidrandBooks/Products/Models/ProductCategory.cs new file mode 100644 index 0000000..38e70de --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/ProductCategory.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class ProductCategory +{ + public long Id { get; set; } + + public long ProductId { get; set; } + + public long CategoryId { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 4d1b0ab..a7b47ce 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -1,4 +1,5 @@ using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Categories.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Products.Models; @@ -8,6 +9,100 @@ namespace LiteCharms.Features.MidrandBooks.Products; public sealed class ProductService(IDbContextFactory contextFactory) : IService { + public async ValueTask AddProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Products.AnyAsync(p => p.Id == productId && p.Enabled, cancellationToken)) + return Result.Fail("Product does not exist"); + + if (!await context.Categories.AnyAsync(c => c.Id == categoryId && c.Enabled, cancellationToken)) + return Result.Fail("Category does not exist"); + + if (await context.ProductCategories.AnyAsync(c => c.ProductId == productId && c.CategoryId == categoryId, cancellationToken)) + return Result.Fail("Category already assigned to product"); + + context.ProductCategories.Add(new Entities.ProductCategory + { + ProductId = productId, + CategoryId = categoryId, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Could not add category to product"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductCategoriesAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var categories = await context.ProductCategories.AsNoTracking() + .Where(p => p.ProductId == productId) + .OrderByDescending(o => o.Id) + .Select(p => p.Category) + .ToArrayAsync(cancellationToken); + + return categories?.Length > 0 + ? Result.Ok(categories.Select(c => c!.ToModel()).ToArray()) + : Result.Fail("Failed to get product categories"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeleteProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.ProductCategories + .Where(p => p.ProductId == productId && p.CategoryId == categoryId) + .ExecuteDeleteAsync(cancellationToken); + + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("No product categories were deleted"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeleteAllProductCategoriesAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.ProductCategories + .Where(p => p.ProductId == productId) + .ExecuteDeleteAsync(cancellationToken); + + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("No product categories were deleted"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + public async ValueTask UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) { try @@ -68,9 +163,6 @@ public sealed class ProductService(IDbContextFactory cont if (!string.IsNullOrWhiteSpace(filter.Title)) query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Title}%")); - if (!string.IsNullOrWhiteSpace(filter.Category)) - query = query.Where(p => p.Categories.Contains(filter.Category)); - if (!string.IsNullOrWhiteSpace(filter.Manufacturer)) query = query.Where(p => EF.Functions.ILike(p.Metadata!.Manufacturer!, $"%{filter.Manufacturer}%")); @@ -152,7 +244,6 @@ public sealed class ProductService(IDbContextFactory cont ImageUrl = request.ImageUrl, ThumbnailUrls = request.ThumbnailUrls, Metadata = request.Metadata, - Categories = request.Categories, Enabled = true }); diff --git a/LiteCharms.Features/Models/ProductFilter.cs b/LiteCharms.Features/Models/ProductFilter.cs index bfed022..78f6d90 100644 --- a/LiteCharms.Features/Models/ProductFilter.cs +++ b/LiteCharms.Features/Models/ProductFilter.cs @@ -6,8 +6,6 @@ public sealed class ProductFilter public string? Title { get; set; } - public string? Category { get; set; } - public string? Manufacturer { get; set; } public string? SerialNumber { get; set; } From 4d217401248d827faeea9902d7973221577e9773 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 16:07:23 +0200 Subject: [PATCH 070/153] Added category seeder --- .../CategorySeederService.cs | 133 ++++++++++++++++++ .../Program.cs | 1 + .../appsettings.json | 1 + .../AuthorBooks/BooksService.cs | 2 +- 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs diff --git a/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs new file mode 100644 index 0000000..5856761 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs @@ -0,0 +1,133 @@ +using LiteCharms.Features.MidrandBooks.Categories; +using LiteCharms.Features.MidrandBooks.Products; + +namespace LiteCharms.Features.MidrandBooks.Seed; + +public class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!await features.IsEnabledAsync("CategorySeederService")) return; + + logger.LogInformation("Category and Product-Tag Mapping Seeding started (15-char limit applied)"); + + // Initialize Bogus to ensure repeatable distribution matrix pathing + var faker = new Faker(); + Randomizer.Seed = new Random(101); + + // 1. Curate Broad Book Categories (IsMain = true, Max 20, Max 15 chars) + var broadMainCategories = new[] + { + "Fiction", "Non-Fiction", "Youth & Kids", "Academic", + "Biographies", "Business", "Sci-Fi & Fantasy", + "Thrillers", "Self-Help", "History", + "Spirituality", "Arts & Photo", "Technology", + "Cookbooks", "Travel & Maps", "Poetry & Drama", "Graphic Novels" + }; + + // 2. Curate Niche Subcategories/Tags (IsMain = false, Max 15 chars) + var specializedSubCategories = new[] + { + "Cyberpunk", "Space Opera", "Historical Fix", "Cozy Mystery", "True Crime", + "Agile Project", "Software Eng", "AI & ML", "Cloud Comput", + "SA History", "African Lit", "Apartheid Era", "Mandela Legacy", + "Finance", "Investments", "Startup", "Leadership", + "CBT Therapy", "Mindfulness", "Yoga & Health", + "Baking Basics", "African Food", "Vegan Recipes", + "Ancient World", "WWII History", "Geopolitics", + "Writing Guides", "Criticism", "Classic Poetry", + "Early Learning", "Teen Romance", "Survival", + "Urban Fantasy", "Dark Fantasy", "Psych Thriller", "Hard Sci-Fi", + "Data Science", "DevOps", "Cybersecurity", + "Economics", "Real Estate", "Governance", + "Essays", "Memoirs", "Art History", + "Architecture", "Photography", "Travel Writing", + "Gaming Culture", "Philosophy", "Ethics", + "DIY Home", "SA Gardening", "Parenting" + }; + + // 3. Seed Main Categories into the System + logger.LogInformation("Seeding broad main categories..."); + foreach (var mainCat in broadMainCategories) + { + if (stoppingToken.IsCancellationRequested) return; + + // Defensive truncation fallback just in case strings get modified later + string safeName = mainCat.Length > 15 ? mainCat.Substring(0, 15) : mainCat; + + var result = await categoryService.CreateCategoryAsync(safeName, isMain: true, stoppingToken); + if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase)) + { + logger.LogWarning("Notice while adding main category '{Name}': {Msg}", safeName, result.Errors[0].Message); + } + } + + // 4. Seed Subcategories into the System + logger.LogInformation("Seeding boundless specialized niche tags..."); + foreach (var subCat in specializedSubCategories) + { + if (stoppingToken.IsCancellationRequested) return; + + string safeName = subCat.Length > 15 ? subCat.Substring(0, 15) : subCat; + + var result = await categoryService.CreateCategoryAsync(safeName, isMain: false, stoppingToken); + if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase)) + { + logger.LogWarning("Notice while adding subcategory '{Name}': {Msg}", safeName, result.Errors[0].Message); + } + } + + // 5. Query back all enabled categories to extract active IDs for junction mapping + var fetchMainResult = await categoryService.GetCategoriesAsync(isMain: true, stoppingToken); + var fetchSubResult = await categoryService.GetCategoriesAsync(isMain: false, stoppingToken); + + if (fetchMainResult.IsFailed || fetchSubResult.IsFailed) + { + logger.LogError("Aborting junction seeding: Could not retrieve categories from data store."); + return; + } + + var mainCategoryIds = fetchMainResult.Value.Select(c => c.Id).ToArray(); + var subCategoryIds = fetchSubResult.Value.Select(c => c.Id).ToArray(); + + // 6. Map Categories to your Product Collection (Product IDs 0 - 21) + logger.LogInformation("Beginning Product-Category mapping assignments for Product IDs 0 through 21..."); + + for (long productId = 0; productId <= 21; productId++) + { + if (stoppingToken.IsCancellationRequested) break; + + // Every book belongs to 1 or 2 main categories + int mainCategoriesToAssign = faker.Random.Number(1, 2); + var chosenMainIds = faker.PickRandom(mainCategoryIds, mainCategoriesToAssign).Distinct(); + + foreach (var mainId in chosenMainIds) + { + var linkResult = await productService.AddProductCategoryAsync(productId, mainId, stoppingToken); + if (linkResult.IsFailed) + { + if (!linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug("Junction note for Product {PId} and Main Category {CId}: {Msg}", productId, mainId, linkResult.Errors[0].Message); + } + } + } + + // Every book gets 1 to 4 granular subgenre tags + int subCategoriesToAssign = faker.Random.Number(1, 4); + var chosenSubIds = faker.PickRandom(subCategoryIds, subCategoriesToAssign).Distinct(); + + foreach (var subId in chosenSubIds) + { + var linkResult = await productService.AddProductCategoryAsync(productId, subId, stoppingToken); + if (linkResult.IsFailed && !linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug("Junction note for Product {PId} and Sub Category {CId}: {Msg}", productId, subId, linkResult.Errors[0].Message); + } + } + } + + logger.LogInformation("Category and Product-Tag Mapping Seeding completed successfully."); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Seed/Program.cs b/LiteCharms.Features.MidrandBooks.Seed/Program.cs index 01b4fbf..10d3172 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/Program.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/Program.cs @@ -16,6 +16,7 @@ builder.Services .AddLogging() .AddShopServices() .AddHostedService() + .AddHostedService() .AddHostedService() .AddMidrandShopDatabase(builder.Configuration); diff --git a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json index b710f54..b394b55 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json +++ b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json @@ -1,5 +1,6 @@ { "FeatureManagement": { + "CategorySeederService": true, "CustomerSeederService": false, "ProductsSeederService": false }, diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index 4375d8e..545fe87 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -95,7 +95,7 @@ public sealed class BooksService(IDbContextFactory contex .AsNoTracking() .Include(b => b.Author) .Include(b => b.Product) - .ThenInclude(b => b.Prices) + .ThenInclude(b => b!.Prices) .OrderByDescending(b => b.CreatedAt) .Where(b => b.AuthorId == authorId) .ToListAsync(cancellationToken); From 91ede2d56868e1363f8fbc66f27c5322d0e9b11d Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 18:17:55 +0200 Subject: [PATCH 071/153] Added a way to get the Author by productId --- .../Authors/AuthorService.cs | 23 +++++++++++++++++++ .../Products/ProductService.cs | 8 +++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs index c1aa6b3..3575214 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -8,6 +8,29 @@ namespace LiteCharms.Features.MidrandBooks.Authors; public sealed class AuthorService(IDbContextFactory contextFactory) : IService { + public async ValueTask> GetAuthorByProductIdAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var author = await context.Books + .AsNoTracking() + .Include(i => i.Author) + .Where(b => b.ProductId == productId) + .FirstOrDefaultAsync(cancellationToken); + + if (author is null) + return Result.Fail(new Error($"No author association discovered for Product ID {productId}")); + + return Result.Ok(author.Author!.ToModel()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + public async ValueTask UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken = default) { try diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index a7b47ce..d971623 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -153,7 +153,9 @@ public sealed class ProductService(IDbContextFactory cont { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var query = context.Products.AsQueryable(); + var query = context.Products + .Include(i => i.Price) + .AsQueryable(); var cultureInfo = CultureInfo.InvariantCulture; @@ -304,7 +306,9 @@ public sealed class ProductService(IDbContextFactory cont { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var product = await context.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + var product = await context.Products + .Include(i => i.Price) + .AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); return product is null ? Result.Fail(new Error($"Product with ID {productId} not found.")) From 4f6dbfcd37528cc8035bf0310ae9c10f0bc08b24 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 18:49:15 +0200 Subject: [PATCH 072/153] Redacted Product.Price mapping on filter --- LiteCharms.Features.MidrandBooks/Products/ProductService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index d971623..15be23c 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -153,9 +153,7 @@ public sealed class ProductService(IDbContextFactory cont { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var query = context.Products - .Include(i => i.Price) - .AsQueryable(); + var query = context.Products.AsQueryable(); var cultureInfo = CultureInfo.InvariantCulture; From 0702caa42d1e7e10072a2fbee7dee16a9d93560a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 18:55:23 +0200 Subject: [PATCH 073/153] Redacted Price resolution from GetProductAsync --- LiteCharms.Features.MidrandBooks/Products/ProductService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 15be23c..de96fd8 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -304,8 +304,7 @@ public sealed class ProductService(IDbContextFactory cont { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - var product = await context.Products - .Include(i => i.Price) + var product = await context.Products .AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); return product is null From 494b8067448084286175cec8010c80bddb00d286 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 19:54:14 +0200 Subject: [PATCH 074/153] Added search state model --- LiteCharms.Features/Models/SearchState.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 LiteCharms.Features/Models/SearchState.cs diff --git a/LiteCharms.Features/Models/SearchState.cs b/LiteCharms.Features/Models/SearchState.cs new file mode 100644 index 0000000..6c483cd --- /dev/null +++ b/LiteCharms.Features/Models/SearchState.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.Models; + +public class SearchState +{ + public string Query { get; private set; } = string.Empty; + + public event Action? OnSearchSubmitted; + + public void UpdateQuery(string newQuery) => Query = newQuery; + + public void SubmitSearch() => OnSearchSubmitted?.Invoke(); +} From 0e21ec283d4d6aa879b381a8b13520599e14cce5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 31 May 2026 12:05:59 +0200 Subject: [PATCH 075/153] Added payment database objects --- LiteCharms.Features.MidrandBooks/Enums.cs | 62 - .../Extensions/Mappers.cs | 49 + .../Payments/Entities/Payment.cs | 9 + .../Payments/Entities/PaymentConfiguration.cs | 22 + .../Payments/Entities/PaymentGateway.cs | 4 + .../Entities/PaymentGatewayConfiguration.cs | 20 + .../Payments/Entities/PaymentLedger.cs | 16 + .../Entities/PaymentLedgerConfiguration.cs | 41 + .../Payments/Models/Payment.cs | 18 + .../Payments/Models/PaymentGateway.cs | 24 + .../Payments/Models/PaymentLedger.cs | 20 + .../Postgres/MidrandBooksDbContext.cs | 8 + ...0531094401_AddedPaymentObjects.Designer.cs | 1235 +++++++++++++++++ .../20260531094401_AddedPaymentObjects.cs | 185 +++ .../MidrandBooksDbContextModelSnapshot.cs | 228 +++ .../Products/Entities/ProductInventory.cs | 9 + .../Entities/ProductInventoryConfiguration.cs | 27 + .../Products/Models/ProductInventory.cs | 18 + LiteCharms.Features/Enums.cs | 91 ++ 19 files changed, 2024 insertions(+), 62 deletions(-) delete mode 100644 LiteCharms.Features.MidrandBooks/Enums.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/Payment.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGateway.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/Payment.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventory.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventoryConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/ProductInventory.cs diff --git a/LiteCharms.Features.MidrandBooks/Enums.cs b/LiteCharms.Features.MidrandBooks/Enums.cs deleted file mode 100644 index b3325c2..0000000 --- a/LiteCharms.Features.MidrandBooks/Enums.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace LiteCharms.Features.MidrandBooks; - -public enum PublisherTypes : int -{ - Individual = 0, - Company = 1, - Organization = 2, - SelfPublished = 3, - UniversityPress = 4, - GovernmentAgency = 5, - NonProfit = 6, - Independent = 7 -} - -public enum BookTypes : int -{ - Fiction = 0, - NonFiction = 1, - Academic = 2, - SelfHelp = 3, - Biography = 4, - Poetry = 5, - Children = 6, - YoungAdult = 7, - ScienceFiction = 8, - Fantasy = 9 -} - -public enum BookContentTypes : int -{ - Text = 0, - Image = 1, - Video = 2, - Audio = 3, - Interactive = 4, - Markdown = 5, - Html = 6, - Json = 7, - Yaml = 8 -} - -public enum BookPageTypes : int -{ - Cover = 0, - Preface = 1, - Introduction = 2, - Content = 3, - Closing = 4, - Referencer = 5, - Credits = 6, - BackCover = 7 -} - -public enum ProductTypes : int -{ - Book = 1, - Journal = 2, - Magazine = 3, - EBook = 4, - Audiobook = 5, - Accessory = 6 -} diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 1a81b74..0380370 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -4,12 +4,61 @@ using LiteCharms.Features.MidrandBooks.Categories.Models; using LiteCharms.Features.MidrandBooks.Customers.Models; using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Pages.Models; +using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Products.Models; namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Mappers { + public static PaymentLedger ToModel(this Payments.Entities.PaymentLedger entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CustomerId = entity.CustomerId, + OrderId = entity.OrderId, + PaymentGatewayId = entity.PaymentGatewayId, + PaymentGatewayReference = entity.PaymentGatewayReference, + PaymentId = entity.PaymentId, + Status = entity.Status, + }; + + public static PaymentGateway ToModel(this Payments.Entities.PaymentGateway entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Enabled = entity.Enabled, + IsSandbox = entity.IsSandbox, + MerchantId = entity.MerchantId, + MerchantKey = entity.MerchantKey, + Name = entity.Name, + Passphrase = entity.Passphrase, + Website = entity.Website, + }; + + public static Payment ToModel(this Payments.Entities.Payment entity) => new() + { + Id = entity.Id, + Amount = entity.Amount, + CreatedAt = entity.CreatedAt, + OrderId = entity.OrderId, + Reference = entity.Reference, + Status = entity.Status, + UpdatedAt = entity.UpdatedAt, + }; + + public static ProductInventory ToModel(this Products.Entities.ProductInventory entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + ProductId = entity.ProductId, + ProductPriceId = entity.ProductPriceId, + Status = entity.Status, + TotalAllocated = entity.TotalAllocated, + TotalReserved = entity.TotalReserved, + }; + public static Category ToModel(this Categories.Entities.Category entity) => new() { Id = entity.Id, diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/Payment.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/Payment.cs new file mode 100644 index 0000000..69e5fcb --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/Payment.cs @@ -0,0 +1,9 @@ +using LiteCharms.Features.MidrandBooks.Orders.Entities; + +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class Payment : Models.Payment +{ + public virtual Order? Order { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentConfiguration.cs new file mode 100644 index 0000000..d1f32ef --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentConfiguration.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class PaymentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Payments"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt); + builder.Property(f => f.Status).IsRequired(); + builder.Property(f => f.Reference).IsRequired(); + builder.Property(f => f.OrderId).IsRequired(); + builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); + + builder.HasOne(f => f.Order) + .WithMany() + .HasForeignKey(f => f.OrderId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGateway.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGateway.cs new file mode 100644 index 0000000..cf47bf9 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGateway.cs @@ -0,0 +1,4 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class PaymentGateway : Models.PaymentGateway; diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs new file mode 100644 index 0000000..adfb8a5 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class PaymentGatewayConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Gateways"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt); + builder.Property(f => f.Website).IsRequired(false); + builder.Property(f => f.IsSandbox); + builder.Property(f => f.MerchantKey).IsRequired(); + builder.Property(f => f.MerchantId).IsRequired(); + builder.Property(f => f.Enabled); + builder.Property(f => f.Name).IsRequired(); + builder.Property(f => f.Passphrase).IsRequired(); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs new file mode 100644 index 0000000..3bba78b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs @@ -0,0 +1,16 @@ +using LiteCharms.Features.MidrandBooks.Customers.Entities; +using LiteCharms.Features.MidrandBooks.Orders.Entities; + +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class PaymentLedger : Models.PaymentLedger +{ + public virtual Payment? Payment { get; set; } + + public virtual Order? Order { get; set; } + + public virtual Customer? Customer { get; set; } + + public virtual PaymentGateway? Gateway { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs new file mode 100644 index 0000000..0e51655 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs @@ -0,0 +1,41 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class PaymentLedgerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Ledger"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.Status).IsRequired(); + builder.Property(f => f.PaymentGatewayReference).IsRequired(false); + builder.Property(f => f.PaymentGatewayId).IsRequired(false); + builder.Property(f => f.OrderId).IsRequired(); + builder.Property(f => f.CustomerId).IsRequired(); + builder.Property(f => f.PaymentId).IsRequired(); + + builder.HasOne(f => f.Payment) + .WithMany() + .IsRequired() + .HasForeignKey(f => f.PaymentId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Order) + .WithMany() + .IsRequired() + .HasForeignKey(f => f.OrderId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Customer) + .WithMany() + .IsRequired() + .HasForeignKey(f => f.CustomerId); + + builder.HasOne(f => f.Gateway) + .WithMany() + .IsRequired(false) + .HasForeignKey(f => f.PaymentGatewayId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/Payment.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Payment.cs new file mode 100644 index 0000000..61c22eb --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Payment.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class Payment +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public decimal Amount { get; set; } + + public long OrderId { get; set; } + + public string? Reference { get; set; } + + public PaymentStatuses Status { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs new file mode 100644 index 0000000..bdb8a69 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs @@ -0,0 +1,24 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class PaymentGateway +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Name { get; set; } + + public string? Website { get; set; } + + public string? MerchantId { get; set; } + + public string? MerchantKey { get; set; } + + public string? Passphrase { get; set; } + + public bool IsSandbox { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs new file mode 100644 index 0000000..33f3d25 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class PaymentLedger +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public LedgerStatuses Status { get; set; } + + public long OrderId { get; set; } + + public long PaymentId { get; set; } + + public long CustomerId { get; set; } + + public string? PaymentGatewayReference { get; set; } + + public long? PaymentGatewayId { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index 782bb1f..3d0b380 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -40,4 +40,12 @@ public sealed class MidrandBooksDbContext(DbContextOptions Categories => Set(); public DbSet ProductCategories => Set(); + + public DbSet Inventories => Set(); + + public DbSet Payments => Set(); + + public DbSet Gateways => Set(); + + public DbSet Ledger => Set(); } diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.Designer.cs new file mode 100644 index 0000000..9bdf38e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.Designer.cs @@ -0,0 +1,1235 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260531094401_AddedPaymentObjects")] + partial class AddedPaymentObjects + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Passphrase") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayReference") + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentGatewayId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", "Gateway") + .WithMany() + .HasForeignKey("PaymentGatewayId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Gateway"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.cs new file mode 100644 index 0000000..b485cd1 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.cs @@ -0,0 +1,185 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedPaymentObjects : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Gateways", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Name = table.Column(type: "text", nullable: false), + Website = table.Column(type: "text", nullable: true), + MerchantId = table.Column(type: "text", nullable: false), + MerchantKey = table.Column(type: "text", nullable: false), + Passphrase = table.Column(type: "text", nullable: false), + IsSandbox = table.Column(type: "boolean", nullable: false), + Enabled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Gateways", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Inventories", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Status = table.Column(type: "integer", nullable: false), + ProductId = table.Column(type: "bigint", nullable: false), + ProductPriceId = table.Column(type: "bigint", nullable: false), + TotalAllocated = table.Column(type: "integer", nullable: false), + TotalReserved = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Inventories", x => x.Id); + table.ForeignKey( + name: "FK_Inventories_Prices_ProductPriceId", + column: x => x.ProductPriceId, + principalTable: "Prices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Inventories_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Payments", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + OrderId = table.Column(type: "bigint", nullable: false), + Reference = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Payments", x => x.Id); + table.ForeignKey( + name: "FK_Payments_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Ledger", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Status = table.Column(type: "integer", nullable: false), + OrderId = table.Column(type: "bigint", nullable: false), + PaymentId = table.Column(type: "bigint", nullable: false), + CustomerId = table.Column(type: "bigint", nullable: false), + PaymentGatewayReference = table.Column(type: "text", nullable: true), + PaymentGatewayId = table.Column(type: "bigint", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Ledger", x => x.Id); + table.ForeignKey( + name: "FK_Ledger_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Ledger_Gateways_PaymentGatewayId", + column: x => x.PaymentGatewayId, + principalTable: "Gateways", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Ledger_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Ledger_Payments_PaymentId", + column: x => x.PaymentId, + principalTable: "Payments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Inventories_ProductId", + table: "Inventories", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Inventories_ProductPriceId", + table: "Inventories", + column: "ProductPriceId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_CustomerId", + table: "Ledger", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_OrderId", + table: "Ledger", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_PaymentGatewayId", + table: "Ledger", + column: "PaymentGatewayId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_PaymentId", + table: "Ledger", + column: "PaymentId"); + + migrationBuilder.CreateIndex( + name: "IX_Payments_OrderId", + table: "Payments", + column: "OrderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Inventories"); + + migrationBuilder.DropTable( + name: "Ledger"); + + migrationBuilder.DropTable( + name: "Gateways"); + + migrationBuilder.DropTable( + name: "Payments"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 8af6b80..635ea90 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -536,6 +536,133 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.ToTable("BookPages", (string)null); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Passphrase") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayReference") + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentGatewayId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => { b.Property("Id") @@ -653,6 +780,43 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.ToTable("ProductCategories", (string)null); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.Property("Id") @@ -891,6 +1055,51 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("References"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", "Gateway") + .WithMany() + .HasForeignKey("PaymentGatewayId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Gateway"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") @@ -950,6 +1159,25 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Product"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventory.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventory.cs new file mode 100644 index 0000000..7b43c55 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventory.cs @@ -0,0 +1,9 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +[EntityTypeConfiguration] +public class ProductInventory : Models.ProductInventory +{ + public virtual Product? Product { get; set; } + + public virtual ProductPrice? Price { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventoryConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventoryConfiguration.cs new file mode 100644 index 0000000..4e68570 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventoryConfiguration.cs @@ -0,0 +1,27 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public sealed class ProductInventoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Inventories"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.Status).IsRequired(); + builder.Property(f => f.TotalAllocated).IsRequired(); + builder.Property(f => f.TotalReserved).IsRequired(); + builder.Property(f => f.ProductId).IsRequired(); + builder.Property(f => f.ProductPriceId).IsRequired(); + + builder.HasOne(f => f.Product) + .WithMany() + .HasForeignKey(f => f.ProductId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Price) + .WithMany() + .HasForeignKey(f => f.ProductPriceId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/ProductInventory.cs b/LiteCharms.Features.MidrandBooks/Products/Models/ProductInventory.cs new file mode 100644 index 0000000..3212a0e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/ProductInventory.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class ProductInventory +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public InventoryStatuses Status { get; set; } + + public long ProductId { get; set; } + + public long ProductPriceId { get; set; } + + public int TotalAllocated { get; set; } + + public int TotalReserved { get; set; } +} diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs index 40aabdc..786d3e5 100644 --- a/LiteCharms.Features/Enums.cs +++ b/LiteCharms.Features/Enums.cs @@ -1,5 +1,35 @@ namespace LiteCharms.Features; +public enum InventoryStatuses : int +{ + Adjustment = 0, + Reserved = 1, + Released = 2, + Sold = 3, + Replenished = 4, + Correction = 5, +} + +public enum LedgerStatuses : int +{ + Changed = 0, + Sent = 1, + Received = 2, + Refunded = 3, + Cancelled = 4, + Failed = 5, + Partial = 6, +} + +public enum PaymentStatuses : int +{ + NotPaid = 0, + Paid = 1, + Cancelled = 2, + Requested = 3, + Failed = 4, +} + public enum ShippingProviderTypes : int { Dsv = 0, @@ -114,4 +144,65 @@ public enum Priorities : int Low = 0, Medium = 1, High = 2, +} + +public enum PublisherTypes : int +{ + Individual = 0, + Company = 1, + Organization = 2, + SelfPublished = 3, + UniversityPress = 4, + GovernmentAgency = 5, + NonProfit = 6, + Independent = 7 +} + +public enum BookTypes : int +{ + Fiction = 0, + NonFiction = 1, + Academic = 2, + SelfHelp = 3, + Biography = 4, + Poetry = 5, + Children = 6, + YoungAdult = 7, + ScienceFiction = 8, + Fantasy = 9 +} + +public enum BookContentTypes : int +{ + Text = 0, + Image = 1, + Video = 2, + Audio = 3, + Interactive = 4, + Markdown = 5, + Html = 6, + Json = 7, + Yaml = 8 +} + +public enum BookPageTypes : int +{ + Cover = 0, + Preface = 1, + Introduction = 2, + Content = 3, + Closing = 4, + Referencer = 5, + Credits = 6, + BackCover = 7 +} + +public enum ProductTypes : int +{ + Book = 1, + Journal = 2, + Magazine = 3, + EBook = 4, + Audiobook = 5, + Accessory = 6 } \ No newline at end of file From f88cc42a88a440b606a73a460fe8a09f81cf7da6 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 31 May 2026 18:42:00 +0200 Subject: [PATCH 076/153] Completed payment service implementation --- .../PaymentServiceFeatureTests.cs | 98 +++++++ .../ProductServiceFeatureTests.cs | 41 +++ .../Extensions/Mappers.cs | 13 + .../Payments/Models/Records.cs | 53 ++++ .../Payments/PaymentService.cs | 260 +++++++++++++++++- .../Products/Models/Records.cs | 18 ++ .../Products/ProductService.cs | 118 ++++++++ 7 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs diff --git a/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs new file mode 100644 index 0000000..3681116 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs @@ -0,0 +1,98 @@ +using LiteCharms.Features.MidrandBooks.Payments; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.MidrandBooks.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public sealed class PaymentServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly PaymentService paymentService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateRefundAsync_ShouldReturn_ResultWithRefundId() + { + var request = new CreateRefund + { + Amount = 50, + OrderId = 2, + Type = RefundTypes.Partial, + Reason = "Returned damaged book", + Status = RefundStatus.Completed, + }; + + var result = await paymentService.CreateRefundAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task WriteLedgerEntryAsync_ShouldReturn_ResultWithSuccess() + { + var request = new CreateLedgerEntry + { + CustomerId = 1, + OrderId = 1, + PaymentGatewayId = 1, + PaymentGatewayReference = "TEST REFERENCE", + PaymentId = 1, + Status = LedgerStatuses.Received, + }; + + var result = await paymentService.WriteLedgerEntryAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetPaymentGatewayAsync_ShouldReturn_ResultWithPaymentGateway() + { + var result = await paymentService.GetPaymentGatewayAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task CreatePaymentGatewayAsync_ShouldReturn_ResultWithGatewayId() + { + var request = new CreatePaymentGateway + { + IsSandbox = true, + MerchantId = "10049307", + MerchantKey = "ju6navn0jcbf0", + Name = "Payfast", + Website = "https://sandbox.payfast.co.za/eng/process", + }; + + var result = await paymentService.CreatePaymentGatewayAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task CompletePaymentAsync_ShouldReturn_ResultWithSuccess() + { + var result = await paymentService.CompletePaymentAsync(1, PaymentStatuses.Paid, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdatePaymentAsync_ShouldReturn_ResultWithSuccess() + { + var result = await paymentService.UpdatePaymentAsync(1, 200, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreatePaymentAsync_ShouldReturn_ResultWithPaymentId() + { + var result = await paymentService.CreatePaymentAsync(100, 1, "HASHEDID", fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs index d4e6e4c..827025f 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs @@ -9,6 +9,47 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu { private readonly ProductService productService = fixture.Services.GetRequiredService(); + [IntegrationFact] + public async Task CheckProductStockAvailabilityAsync_ShouldReturn_ResultWithProductInventory() + { + var result = await productService.CheckProductStockAvailabilityAsync(1, 1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task ReserveProductInventoryAsync_ShouldReturn_ResultWithSuccess() + { + var request = new ReserveStock + { + ProductId = 1, + ProductPriceId = 1, + Reservation = 100, + }; + + var result = await productService.ReserveProductInventoryAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task AllocateProductInventoryAsync_ShouldReturn_ResultWithSuccess() + { + var request = new AllocateStock + { + ProductId = 1, + ProductPriceId = 1, + Allocation = 500, + }; + + var result = await productService.AllocateProductInventoryAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + [IntegrationFact] public async Task AddProductCategoryAsync_ShouldReturn_ResultWithId() { diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 0380370..e8a8612 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -6,11 +6,24 @@ using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Products.Models; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Mappers { + public static Refund ToModel(this Payments.Entities.Refund entity) => new() + { + CreatedAt = entity.CreatedAt, + Amount = entity.Amount, + Id = entity.Id, + OrderId = entity.OrderId, + Reason = entity.Reason, + Status = entity.Status, + Type = entity.Type, + UpdatedAt = entity.UpdatedAt, + }; + public static PaymentLedger ToModel(this Payments.Entities.PaymentLedger entity) => new() { Id = entity.Id, diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs new file mode 100644 index 0000000..2326397 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs @@ -0,0 +1,53 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public sealed record UpdateRefund +{ + public long OrderId { get; set; } + + public RefundStatus Status { get; set; } + + public string? Reason { get; set; } + + public decimal Amount { get; set; } +}; + +public sealed record CreateRefund +{ + public long OrderId { get; set; } + + public RefundTypes Type { get; set; } + + public RefundStatus Status { get; set; } + + public string? Reason { get; set; } + + public decimal Amount { get; set; } +} + +public sealed record CreateLedgerEntry +{ + public required LedgerStatuses Status { get; set; } + + public required long OrderId { get; set; } + + public required long PaymentId { get; set; } + + public required long CustomerId { get; set; } + + public string? PaymentGatewayReference { get; set; } + + public long? PaymentGatewayId { get; set; } +} + +public sealed record CreatePaymentGateway +{ + public required string? Name { get; set; } + + public string? Website { get; set; } + + public required string? MerchantId { get; set; } + + public required string? MerchantKey { get; set; } + + public bool IsSandbox { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs index ce3668a..5570fb8 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -1,7 +1,265 @@ using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.Payments; -public sealed class PaymentService : IService +public sealed class PaymentService(IDbContextFactory contextFactory) : IService { + public async ValueTask> GetRefundAsync(long refundId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var refund = await context.Refunds.AsNoTracking() + .FirstOrDefaultAsync(r => r.Id == refundId, cancellationToken); + + return refund is not null + ? Result.Ok(refund.ToModel()) + : Result.Fail("Could not find refund"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateRefundAsync(long refundId, UpdateRefund request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) + return Result.Fail("Order not found"); + + var updatedRows = await context.Refunds + .Where(r => r.Id == refundId && r.OrderId == request.OrderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(r => r.Status, request.Status) + .SetProperty(r => r.Reason, request.Reason) + .SetProperty(r => r.UpdatedAt, DateTime.UtcNow) + .SetProperty(r => r.Amount, request.Amount), cancellationToken); + + return updatedRows > 0 + ? Result.Ok() + : Result.Fail("Failed to update refund"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateRefundAsync(CreateRefund request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == request.OrderId + && o.Status == OrderStatus.Completed, cancellationToken); + + if (order is null) return Result.Fail("Order not found"); + + if (request.Amount > order.Total) + return Result.Fail("Refund amount cannot be greater than order total"); + + var totalRefundsPaid = await context.Refunds + .Where(r => r.OrderId == request.OrderId) + .SumAsync(r => r.Amount, cancellationToken); + + if (request.Amount > (order.Total - totalRefundsPaid)) + return Result.Fail("Refund amount exceeds amount available for refund"); + + var refund = context.Refunds.Add(new Entities.Refund + { + Amount = request.Amount, + CreatedAt = DateTime.UtcNow, + OrderId = request.OrderId, + Reason = request.Reason, + Status = request.Status, + Type = request.Type, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(refund.Entity.Id) + : Result.Fail("Failed to create refund"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask WriteLedgerEntryAsync(CreateLedgerEntry request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) + return Result.Fail("Order not found"); + + if (!await context.Customers.AnyAsync(o => o.Id == request.CustomerId, cancellationToken)) + return Result.Fail("Customer not found"); + + if (!await context.Orders.AnyAsync(oc => oc.Id == request.OrderId && oc.CustomerId == request.CustomerId, cancellationToken)) + return Result.Fail("Customer does not match the order"); + + if (!await context.Payments.AnyAsync(o => o.Id == request.PaymentId && o.OrderId == request.OrderId, cancellationToken)) + return Result.Fail("Payment not found"); + + if (request.PaymentGatewayId is not null) + if (!await context.Gateways.AnyAsync(o => o.Id == request.PaymentGatewayId, cancellationToken)) + return Result.Fail("Gateway not found"); + + context.Ledger.Add(new Entities.PaymentLedger + { + CreatedAt = DateTime.UtcNow, + CustomerId = request.CustomerId, + OrderId = request.OrderId, + PaymentGatewayId = request.PaymentGatewayId, + PaymentGatewayReference = request.PaymentGatewayReference, + PaymentId = request.PaymentId, + Status = request.Status, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to create ledger entry"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPaymentGatewayAsync(long paymentGatewayId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var gateway = await context.Gateways.AsNoTracking().FirstOrDefaultAsync(g => g.Id == paymentGatewayId, cancellationToken); + + return gateway is not null + ? Result.Ok(gateway.ToModel()) + : Result.Fail("Could not find gateway"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreatePaymentGatewayAsync(CreatePaymentGateway request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Gateways.AnyAsync(g => g.MerchantId == request.MerchantId && g.MerchantKey == request.MerchantKey, cancellationToken)) + return Result.Fail("A gateway with the same credentials already exists"); + + var gateway = context.Gateways.Add(new Entities.PaymentGateway + { + CreatedAt = DateTime.UtcNow, + Enabled = true, + IsSandbox = request.IsSandbox, + MerchantId = request.MerchantId, + MerchantKey = request.MerchantKey, + Name = request.Name, + Website = request.Website, + Passphrase = "N/A", + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(gateway.Entity.Id) + : Result.Fail("Failed to create payment gateway"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask CompletePaymentAsync(long paymentId, PaymentStatuses status, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (status == PaymentStatuses.NotPaid) + return Result.Fail("Cannot finalise a payment using NotPaid status"); + + var updatedRecords = await context.Payments + .Where(p => p.Id == paymentId && p.Status != PaymentStatuses.Paid && p.Status != status) + .ExecuteUpdateAsync(setters => setters + .SetProperty(u => u.Status, status) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return updatedRecords > 0 + ? Result.Ok() + : Result.Fail("Failed to update payment"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdatePaymentAsync(long paymentId, decimal amount, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var updatedRecords = await context.Payments + .Where(p => p.Id == paymentId && p.Status == PaymentStatuses.NotPaid) + .ExecuteUpdateAsync(setters => setters + .SetProperty(u => u.Amount, amount) + .SetProperty(u => u.Status, PaymentStatuses.NotPaid) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return updatedRecords > 0 + ? Result.Ok() + : Result.Fail("Failed to update payment"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreatePaymentAsync(decimal amount, long orderId, string reference, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Payments.AnyAsync(p => p.OrderId == orderId && p.Amount == amount && p.Status != PaymentStatuses.Paid, cancellationToken)) + return Result.Fail("An order with the same amount already exists in the system"); + + var payment = context.Payments.Add(new Entities.Payment + { + CreatedAt = DateTime.UtcNow, + Amount = amount, + OrderId = orderId, + Reference = reference, + Status = PaymentStatuses.NotPaid, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(payment.Entity.Id) + : Result.Fail("Failed to make payment"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } } diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs index 9c09551..0a5a588 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs @@ -2,6 +2,24 @@ namespace LiteCharms.Features.MidrandBooks.Products.Models; +public sealed record ReserveStock +{ + public required long ProductId { get; set; } + + public required long ProductPriceId { get; set; } + + public int Reservation { get; set; } +} + +public sealed record AllocateStock +{ + public required long ProductId { get; set; } + + public required long ProductPriceId { get; set; } + + public int Allocation { get; set; } +} + public sealed record CreateProduct { public required ProductTypes Type { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index de96fd8..1682a70 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -4,11 +4,129 @@ using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.Models; +using Org.BouncyCastle.Asn1.Ocsp; namespace LiteCharms.Features.MidrandBooks.Products; public sealed class ProductService(IDbContextFactory contextFactory) : IService { + public async ValueTask> CheckProductStockAvailabilityAsync(long productId, long productPriceId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var inventory = await context.Inventories + .AsNoTracking() + .Where(i => i.ProductPriceId == productPriceId && i.ProductId == productId) + .OrderByDescending(o => o.Id) + .FirstOrDefaultAsync(cancellationToken); + + return inventory is not null + ? Result.Ok(inventory.ToModel()) + : Result.Fail("Product sold out"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> ReserveProductInventoryAsync(ReserveStock request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var oldInventory = await context.Inventories + .AsNoTracking() + .Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId) + .OrderByDescending(o => o.Id) + .FirstOrDefaultAsync(cancellationToken); + + var newAllocation = 0; + var newReservation = 0; + + if (oldInventory is not null) + { + newAllocation = oldInventory.TotalAllocated; + newReservation = oldInventory.TotalReserved + request.Reservation; + } + else + { + newAllocation = 0; + newReservation = request.Reservation; + } + + if (newAllocation - newReservation < 0) + return Result.Fail("Allocation failure: The requested book quantity exceeds current physical inventory availability."); + + var inventory = context.Inventories.Add(new Entities.ProductInventory + { + CreatedAt = DateTime.UtcNow, + ProductId = request.ProductId, + ProductPriceId = request.ProductPriceId, + Status = InventoryStatuses.Reserved, + TotalAllocated = newAllocation, + TotalReserved = newReservation, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(inventory.Entity.Id) + : Result.Fail("Failed to create inventory entry"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> AllocateProductInventoryAsync(AllocateStock request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var oldInventory = await context.Inventories + .AsNoTracking() + .Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId) + .OrderByDescending(o => o.Id) + .FirstOrDefaultAsync(cancellationToken); + + var newAllocation = 0; + var newReservation = 0; + + if (oldInventory is not null) + { + newAllocation = oldInventory.TotalAllocated + request.Allocation; + newReservation = oldInventory.TotalReserved; + } + else + { + newAllocation = request.Allocation; + newReservation = 0; + } + + var inventory = context.Inventories.Add(new Entities.ProductInventory + { + CreatedAt = DateTime.UtcNow, + ProductId = request.ProductId, + ProductPriceId = request.ProductPriceId, + Status = InventoryStatuses.Adjustment, + TotalAllocated = newAllocation, + TotalReserved = newReservation, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(inventory.Entity.Id) + : Result.Fail("Failed to create inventory entry"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + public async ValueTask AddProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default) { try From 48f4cd45f14480cc3b794d47ff1f18adfa9c0ba5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 31 May 2026 19:37:19 +0200 Subject: [PATCH 077/153] Implemented the HashService and its service registration code --- .../Abstractions/IService.cs | 3 - .../AuthorBooks/BooksService.cs | 2 +- .../Authors/AuthorService.cs | 2 +- .../Categories/CategoryService.cs | 2 +- .../Customers/CustomerService.cs | 2 +- .../Extensions/Shop.cs | 2 +- .../Orders/OrderService.cs | 2 +- .../Pages/PageService.cs | 2 +- .../Payments/PaymentService.cs | 2 +- .../Products/ProductService.cs | 2 +- LiteCharms.Features/Abstractions/IService.cs | 3 + LiteCharms.Features/Extensions/Hash.cs | 26 ++-- .../Hasher/Configuration/HasherSettings.cs | 10 ++ LiteCharms.Features/Hasher/HashService.cs | 118 ++++++++++++++++++ .../LiteCharms.Features.csproj | 2 + .../S3/Abstractions/S3ServiceBase.cs | 6 +- 16 files changed, 163 insertions(+), 23 deletions(-) delete mode 100644 LiteCharms.Features.MidrandBooks/Abstractions/IService.cs create mode 100644 LiteCharms.Features/Abstractions/IService.cs create mode 100644 LiteCharms.Features/Hasher/Configuration/HasherSettings.cs create mode 100644 LiteCharms.Features/Hasher/HashService.cs diff --git a/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs b/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs deleted file mode 100644 index 6218faf..0000000 --- a/LiteCharms.Features.MidrandBooks/Abstractions/IService.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace LiteCharms.Features.MidrandBooks.Abstractions; - -public interface IService; diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index 545fe87..2ee001e 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs index 3575214..0b30a85 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs index 2a5b9f4..411f163 100644 --- a/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs +++ b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Categories.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs index 03f34c0..5350220 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Customers.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs index f407c5b..fddb438 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; namespace LiteCharms.Features.MidrandBooks.Extensions; diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs index 290908b..4ea6252 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs index 26569bc..22db7b7 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs index 5570fb8..3dd26dc 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 1682a70..771632b 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -1,4 +1,4 @@ -using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Categories.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; diff --git a/LiteCharms.Features/Abstractions/IService.cs b/LiteCharms.Features/Abstractions/IService.cs new file mode 100644 index 0000000..17ec5e0 --- /dev/null +++ b/LiteCharms.Features/Abstractions/IService.cs @@ -0,0 +1,3 @@ +namespace LiteCharms.Features.Abstractions; + +public interface IService; diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs index ca24629..6aa95ed 100644 --- a/LiteCharms.Features/Extensions/Hash.cs +++ b/LiteCharms.Features/Extensions/Hash.cs @@ -1,13 +1,23 @@ -namespace LiteCharms.Features.Extensions; +using LiteCharms.Features.Hasher; +using LiteCharms.Features.Hasher.Configuration; + +namespace LiteCharms.Features.Extensions; public static class Hash { - public static readonly Func StringToSha256Hash = (input) => - Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!))); + public const string HasherConfigSectionName = "HasherSettings"; - public static readonly Func StreamToSha256Hash = (stream) => - Convert.ToHexString(SHA256.HashData(stream)); + public static IServiceCollection AddHashServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection(HasherConfigSectionName)); - public static readonly Func BytesToSha256Hash = (bytes) => - Convert.ToHexString(SHA256.HashData(bytes)); -} + var settings = configuration.GetSection(HasherConfigSectionName).Get(); + + services.AddSingleton(_ => + new Hashids(settings!.Salt, minHashLength: settings.MinHashLength)); + + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/LiteCharms.Features/Hasher/Configuration/HasherSettings.cs b/LiteCharms.Features/Hasher/Configuration/HasherSettings.cs new file mode 100644 index 0000000..e30fb40 --- /dev/null +++ b/LiteCharms.Features/Hasher/Configuration/HasherSettings.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.Hasher.Configuration; + +public sealed class HasherSettings +{ + public string? Salt { get; set; } + + public int MinHashLength { get; set; } + + public string? PayfastPassphrase { get; set; } +} diff --git a/LiteCharms.Features/Hasher/HashService.cs b/LiteCharms.Features/Hasher/HashService.cs new file mode 100644 index 0000000..853ccb8 --- /dev/null +++ b/LiteCharms.Features/Hasher/HashService.cs @@ -0,0 +1,118 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Hasher.Configuration; + +namespace LiteCharms.Features.Hasher; + +public sealed partial class HashService(IHashids hasher, IOptions options) : IService +{ + private readonly HasherSettings settings = options.Value; + + [System.Text.RegularExpressions.GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")] + private static partial System.Text.RegularExpressions.Regex HexHashRegex(); + + public static readonly Func StringToSha256Hash = (input) => + string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))); + + public static readonly Func StreamToSha256Hash = (stream) => + stream is null ? null : Convert.ToHexString(SHA256.HashData(stream)); + + public static readonly Func BytesToSha256Hash = (bytes) => + bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes)); + + public static Result ComputeMd5Hash(string input) + { + if (string.IsNullOrEmpty(input)) + return Result.Fail("Input content cannot be null or empty for MD5 processing."); + + byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input)); + return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant()); + } + + public Result VerifyPayfastWebhookSignature(IDictionary incomingFormData, string incomingSignature) + { + try + { + if (string.IsNullOrWhiteSpace(incomingSignature)) + return Result.Fail("Validation failed: Missing signature string parameter."); + + // 1. Sort the parameters alphabetically and exclude the signature parameter to prevent recursive checking + var sortedFields = incomingFormData + .Where(field => field.Key != "signature") + .OrderBy(field => field.Key) + .Select(field => $"{field.Key}={Uri.EscapeDataString(field.Value).Replace("%20", "+")}"); + + string payload = string.Join("&", sortedFields); + + // 2. Append the secure, passphrase injected into the container pod from your environment variables + if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase)) + { + payload += $"&passphrase={Uri.EscapeDataString(settings.PayfastPassphrase).Replace("%20", "+")}"; + } + + // 3. Compute localized hex token + var localHashResult = ComputeMd5Hash(payload); + + if (!localHashResult.IsSuccess) + return Result.Fail(localHashResult.Errors); + + // 4. Constant-time secure text comparison to fully block timing analysis attacks + bool isValid = string.Equals(localHashResult.Value, incomingSignature, StringComparison.OrdinalIgnoreCase); + + return Result.Ok(isValid); + } + catch (Exception ex) + { + return Result.Fail(new Error("An error occurred during MD5 verification loop.").CausedBy(ex)); + } + } + + public Result HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex().IsMatch(input) + ? Result.Fail("Input must be a valid hexadecimal string.") + : Result.Ok(hasher.EncodeHex(input)); + + public Result HashEncodeIntId(int id) => id < 0 + ? Result.Fail("Id cannot be negative.") + : Result.Ok(hasher.Encode(id)); + + public Result HashEncodeLongId(long id) => id < 0 + ? Result.Fail("Id cannot be negative.") + : Result.Ok(hasher.EncodeLong(id)); + + public Result DecodeIntIdHash(string hash) + { + if (string.IsNullOrWhiteSpace(hash)) return Result.Fail("Invalid token layout."); + + int[] decoded = hasher.Decode(hash); + + return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail("Invalid or modified Int hash token."); + } + + public Result DecodeLongIdHash(string hash) + { + if (string.IsNullOrWhiteSpace(hash)) return Result.Fail("Invalid token layout."); + + long[] decoded = hasher.DecodeLong(hash); + + return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail("Invalid or modified Long hash token."); + } + + public Result DecodeHexHash(string hex) + { + try + { + string decoded = hasher.DecodeHex(hex); + + return string.IsNullOrEmpty(decoded) + ? Result.Fail("Invalid or corrupted hex hash.") + : Result.Ok(decoded); + } + catch (FormatException fex) + { + return Result.Fail(new Error("Invalid hash structure.").CausedBy(fex)); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} \ No newline at end of file diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 552ee0a..6079860 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -31,6 +31,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -146,6 +147,7 @@ + diff --git a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs index c1b9ae4..3fcb5d8 100644 --- a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs +++ b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs @@ -1,4 +1,4 @@ -using static LiteCharms.Features.Extensions.Hash; +using LiteCharms.Features.Hasher; namespace LiteCharms.Features.S3.Abstractions; @@ -26,7 +26,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3) stream.Seek(0, SeekOrigin.Begin); - var fileHash = StreamToSha256Hash(stream); + var fileHash = HashService.StreamToSha256Hash(stream); if(string.IsNullOrWhiteSpace(fileHash)) return Result.Fail("Failed to compute file hash."); @@ -39,7 +39,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3) Key = fileKey, InputStream = stream, ContentType = contentType, - UseChunkEncoding = false + UseChunkEncoding = false, }; stream.Seek(0, SeekOrigin.Begin); From c4f73fd999bf32d5e1d8712038a9f4b1ec6daec5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 31 May 2026 19:38:03 +0200 Subject: [PATCH 078/153] Removed comments from function blocks --- LiteCharms.Features/Hasher/HashService.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/LiteCharms.Features/Hasher/HashService.cs b/LiteCharms.Features/Hasher/HashService.cs index 853ccb8..fc18e38 100644 --- a/LiteCharms.Features/Hasher/HashService.cs +++ b/LiteCharms.Features/Hasher/HashService.cs @@ -35,7 +35,6 @@ public sealed partial class HashService(IHashids hasher, IOptions("Validation failed: Missing signature string parameter."); - // 1. Sort the parameters alphabetically and exclude the signature parameter to prevent recursive checking var sortedFields = incomingFormData .Where(field => field.Key != "signature") .OrderBy(field => field.Key) @@ -43,19 +42,14 @@ public sealed partial class HashService(IHashids hasher, IOptions(localHashResult.Errors); - // 4. Constant-time secure text comparison to fully block timing analysis attacks bool isValid = string.Equals(localHashResult.Value, incomingSignature, StringComparison.OrdinalIgnoreCase); return Result.Ok(isValid); From 8fe129e19ce5aba964b9c380055aa6acc1b9de5f Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 1 Jun 2026 09:15:14 +0200 Subject: [PATCH 079/153] Implemented HashService and tests --- .../CategorySeederService.cs | 2 +- .../Configuration/CdnSettings.cs | 2 +- .../CustomerSeederService.cs | 2 +- .../ProductsSeederService.cs | 2 +- .../AuthorServiceFeatureTests.cs | 2 +- .../Common/Fixture.cs | 1 - .../Leads/LeadService.cs | 5 +- LiteCharms.Features.Tests/Fixture.cs | 1 + .../HashServiceFeatureTests.cs | 152 ++++++++++++++++++ .../LiteCharms.Features.Tests.csproj | 2 + LiteCharms.Features/Hasher/HashService.cs | 46 ++++-- .../LiteCharms.Features.csproj | 2 + LiteCharms.Features/Models/SearchState.cs | 12 -- 13 files changed, 193 insertions(+), 38 deletions(-) create mode 100644 LiteCharms.Features.Tests/HashServiceFeatureTests.cs delete mode 100644 LiteCharms.Features/Models/SearchState.cs diff --git a/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs index 5856761..c4a2aae 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs @@ -3,7 +3,7 @@ using LiteCharms.Features.MidrandBooks.Products; namespace LiteCharms.Features.MidrandBooks.Seed; -public class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features, +public sealed class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features, ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) diff --git a/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs index 6eb1107..7b3a904 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs @@ -1,6 +1,6 @@ namespace LiteCharms.Features.MidrandBooks.Seed.Configuration; -public class CdnSettings +public sealed class CdnSettings { public string? BaseCdn { get; set; } diff --git a/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs index 6814a6d..0a22738 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Orders.Models; namespace LiteCharms.Features.MidrandBooks.Seed; -public class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features, +public sealed class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features, ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) diff --git a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs index 868a454..cb96bfe 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration; namespace LiteCharms.Features.MidrandBooks.Seed; -public class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService, +public sealed class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService, IFeatureManager features, IOptions options, ILogger logger) : BackgroundService { private readonly CdnSettings cdnSettings = options.Value; diff --git a/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs index 1a0826a..d74aac1 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs @@ -5,7 +5,7 @@ using LiteCharms.Features.Models; namespace LiteCharms.Features.MidrandBooks.Tests; -public class AuthorServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture +public class AuthorServiceFeatureTests(Fixture fixture) : IClassFixture { private readonly AuthorService authorService = fixture.Services.GetRequiredService(); diff --git a/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs index 6948d48..dae42e0 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs @@ -1,5 +1,4 @@ using LiteCharms.Features.Extensions; -using LiteCharms.Features.MidrandBooks.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; namespace LiteCharms.Features.MidrandBooks.Tests.Common; diff --git a/LiteCharms.Features.TechShop/Leads/LeadService.cs b/LiteCharms.Features.TechShop/Leads/LeadService.cs index 79c9858..4469e03 100644 --- a/LiteCharms.Features.TechShop/Leads/LeadService.cs +++ b/LiteCharms.Features.TechShop/Leads/LeadService.cs @@ -1,9 +1,8 @@ -using LiteCharms.Features.Extensions; +using LiteCharms.Features.Hasher; using LiteCharms.Features.Models; using LiteCharms.Features.TechShop.Extensions; using LiteCharms.Features.TechShop.Leads.Models; using LiteCharms.Features.TechShop.Postgres; -using static LiteCharms.Features.Extensions.Hash; namespace LiteCharms.Features.TechShop.Leads; @@ -29,7 +28,7 @@ public class LeadService(IDbContextFactory contextFactory) FeedItemId = request.FeedItemId, Status = LeadStatus.New, TargetId = request.TargetId, - AttributionHash = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}") + AttributionHash = HashService.StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}") }); return await context.SaveChangesAsync(cancellationToken) > 0 diff --git a/LiteCharms.Features.Tests/Fixture.cs b/LiteCharms.Features.Tests/Fixture.cs index 466d0a0..1ad8e4a 100644 --- a/LiteCharms.Features.Tests/Fixture.cs +++ b/LiteCharms.Features.Tests/Fixture.cs @@ -26,6 +26,7 @@ public class Fixture : IDisposable .AddGarageS3(Configuration) .AddEmailServices(Configuration) .AddSingleton(Configuration) + .AddHashServices(Configuration) .BuildServiceProvider(); Mediator = Services.GetRequiredService(); diff --git a/LiteCharms.Features.Tests/HashServiceFeatureTests.cs b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs new file mode 100644 index 0000000..1515107 --- /dev/null +++ b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs @@ -0,0 +1,152 @@ +using LiteCharms.Features.Hasher; + +namespace LiteCharms.Features.Tests; + +public class HashServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly HashService hashService = fixture.Services.GetRequiredService(); + private readonly string payfastPassphrase = fixture.Configuration.GetSection("HasherSettings:PayfastPassphrase").Value!; + + [Fact] + public void StringToSha256Hash_Should_GenerateHash() + { + var input = "We are the best"; + var expectedHash = "96E17275B53F6BEB7A0D1C4F789F226D3C71CBE398585F25B3028F2B432E78AB"; + + var result = HashService.StringToSha256Hash(input); + + Assert.NotNull(result); + Assert.True(HashService.IsSha256Hash(result)); + Assert.Equal(expectedHash, result); + } + + [Fact] + public void StreamToSha256Hash_Should_GenerateHash() + { + var input = "We are successful"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(input)); + var expectedHash = "C27872EE494B09D72203C98FC858268F3CD3492D62AA7B766A873520C2C73AFB"; + + var result = HashService.StreamToSha256Hash(stream); + + Assert.NotNull(result); + Assert.True(HashService.IsSha256Hash(result)); + Assert.Equal(expectedHash, result); + } + + [Fact] + public void BytesToSha256Hash_Should_GenerateHash() + { + var inputBytes = Encoding.UTF8.GetBytes("We are wealthy"); + var expectedHash = "3876BF98F6E4A8E42B22C40415687D6FF13F0E887F3F508B71852298FC665737"; + + var result = HashService.BytesToSha256Hash(inputBytes); + + Assert.NotNull(result); + Assert.True(HashService.IsSha256Hash(result)); + Assert.Equal(expectedHash, result); + } + + [Fact] + public void ToMd5Hash_Should_GenerateHash() + { + var input = "We manifest our desired destiny"; + var expectedMd5Lowercase = "6c7816869bcebe4634f7afe9c66dfa08"; + + var result = HashService.ToMd5Hash(input); + + Assert.True(result.IsSuccess); + Assert.True(HashService.IsMd5Hash(result.Value)); + Assert.Equal(expectedMd5Lowercase, result.Value); + } + + [Fact] + public void VerifyPayfastWebhookSignature_Should_GenerateHash() + { + var paymentId = hashService.HashEncodeLongId(1001).Value; + + var incomingForm = new Dictionary + { + { "m_payment_id", paymentId }, + { "amount", "350.00" }, + { "item_name", "System Architecture Book" } + }; + + var rawPayload = $"amount=350.00&item_name=System+Architecture+Book&m_payment_id={paymentId}&passphrase={payfastPassphrase}"; + var generatedSignature = HashService.ToMd5Hash(rawPayload).Value; + + var result = hashService.VerifyPayfastWebhookSignature(incomingForm, generatedSignature); + + Assert.True(result.IsSuccess); + Assert.True(result.Value); + } + + [Fact] + public void HashEncodeHex_Should_GenerateHash() + { + var validHexInput = "DEADBEEF42"; + + var result = hashService.HashEncodeHex(validHexInput); + + Assert.True(result.IsSuccess); + Assert.False(string.IsNullOrWhiteSpace(result.Value)); + } + + [Fact] + public void HashEncodeIntId_Should_GenerateHash() + { + int targetId = 42; + + var result = hashService.HashEncodeIntId(targetId); + + Assert.True(result.IsSuccess); + Assert.True(result.Value.Length >= 10); + } + + [Fact] + public void HashEncodeLongId_Should_GenerateHash() + { + long targetId = 9904185012L; + + var result = hashService.HashEncodeLongId(targetId); + + Assert.True(result.IsSuccess); + Assert.True(result.Value.Length >= 10); + } + + [Fact] + public void DecodeIntIdHash_Should_GenerateHash() + { + int originalId = 88041; + var hashedString = hashService.HashEncodeIntId(originalId).Value; + + var result = hashService.DecodeIntIdHash(hashedString); + + Assert.True(result.IsSuccess); + Assert.Equal(originalId, result.Value); + } + + [Fact] + public void DecodeLongIdHash_Should_GenerateHash() + { + long originalId = 9081230491823L; + var hashedString = hashService.HashEncodeLongId(originalId).Value; + + var result = hashService.DecodeLongIdHash(hashedString); + + Assert.True(result.IsSuccess); + Assert.Equal(originalId, result.Value); + } + + [Fact] + public void DecodeHexHash_Should_GenerateHash() + { + var originalHex = "ABCDEF12345"; + var hashedString = hashService.HashEncodeHex(originalHex).Value; + + var result = hashService.DecodeHexHash(hashedString); + + Assert.True(result.IsSuccess); + Assert.Equal(originalHex, result.Value); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj index 6f93280..015c0b1 100644 --- a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj +++ b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj @@ -27,6 +27,8 @@ + + diff --git a/LiteCharms.Features/Hasher/HashService.cs b/LiteCharms.Features/Hasher/HashService.cs index fc18e38..bbe4a47 100644 --- a/LiteCharms.Features/Hasher/HashService.cs +++ b/LiteCharms.Features/Hasher/HashService.cs @@ -7,25 +7,37 @@ public sealed partial class HashService(IHashids hasher, IOptions StringToSha256Hash = (input) => - string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))); + [GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex Md5Regex { get; } - public static readonly Func StreamToSha256Hash = (stream) => + [GeneratedRegex(@"\A[0-9a-fA-F]{64}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex Sha256Regex { get; } + + public static bool IsMd5Hash(string? value) => + !string.IsNullOrWhiteSpace(value) && Md5Regex.IsMatch(value); + + public static bool IsSha256Hash(string? value) => + !string.IsNullOrWhiteSpace(value) && Sha256Regex.IsMatch(value); + + public static string? StringToSha256Hash(string? input) => + string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))); + + public static string? StreamToSha256Hash(Stream stream) => stream is null ? null : Convert.ToHexString(SHA256.HashData(stream)); - public static readonly Func BytesToSha256Hash = (bytes) => + public static string? BytesToSha256Hash(byte[] bytes) => bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes)); - - public static Result ComputeMd5Hash(string input) + + public static Result ToMd5Hash(string input) { if (string.IsNullOrEmpty(input)) return Result.Fail("Input content cannot be null or empty for MD5 processing."); byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input)); - return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant()); + return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant()); } public Result VerifyPayfastWebhookSignature(IDictionary incomingFormData, string incomingSignature) @@ -36,16 +48,16 @@ public sealed partial class HashService(IHashids hasher, IOptions("Validation failed: Missing signature string parameter."); var sortedFields = incomingFormData - .Where(field => field.Key != "signature") - .OrderBy(field => field.Key) - .Select(field => $"{field.Key}={Uri.EscapeDataString(field.Value).Replace("%20", "+")}"); + .Where(field => !string.Equals(field.Key, "signature", StringComparison.OrdinalIgnoreCase)) + .OrderBy(field => field.Key, StringComparer.Ordinal) + .Select(field => $"{field.Key}={WebUtility.UrlEncode(field.Value)}"); string payload = string.Join("&", sortedFields); if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase)) - payload += $"&passphrase={Uri.EscapeDataString(settings.PayfastPassphrase).Replace("%20", "+")}"; + payload += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}"; - var localHashResult = ComputeMd5Hash(payload); + var localHashResult = ToMd5Hash(payload); if (!localHashResult.IsSuccess) return Result.Fail(localHashResult.Errors); @@ -60,9 +72,9 @@ public sealed partial class HashService(IHashids hasher, IOptions HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex().IsMatch(input) - ? Result.Fail("Input must be a valid hexadecimal string.") - : Result.Ok(hasher.EncodeHex(input)); + public Result HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input) + ? Result.Fail("Input must be a valid hexadecimal string.") + : Result.Ok(hasher.EncodeHex(input)); public Result HashEncodeIntId(int id) => id < 0 ? Result.Fail("Id cannot be negative.") diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 6079860..df4841b 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -148,6 +148,8 @@ + + diff --git a/LiteCharms.Features/Models/SearchState.cs b/LiteCharms.Features/Models/SearchState.cs deleted file mode 100644 index 6c483cd..0000000 --- a/LiteCharms.Features/Models/SearchState.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace LiteCharms.Features.Models; - -public class SearchState -{ - public string Query { get; private set; } = string.Empty; - - public event Action? OnSearchSubmitted; - - public void UpdateQuery(string newQuery) => Query = newQuery; - - public void SubmitSearch() => OnSearchSubmitted?.Invoke(); -} From f1ef614cbba61889185eb29b1aee003e69794f18 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 1 Jun 2026 09:19:08 +0200 Subject: [PATCH 080/153] Removed passphrase from PaymentGateway --- .../Extensions/Mappers.cs | 1 - .../Entities/PaymentGatewayConfiguration.cs | 1 - .../Payments/Models/PaymentGateway.cs | 2 - .../Payments/PaymentService.cs | 1 - ...edPassphraseFromPaymentGateway.Designer.cs | 1231 +++++++++++++++++ ...804_RemovedPassphraseFromPaymentGateway.cs | 29 + .../MidrandBooksDbContextModelSnapshot.cs | 4 - 7 files changed, 1260 insertions(+), 9 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.cs diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index e8a8612..118dc79 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -46,7 +46,6 @@ public static class Mappers MerchantId = entity.MerchantId, MerchantKey = entity.MerchantKey, Name = entity.Name, - Passphrase = entity.Passphrase, Website = entity.Website, }; diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs index adfb8a5..43873ed 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs @@ -15,6 +15,5 @@ public sealed class PaymentGatewayConfiguration : IEntityTypeConfiguration f.MerchantId).IsRequired(); builder.Property(f => f.Enabled); builder.Property(f => f.Name).IsRequired(); - builder.Property(f => f.Passphrase).IsRequired(); } } diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs index bdb8a69..4c2701c 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs @@ -16,8 +16,6 @@ public class PaymentGateway public string? MerchantKey { get; set; } - public string? Passphrase { get; set; } - public bool IsSandbox { get; set; } public bool Enabled { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs index 3dd26dc..58e418b 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -174,7 +174,6 @@ public sealed class PaymentService(IDbContextFactory cont MerchantKey = request.MerchantKey, Name = request.Name, Website = request.Website, - Passphrase = "N/A", }); return await context.SaveChangesAsync(cancellationToken) > 0 diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.Designer.cs new file mode 100644 index 0000000..bcf52cd --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.Designer.cs @@ -0,0 +1,1231 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260601071804_RemovedPassphraseFromPaymentGateway")] + partial class RemovedPassphraseFromPaymentGateway + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayReference") + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentGatewayId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", "Gateway") + .WithMany() + .HasForeignKey("PaymentGatewayId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Gateway"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.cs new file mode 100644 index 0000000..9401647 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class RemovedPassphraseFromPaymentGateway : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Passphrase", + table: "Gateways"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Passphrase", + table: "Gateways", + type: "text", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 635ea90..79de829 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -604,10 +604,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .IsRequired() .HasColumnType("text"); - b.Property("Passphrase") - .IsRequired() - .HasColumnType("text"); - b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); From afc984f3bcc27acc795abc83ee9a7e34d3475446 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 1 Jun 2026 09:25:46 +0200 Subject: [PATCH 081/153] Fixed mdf file name hasher --- LiteCharms.Features.TechShop/Leads/LeadService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LiteCharms.Features.TechShop/Leads/LeadService.cs b/LiteCharms.Features.TechShop/Leads/LeadService.cs index 4469e03..ecaf004 100644 --- a/LiteCharms.Features.TechShop/Leads/LeadService.cs +++ b/LiteCharms.Features.TechShop/Leads/LeadService.cs @@ -28,7 +28,7 @@ public class LeadService(IDbContextFactory contextFactory) FeedItemId = request.FeedItemId, Status = LeadStatus.New, TargetId = request.TargetId, - AttributionHash = HashService.StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}") + AttributionHash = HashService.StringToSha256Hash($"{request.ClickId}{request.AppClickId}{request.WebClickId}") }); return await context.SaveChangesAsync(cancellationToken) > 0 From 5eb6dbc8b2e58e38cbaf5217a9f92a4341d9f1f9 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 1 Jun 2026 16:36:33 +0200 Subject: [PATCH 082/153] Refactored shasher payfast confirmation response handling --- .../HashServiceFeatureTests.cs | 15 +++-- LiteCharms.Features/Extensions/Hash.cs | 62 +++++++++++++++++++ LiteCharms.Features/Hasher/HashService.cs | 25 +++++--- .../Models/PayfastWebhookPayload.cs | 8 +++ 4 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 LiteCharms.Features/Models/PayfastWebhookPayload.cs diff --git a/LiteCharms.Features.Tests/HashServiceFeatureTests.cs b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs index 1515107..6d85422 100644 --- a/LiteCharms.Features.Tests/HashServiceFeatureTests.cs +++ b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs @@ -1,4 +1,6 @@ using LiteCharms.Features.Hasher; +using LiteCharms.Features.Models; +using static LiteCharms.Features.Extensions.Hash; namespace LiteCharms.Features.Tests; @@ -65,17 +67,18 @@ public class HashServiceFeatureTests(Fixture fixture) : IClassFixture { var paymentId = hashService.HashEncodeLongId(1001).Value; - var incomingForm = new Dictionary + var payload = new PayfastWebhookPayload { - { "m_payment_id", paymentId }, - { "amount", "350.00" }, - { "item_name", "System Architecture Book" } + Amount = "350.00", + ItemName = "System Architecture Book", + MPaymentId = paymentId, }; - var rawPayload = $"amount=350.00&item_name=System+Architecture+Book&m_payment_id={paymentId}&passphrase={payfastPassphrase}"; + var rawPayload = payload.ToRawPayfastPayload(payfastPassphrase); + var generatedSignature = HashService.ToMd5Hash(rawPayload).Value; - var result = hashService.VerifyPayfastWebhookSignature(incomingForm, generatedSignature); + var result = hashService.VerifyPayfastWebhookSignature(payload, generatedSignature); Assert.True(result.IsSuccess); Assert.True(result.Value); diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs index 6aa95ed..0c17d85 100644 --- a/LiteCharms.Features/Extensions/Hash.cs +++ b/LiteCharms.Features/Extensions/Hash.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.Hasher; using LiteCharms.Features.Hasher.Configuration; +using LiteCharms.Features.Models; namespace LiteCharms.Features.Extensions; @@ -20,4 +21,65 @@ public static class Hash return services; } + + public static string ToRawPayfastPayload(this PayfastWebhookPayload input, string passphrase) + { + var parameters = new List(); + + if (!string.IsNullOrWhiteSpace(input.Amount)) + parameters.Add($"amount={WebUtility.UrlEncode(input.Amount)}"); + + if (!string.IsNullOrWhiteSpace(input.ItemName)) + parameters.Add($"item_name={WebUtility.UrlEncode(input.ItemName)}"); + + if (!string.IsNullOrWhiteSpace(input.MPaymentId)) + parameters.Add($"m_payment_id={WebUtility.UrlEncode(input.MPaymentId)}"); + + string payload = string.Join("&", parameters); + + if (!string.IsNullOrWhiteSpace(passphrase)) + payload += $"&passphrase={WebUtility.UrlEncode(passphrase)}"; + + return payload; + } + + public static (PayfastWebhookPayload Payload, string Passphrase) FromRawPayfastPayload(this string rawPayload) + { + string passphrase = string.Empty; + var payload = new PayfastWebhookPayload(); + + if (string.IsNullOrWhiteSpace(rawPayload)) return (payload, passphrase); + + var segments = rawPayload.Split('&', StringSplitOptions.RemoveEmptyEntries); + + foreach (var segment in segments) + { + int delimiterIndex = segment.IndexOf('='); + if (delimiterIndex == -1) + continue; + + string key = segment[..delimiterIndex].Trim(); + string rawValue = segment[(delimiterIndex + 1)..]; + + string decodedValue = WebUtility.UrlDecode(rawValue); + + switch (key.ToLowerInvariant()) + { + case "amount": + payload.Amount = decodedValue; + break; + case "item_name": + payload.ItemName = decodedValue; + break; + case "m_payment_id": + payload.MPaymentId = decodedValue; + break; + case "passphrase": + passphrase = decodedValue; + break; + } + } + + return (payload, passphrase); + } } \ No newline at end of file diff --git a/LiteCharms.Features/Hasher/HashService.cs b/LiteCharms.Features/Hasher/HashService.cs index bbe4a47..1b38ed7 100644 --- a/LiteCharms.Features/Hasher/HashService.cs +++ b/LiteCharms.Features/Hasher/HashService.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.Abstractions; using LiteCharms.Features.Hasher.Configuration; +using LiteCharms.Features.Models; namespace LiteCharms.Features.Hasher; @@ -40,24 +41,30 @@ public sealed partial class HashService(IHashids hasher, IOptions VerifyPayfastWebhookSignature(IDictionary incomingFormData, string incomingSignature) + public Result VerifyPayfastWebhookSignature(PayfastWebhookPayload payload, string incomingSignature) { try { if (string.IsNullOrWhiteSpace(incomingSignature)) return Result.Fail("Validation failed: Missing signature string parameter."); - var sortedFields = incomingFormData - .Where(field => !string.Equals(field.Key, "signature", StringComparison.OrdinalIgnoreCase)) - .OrderBy(field => field.Key, StringComparer.Ordinal) - .Select(field => $"{field.Key}={WebUtility.UrlEncode(field.Value)}"); + var parameters = new List(); - string payload = string.Join("&", sortedFields); + if (!string.IsNullOrWhiteSpace(payload.Amount)) + parameters.Add($"amount={WebUtility.UrlEncode(payload.Amount)}"); + + if (!string.IsNullOrWhiteSpace(payload.ItemName)) + parameters.Add($"item_name={WebUtility.UrlEncode(payload.ItemName)}"); + + if (!string.IsNullOrWhiteSpace(payload.MPaymentId)) + parameters.Add($"m_payment_id={WebUtility.UrlEncode(payload.MPaymentId)}"); + + string signatureString = string.Join("&", parameters); if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase)) - payload += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}"; + signatureString += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}"; - var localHashResult = ToMd5Hash(payload); + var localHashResult = ToMd5Hash(signatureString); if (!localHashResult.IsSuccess) return Result.Fail(localHashResult.Errors); @@ -68,7 +75,7 @@ public sealed partial class HashService(IHashids hasher, IOptions(new Error("An error occurred during MD5 verification loop.").CausedBy(ex)); + return Result.Fail(new Error("An error occurred during Payfast MD5 verification.").CausedBy(ex)); } } diff --git a/LiteCharms.Features/Models/PayfastWebhookPayload.cs b/LiteCharms.Features/Models/PayfastWebhookPayload.cs new file mode 100644 index 0000000..6b0a5db --- /dev/null +++ b/LiteCharms.Features/Models/PayfastWebhookPayload.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.Models; + +public sealed class PayfastWebhookPayload +{ + public string? Amount { get; set; } + public string? ItemName { get; set; } + public string? MPaymentId { get; set; } +} From ac31c6ada8652464dc3fff07b8b2d0effc9869de Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 1 Jun 2026 17:02:30 +0200 Subject: [PATCH 083/153] Implemented overload taking in IFormCollection --- LiteCharms.Features/Extensions/Hash.cs | 17 +++++++++++++++++ LiteCharms.Features/LiteCharms.Features.csproj | 2 ++ 2 files changed, 19 insertions(+) diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs index 0c17d85..78f2c6c 100644 --- a/LiteCharms.Features/Extensions/Hash.cs +++ b/LiteCharms.Features/Extensions/Hash.cs @@ -82,4 +82,21 @@ public static class Hash return (payload, passphrase); } + + public static (PayfastWebhookPayload Payload, string Passphrase) FromRawPayfastPayload(this IFormCollection form) + { + string passphrase = string.Empty; + var payload = new PayfastWebhookPayload(); + + if (form.IsNullOrEmpty()) return (payload, passphrase); + + payload = new PayfastWebhookPayload + { + Amount = form.TryGetValue("amount", out var amountValues) ? amountValues.ToString() : null, + ItemName = form.TryGetValue("item_name", out var itemValues) ? itemValues.ToString() : null, + MPaymentId = form.TryGetValue("m_payment_id", out var paymentIdValues) ? paymentIdValues.ToString() : null, + }; + + return (payload, passphrase); + } } \ No newline at end of file diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index df4841b..087c9e4 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -147,6 +147,8 @@ + + From 45c2e8310a66df6593b3450ff8d1f624b7ce6489 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 1 Jun 2026 22:51:49 +0200 Subject: [PATCH 084/153] Added PayfastPaymentConfirmationReceivedEvent --- ...PaymentConfirmationReceivedEventHandler.cs | 81 +++++++++++++++++++ ...PayfastPaymentConfirmationReceivedEvent.cs | 22 +++++ .../Payments/PaymentService.cs | 40 +++++++++ LiteCharms.Features/Quartz/JobOrchestrator.cs | 2 +- 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs new file mode 100644 index 0000000..371c07b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs @@ -0,0 +1,81 @@ +using LiteCharms.Features.Hasher; +using LiteCharms.Features.MidrandBooks.Orders; + +namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers; + +public sealed class PayfastPaymentConfirmationReceivedEventHandler(PaymentService paymentService, + HashService hashService, OrderService orderService, ILogger logger) : + INotificationHandler +{ + public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken) + { + var hashResult = hashService.DecodeLongIdHash(notification.Payload?.MPaymentId!); + if (hashResult.IsFailed) + { + logger.LogError("Failed to decode payment ID hash: {Hash}. Errors: {Errors}", notification.Payload?.MPaymentId, string.Join(", ", hashResult.Errors.Select(e => e.Message))); + throw new Exception($"Failed to decode payment ID hash: {notification.Payload?.MPaymentId}."); + } + + var orderResult = await orderService.GetOrderAsync(hashResult.Value, cancellationToken); + if (orderResult.IsFailed) + { + logger.LogError("Failed to retrieve order for payment ID: {PaymentId}. Errors: {Errors}", notification.Payload?.MPaymentId, string.Join(", ", orderResult.Errors.Select(e => e.Message))); + throw new Exception($"Failed to retrieve order for payment ID: {notification.Payload?.MPaymentId}."); + } + + var paymentResult = await paymentService.GetOrderPaymentAsync(orderResult.Value.CustomerId, cancellationToken); + if (paymentResult.IsFailed) + { + logger.LogError("Failed to retrieve payment for order ID: {OrderId}. Errors: {Errors}", orderResult.Value.Id, string.Join(", ", paymentResult.Errors.Select(e => e.Message))); + throw new Exception($"Failed to retrieve payment for order ID: {orderResult.Value.Id}."); + } + + var isAlreadyProcessed = await paymentService.HasLedgerEntryAsync(orderResult.Value.Id, paymentResult.Value.Id, 1, cancellationToken); + + if (isAlreadyProcessed.IsFailed) + { + logger.LogError("Failed to check existing ledger entry for order ID: {OrderId} and payment ID: {PaymentId}. Errors: {Errors}", orderResult.Value.Id, paymentResult.Value.Id, string.Join(", ", isAlreadyProcessed.Errors.Select(e => e.Message))); + throw new Exception($"Failed to check existing ledger entry for order ID: {orderResult.Value.Id} and payment ID: {paymentResult.Value.Id}."); + } + + if (isAlreadyProcessed.Value) + { + logger.LogInformation("Payment confirmation for payment ID: {PaymentId} has already been processed. Skipping.", notification.Payload?.MPaymentId); + return; + } + + var ledgerResult = await paymentService.WriteLedgerEntryAsync(new Models.CreateLedgerEntry + { + CustomerId = orderResult.Value.CustomerId, + OrderId = orderResult.Value.Id, + PaymentId = paymentResult.Value.Id, + Status = LedgerStatuses.Received, + PaymentGatewayId = 1, + PaymentGatewayReference = notification.CorrelationId, + }, cancellationToken); + + if (ledgerResult.IsFailed) + { + logger.LogError("Failed to write ledger entry for payment ID: {PaymentId}. Errors: {Errors}", notification.Payload?.MPaymentId, string.Join(", ", ledgerResult.Errors.Select(e => e.Message))); + throw new Exception($"Failed to write ledger entry for payment ID: {notification.Payload?.MPaymentId}."); + } + + var paymentCompletedResult = await paymentService.CompletePaymentAsync(paymentResult.Value.Id, PaymentStatuses.Paid, cancellationToken); + if (paymentCompletedResult.IsFailed) + { + logger.LogError("Failed to complete payment for order ID: {OrderId}. Errors: {Errors}", orderResult.Value.Id, string.Join(", ", paymentCompletedResult.Errors.Select(e => e.Message))); + throw new Exception($"Failed to complete payment for order ID: {orderResult.Value.Id}."); + } + + var orderCompletedResult = await orderService.UpdateOrderStatusAsync(orderResult.Value.Id, OrderStatus.Completed, cancellationToken); + if (orderCompletedResult.IsFailed) + { + logger.LogError("Failed to update order status to Completed for order ID: {OrderId}. Errors: {Errors}", orderResult.Value.Id, string.Join(", ", orderCompletedResult.Errors.Select(e => e.Message))); + throw new Exception($"Failed to update order status to Completed for order ID: {orderResult.Value.Id}."); + } + + logger.LogInformation("Received Payfast payment confirmation for payment ID: {PaymentId}", notification.Payload?.MPaymentId); + + // TODO: Publish MediatR notifications or queue downstream Quartz jobs (Discord, Shipping, Customer Email, Royalties) + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs new file mode 100644 index 0000000..b322753 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs @@ -0,0 +1,22 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Payments.Events; + +public sealed class PayfastPaymentConfirmationReceivedEvent : EventBase, IEvent +{ + public string Name { get; set; } = nameof(PayfastPaymentConfirmationReceivedEvent); + + public PayfastWebhookPayload? Payload { get; set; } + + public PayfastPaymentConfirmationReceivedEvent() { } + + private PayfastPaymentConfirmationReceivedEvent(PayfastWebhookPayload? payload, string paymentId) + { + Payload = payload; + CorrelationId = paymentId; + } + + public static PayfastPaymentConfirmationReceivedEvent Create(PayfastWebhookPayload? payload, string paymentId) => + new(payload, paymentId); +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs index 58e418b..7904491 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -7,6 +7,27 @@ namespace LiteCharms.Features.MidrandBooks.Payments; public sealed class PaymentService(IDbContextFactory contextFactory) : IService { + public async ValueTask> GetOrderPaymentAsync(long orderId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var payment = await context.Payments.AsNoTracking() + .Where(p => p.OrderId == orderId) + .OrderByDescending(p => p.Id) + .FirstOrDefaultAsync(cancellationToken); + + return payment is not null + ? Result.Ok(payment.ToModel()) + : Result.Fail("Could not find payment for the order"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + public async ValueTask> GetRefundAsync(long refundId, CancellationToken cancellationToken = default) { try @@ -95,6 +116,25 @@ public sealed class PaymentService(IDbContextFactory cont } } + public async ValueTask> HasLedgerEntryAsync(long orderId, long paymentId, long gatewayId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var exists = await context.Ledger.AnyAsync(l => + l.OrderId == orderId && + l.PaymentId == paymentId && + l.PaymentGatewayId == gatewayId, cancellationToken); + + return Result.Ok(exists); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + public async ValueTask WriteLedgerEntryAsync(CreateLedgerEntry request, CancellationToken cancellationToken = default) { try diff --git a/LiteCharms.Features/Quartz/JobOrchestrator.cs b/LiteCharms.Features/Quartz/JobOrchestrator.cs index fae63c3..7c79b8a 100644 --- a/LiteCharms.Features/Quartz/JobOrchestrator.cs +++ b/LiteCharms.Features/Quartz/JobOrchestrator.cs @@ -23,7 +23,7 @@ public sealed class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOr var trigger = global::Quartz.TriggerBuilder.Create() .WithIdentity(triggerKey) - .StartNow() + .StartNow() .Build(); await scheduler.ScheduleJob(job, new List { trigger }.AsReadOnly(), replace: true, cancellationToken); From 139ca1f8663d8b5b027576923140a5baec55536a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Mon, 1 Jun 2026 23:32:35 +0200 Subject: [PATCH 085/153] Fixed event service scope issue --- .../PayfastPaymentConfirmationReceivedEventHandler.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs index 371c07b..96682ed 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs @@ -3,10 +3,13 @@ using LiteCharms.Features.MidrandBooks.Orders; namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers; -public sealed class PayfastPaymentConfirmationReceivedEventHandler(PaymentService paymentService, - HashService hashService, OrderService orderService, ILogger logger) : +public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, ILogger logger) : INotificationHandler { + private readonly PaymentService paymentService = services.GetRequiredService(); + private readonly OrderService orderService = services.GetRequiredService(); + private readonly HashService hashService = services.GetRequiredService(); + public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken) { var hashResult = hashService.DecodeLongIdHash(notification.Payload?.MPaymentId!); From 5ab2d29aac1498fadbcce555a2165f10bfe527d4 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 2 Jun 2026 00:03:01 +0200 Subject: [PATCH 086/153] Used scope to inject services --- .../PayfastPaymentConfirmationReceivedEventHandler.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs index 96682ed..66ba389 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs @@ -6,12 +6,14 @@ namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers; public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, ILogger logger) : INotificationHandler { - private readonly PaymentService paymentService = services.GetRequiredService(); - private readonly OrderService orderService = services.GetRequiredService(); - private readonly HashService hashService = services.GetRequiredService(); - public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken) { + await using var scope = services.CreateAsyncScope(); + + PaymentService paymentService = scope.ServiceProvider.GetRequiredService(); + OrderService orderService = scope.ServiceProvider.GetRequiredService(); + HashService hashService = scope.ServiceProvider.GetRequiredService(); + var hashResult = hashService.DecodeLongIdHash(notification.Payload?.MPaymentId!); if (hashResult.IsFailed) { From 0ed04211bf6df1058d3a54d82d52b58936458c73 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 2 Jun 2026 23:44:45 +0200 Subject: [PATCH 087/153] Added payment gateway ledger service to payments feature --- .../Common/Fixture.cs | 4 +- ...eCharms.Features.MidrandBooks.Tests.csproj | 1 + .../PayfastServiceFeatureTests.cs | 113 ++ .../appsettings.json | 12 + .../Extensions/Mappers.cs | 21 +- .../LiteCharms.Features.MidrandBooks.csproj | 4 + .../Payments/Entities/PaymentGatewayLedger.cs | 11 + .../PaymentGatewayLedgerConfiguration.cs | 30 + .../Payments/Entities/PaymentLedger.cs | 2 - .../Entities/PaymentLedgerConfiguration.cs | 9 +- ...PaymentConfirmationReceivedEventHandler.cs | 188 ++- ...PayfastPaymentConfirmationReceivedEvent.cs | 13 +- .../Payments/Models/PayfastWebhookPayload.cs | 59 + .../Payments/Models/PaymentGatewayLedger.cs | 26 + .../Payments/Models/PaymentLedger.cs | 4 +- .../Payments/Models/Records.cs | 21 + .../Payments/PayfastService.cs | 171 +++ .../Payments/PaymentService.cs | 7 +- .../Postgres/MidrandBooksDbContext.cs | 2 + ...2421_AddedPaymentGatewayLedger.Designer.cs | 1291 ++++++++++++++++ ...0260602202421_AddedPaymentGatewayLedger.cs | 108 ++ ...aymentIdToPaymentGatewayLedger.Designer.cs | 1292 +++++++++++++++++ ...dPayfastPaymentIdToPaymentGatewayLedger.cs | 36 + .../MidrandBooksDbContextModelSnapshot.cs | 91 +- .../HashServiceFeatureTests.cs | 25 - LiteCharms.Features/Enums.cs | 1 + LiteCharms.Features/Extensions/Hash.cs | 81 +- LiteCharms.Features/Hasher/HashService.cs | 44 +- .../LiteCharms.Features.csproj | 1 + .../Models/PayfastWebhookPayload.cs | 8 - 30 files changed, 3420 insertions(+), 256 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedger.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedgerConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/PayfastWebhookPayload.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGatewayLedger.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.cs delete mode 100644 LiteCharms.Features/Models/PayfastWebhookPayload.cs diff --git a/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs index dae42e0..0732e5b 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs @@ -24,7 +24,8 @@ public class Fixture : IDisposable .AddEnvironmentVariables() .Build(); - Services = new ServiceCollection() + Services = new ServiceCollection() + .AddHttpClient() .AddMediator() .AddLogging() .AddEmailServiceBus() @@ -33,6 +34,7 @@ public class Fixture : IDisposable .AddEmailServices(Configuration) .AddSingleton(Configuration) .AddShopServices() + .AddHashServices(Configuration) .BuildServiceProvider(); Mediator = Services.GetRequiredService(); diff --git a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj index 3b0e796..174bbd1 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj +++ b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj @@ -39,6 +39,7 @@ + diff --git a/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs new file mode 100644 index 0000000..66da983 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs @@ -0,0 +1,113 @@ +using LiteCharms.Features.MidrandBooks.Payments; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.MidrandBooks.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public sealed class PayfastServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly PayfastService payfastService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task WriteLedgerEntryAsync_ShouldReturn_ResultWithGatewayLedgerId() + { + var request = new CreateGatewayLedgerEntry + { + OrderId = 1, + PaymentId = 1, + MerchantPaymentId = "M_REF_TEST_99", + PayfastPaymentId = "PF_SYS_ID_10023", + CustomerEmail = "buyer@litecharms.co.za", + AmountGross = 350.00m, + AmountFee = 12.50m, + AmountNet = 337.50m, + PaymentStatus = "COMPLETE" + }; + + var result = await payfastService.WriteLedgerEntryAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task ValidateReferrerIpAsync_WithValidPayfastHostIp_ShouldReturnTrue() + { + var addresses = await Dns.GetHostAddressesAsync("sandbox.payfast.co.za", fixture.CancellationToken); + + string liveTargetIp = addresses.First().ToString(); + + var result = await payfastService.ValidateReferrerIpAsync(liveTargetIp, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value); + } + + [IntegrationFact] + public async Task ValidateReferrerIpAsync_WithUntrustedIp_ShouldReturnFalse() + { + string rogueIp = "8.8.8.8"; + + var result = await payfastService.ValidateReferrerIpAsync(rogueIp, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.False(result.Value); + } + + [IntegrationFact] + public void ValidatePaymentAmount_WhenWithinAllowableDelta_ShouldReturnTrue() + { + decimal systemExpectedTotal = 199.99m; + string gatewayClearedGross = "200.00"; // Variance is exactly R0.01 + + var result = payfastService.ValidatePaymentAmount(systemExpectedTotal, gatewayClearedGross); + + Assert.True(result.IsSuccess); + Assert.True(result.Value); + } + + [IntegrationFact] + public void ValidatePaymentAmount_WhenVarianceBreachesDeltaBounds_ShouldReturnFalse() + { + decimal systemExpectedTotal = 199.99m; + string gatewayClearedGross = "150.00"; + + var result = payfastService.ValidatePaymentAmount(systemExpectedTotal, gatewayClearedGross); + + Assert.True(result.IsSuccess); + Assert.False(result.Value); + } + + [IntegrationFact] + public async Task ValidateServerConfirmationAsync_WithUnrecognizedPayload_ShouldReturnFalseFromCentralGateway() + { + // Arrange - Execute against actual Payfast servers using raw mock parameters. + // The server handshake will return 200 OK with string payload 'INVALID' + string arbitraryParameters = "merchant_id=10000000&payment_status=COMPLETE"; + + var result = await payfastService.ValidateServerConfirmationAsync(arbitraryParameters, isSandbox: true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.False(result.Value); // Handshake data rejected as fraudulent/unrecognized + } + + [IntegrationFact] + public void GenerateSignature_WithStandardTelemetryData_ShouldSucceedAndHashString() + { + var telemetryPayload = new Dictionary + { + { "merchant_id", "10049307" }, + { "merchant_key", "ju6navn0jcbf0" }, + { "amount_gross", "250.00" }, + { "item_name", "Midrand School Textbook Variant A" } + }; + + string passphrase = "oauth_test_signature_pass"; + + var result = PayfastService.GenerateSignature(telemetryPayload, passphrase); + + Assert.True(result.IsSuccess); + Assert.False(string.IsNullOrWhiteSpace(result.Value)); + Assert.Equal(32, result.Value.Length); // MD5 outputs hex representations totaling 32 characters + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Tests/appsettings.json b/LiteCharms.Features.MidrandBooks.Tests/appsettings.json index 7f9a6b8..b6f6ba7 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/appsettings.json +++ b/LiteCharms.Features.MidrandBooks.Tests/appsettings.json @@ -1,4 +1,16 @@ { + "ValidPayfastHosts": [ + "www.payfast.co.za", + "sandbox.payfast.co.za", + "w1w.payfast.co.za", + "w2w.payfast.co.za", + "ips.payfast.co.za", + "api.payfast.co.za", + "payment.payfast.io" + ], + "HasherSettings": { + "MinHashLength": 11 + }, "BookshopS3Settings": { "ServiceUrl": "http://192.168.1.177:30900", "Region": "garage", diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 118dc79..467d096 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -6,12 +6,26 @@ using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Products.Models; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Mappers { + public static PaymentGatewayLedger ToModel(this Payments.Entities.PaymentGatewayLedger entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CustomerEmail = entity.CustomerEmail, + OrderId = entity.OrderId, + PaymentId = entity.PaymentId, + MerchantPaymentId = entity.MerchantPaymentId, + PayfastPaymentId = entity.PayfastPaymentId, + PaymentStatus = entity.PaymentStatus, + AmountGross = entity.AmountGross, + AmountFee = entity.AmountFee, + AmountNet = entity.AmountNet + }; + public static Refund ToModel(this Payments.Entities.Refund entity) => new() { CreatedAt = entity.CreatedAt, @@ -30,10 +44,9 @@ public static class Mappers CreatedAt = entity.CreatedAt, CustomerId = entity.CustomerId, OrderId = entity.OrderId, - PaymentGatewayId = entity.PaymentGatewayId, - PaymentGatewayReference = entity.PaymentGatewayReference, PaymentId = entity.PaymentId, - Status = entity.Status, + Status = entity.Status, + MerchantPaymentId = entity.MerchantPaymentId, }; public static PaymentGateway ToModel(this Payments.Entities.PaymentGateway entity) => new() diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index e2e58d3..1206d52 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -148,6 +148,10 @@ + + + + diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedger.cs new file mode 100644 index 0000000..6ed1fe6 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedger.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.MidrandBooks.Orders.Entities; + +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class PaymentGatewayLedger : Models.PaymentGatewayLedger +{ + public virtual Order? Order { get; set; } + + public virtual Payment? Payment { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedgerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedgerConfiguration.cs new file mode 100644 index 0000000..f95b256 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedgerConfiguration.cs @@ -0,0 +1,30 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class PaymentGatewayLedgerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("GatewayLedger"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.OrderId).IsRequired(); + builder.Property(f => f.PaymentId).IsRequired(); + builder.Property(f => f.PayfastPaymentId).IsRequired(); + builder.Property(f => f.MerchantPaymentId).IsRequired(); + builder.Property(f => f.AmountGross).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.AmountFee).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.AmountNet).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.CustomerEmail).IsRequired(false); + + builder.HasOne(f => f.Order) + .WithMany() + .HasForeignKey(f => f.OrderId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Payment) + .WithMany() + .HasForeignKey(f => f.PaymentId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs index 3bba78b..acec0ea 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs @@ -11,6 +11,4 @@ public class PaymentLedger : Models.PaymentLedger public virtual Order? Order { get; set; } public virtual Customer? Customer { get; set; } - - public virtual PaymentGateway? Gateway { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs index 0e51655..c0add81 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs @@ -9,8 +9,7 @@ public sealed class PaymentLedgerConfiguration : IEntityTypeConfiguration f.Id); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(f => f.Status).IsRequired(); - builder.Property(f => f.PaymentGatewayReference).IsRequired(false); - builder.Property(f => f.PaymentGatewayId).IsRequired(false); + builder.Property(f => f.MerchantPaymentId).IsRequired(false); builder.Property(f => f.OrderId).IsRequired(); builder.Property(f => f.CustomerId).IsRequired(); builder.Property(f => f.PaymentId).IsRequired(); @@ -31,11 +30,5 @@ public sealed class PaymentLedgerConfiguration : IEntityTypeConfiguration f.CustomerId); - - builder.HasOne(f => f.Gateway) - .WithMany() - .IsRequired(false) - .HasForeignKey(f => f.PaymentGatewayId) - .OnDelete(DeleteBehavior.Cascade); } } diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs index 66ba389..fb785f8 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs @@ -1,86 +1,158 @@ using LiteCharms.Features.Hasher; +using LiteCharms.Features.Hasher.Configuration; using LiteCharms.Features.MidrandBooks.Orders; +using LiteCharms.Features.MidrandBooks.Payments.Models; namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers; -public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, ILogger logger) : +public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, IOptions hasherOptions, ILogger logger) : INotificationHandler { + private readonly HasherSettings hasherSettings = hasherOptions.Value; + public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken) { await using var scope = services.CreateAsyncScope(); + var hashService = scope.ServiceProvider.GetRequiredService(); + var orderService = scope.ServiceProvider.GetRequiredService(); + var paymentService = scope.ServiceProvider.GetRequiredService(); + var payfastService = scope.ServiceProvider.GetRequiredService(); - PaymentService paymentService = scope.ServiceProvider.GetRequiredService(); - OrderService orderService = scope.ServiceProvider.GetRequiredService(); - HashService hashService = scope.ServiceProvider.GetRequiredService(); + var payload = notification.Payload ?? throw new Exception("Payload metadata context context is null."); - var hashResult = hashService.DecodeLongIdHash(notification.Payload?.MPaymentId!); - if (hashResult.IsFailed) + var dict = payload.ToParamDictionary(); + var localSignature = PayfastService.GenerateSignature(dict, hasherSettings.PayfastPassphrase); + + if(localSignature.IsFailed) + throw new Exception("Failed to generate local signature for incoming webhook payload."); + + if (!string.Equals(localSignature.Value, payload.Signature, StringComparison.OrdinalIgnoreCase)) { - logger.LogError("Failed to decode payment ID hash: {Hash}. Errors: {Errors}", notification.Payload?.MPaymentId, string.Join(", ", hashResult.Errors.Select(e => e.Message))); - throw new Exception($"Failed to decode payment ID hash: {notification.Payload?.MPaymentId}."); - } + logger.LogCritical("Incoming webhook signature verification failed. Possible payload tampering."); - var orderResult = await orderService.GetOrderAsync(hashResult.Value, cancellationToken); - if (orderResult.IsFailed) - { - logger.LogError("Failed to retrieve order for payment ID: {PaymentId}. Errors: {Errors}", notification.Payload?.MPaymentId, string.Join(", ", orderResult.Errors.Select(e => e.Message))); - throw new Exception($"Failed to retrieve order for payment ID: {notification.Payload?.MPaymentId}."); - } - - var paymentResult = await paymentService.GetOrderPaymentAsync(orderResult.Value.CustomerId, cancellationToken); - if (paymentResult.IsFailed) - { - logger.LogError("Failed to retrieve payment for order ID: {OrderId}. Errors: {Errors}", orderResult.Value.Id, string.Join(", ", paymentResult.Errors.Select(e => e.Message))); - throw new Exception($"Failed to retrieve payment for order ID: {orderResult.Value.Id}."); - } - - var isAlreadyProcessed = await paymentService.HasLedgerEntryAsync(orderResult.Value.Id, paymentResult.Value.Id, 1, cancellationToken); - - if (isAlreadyProcessed.IsFailed) - { - logger.LogError("Failed to check existing ledger entry for order ID: {OrderId} and payment ID: {PaymentId}. Errors: {Errors}", orderResult.Value.Id, paymentResult.Value.Id, string.Join(", ", isAlreadyProcessed.Errors.Select(e => e.Message))); - throw new Exception($"Failed to check existing ledger entry for order ID: {orderResult.Value.Id} and payment ID: {paymentResult.Value.Id}."); - } - - if (isAlreadyProcessed.Value) - { - logger.LogInformation("Payment confirmation for payment ID: {PaymentId} has already been processed. Skipping.", notification.Payload?.MPaymentId); return; } - var ledgerResult = await paymentService.WriteLedgerEntryAsync(new Models.CreateLedgerEntry + var hashResult = hashService.DecodeLongIdHash(payload.MerchantPaymentId!); + + if (hashResult.IsFailed) throw new Exception("Failed to decode application tracking hash key identifier."); + + var orderResult = await orderService.GetOrderAsync(hashResult.Value, cancellationToken); + + if (orderResult.IsFailed) throw new Exception("Target system order entity context cannot be traced."); + + var paymentResult = await paymentService.GetOrderPaymentAsync(orderResult.Value.Id, cancellationToken); + + if (paymentResult.IsFailed) throw new Exception("Target payment ledger entity cannot be resolved."); + + decimal.TryParse(payload.AmountGross, CultureInfo.InvariantCulture, out var gross); + decimal.TryParse(payload.AmountFee, CultureInfo.InvariantCulture, out var fee); + decimal.TryParse(payload.AmountNet, CultureInfo.InvariantCulture, out var net); + string status = payload.PaymentStatus ?? "UNKNOWN"; + + var isAlreadyProcessed = await paymentService.HasLedgerEntryAsync(orderResult.Value.Id, paymentResult.Value.Id, cancellationToken); + + if (isAlreadyProcessed.Value) + { + logger.LogWarning("Webhook reference token '{Ref}' already verified. Skipping validation routines.", payload.MerchantPaymentId); + + return; + } + + if (notification.PerformBackgroundChecks) + { + var isHostValid = await payfastService.ValidateReferrerIpAsync(notification.RemoteIpAddress!, cancellationToken); + + if (isHostValid.IsFailed) + throw new Exception("Security validation exception: Webhook packet source address failed cluster validation checks."); + + if (!isHostValid.Value) + throw new Exception("Security validation exception: Webhook packet source address failed cluster validation checks."); + + var isAmountValid = payfastService.ValidatePaymentAmount(orderResult.Value.Total, payload.AmountGross); + + if (!isAmountValid.Value) + throw new Exception("Security validation exception: Transaction cost variance bounds breached."); + + var paramList = new List(); + + foreach (var kvp in dict) + { + if (!string.IsNullOrEmpty(kvp.Value)) + { + string encoded = HttpUtility.UrlEncode(kvp.Value.Trim()); + + string safeValue = PayfastService.PercentEncodingRegex.Replace(encoded, m => m.Value.ToLowerInvariant()); + paramList.Add($"{kvp.Key}={safeValue}"); + } + } + + string rawParamString = string.Join("&", paramList); + + var serverConfirmation = await payfastService.ValidateServerConfirmationAsync(rawParamString, isSandbox: true, cancellationToken); + + if (serverConfirmation.IsFailed) + throw new Exception("Security validation exception: Payfast central handshake server rejected payload legitimacy."); + } + + await payfastService.WriteLedgerEntryAsync(new CreateGatewayLedgerEntry { - CustomerId = orderResult.Value.CustomerId, OrderId = orderResult.Value.Id, PaymentId = paymentResult.Value.Id, - Status = LedgerStatuses.Received, - PaymentGatewayId = 1, - PaymentGatewayReference = notification.CorrelationId, + MerchantPaymentId = payload.MerchantPaymentId!, + PayfastPaymentId = payload.PaymentId, + CustomerEmail = payload.EmailAddress, + AmountFee = fee, + AmountGross = gross, + AmountNet = net, + PaymentStatus = status, }, cancellationToken); - if (ledgerResult.IsFailed) + if (status.Equals("COMPLETE", StringComparison.OrdinalIgnoreCase)) { - logger.LogError("Failed to write ledger entry for payment ID: {PaymentId}. Errors: {Errors}", notification.Payload?.MPaymentId, string.Join(", ", ledgerResult.Errors.Select(e => e.Message))); - throw new Exception($"Failed to write ledger entry for payment ID: {notification.Payload?.MPaymentId}."); - } + var ledgerWriteResult = await paymentService.WriteLedgerEntryAsync(new CreateLedgerEntry + { + OrderId = orderResult.Value.Id, + PaymentId = paymentResult.Value.Id, + PaymentGatewayReference = payload.PaymentId!, + Status = LedgerStatuses.Completed, + CustomerId = orderResult.Value.CustomerId, + }, cancellationToken); - var paymentCompletedResult = await paymentService.CompletePaymentAsync(paymentResult.Value.Id, PaymentStatuses.Paid, cancellationToken); - if (paymentCompletedResult.IsFailed) + if (ledgerWriteResult.IsFailed) + throw new Exception("Failed to write ledger entry for payment confirmation."); + + var completePaymentResult = await paymentService.CompletePaymentAsync(paymentResult.Value.Id, PaymentStatuses.Paid, cancellationToken); + + if (completePaymentResult.IsFailed) + throw new Exception("Failed to update payment status to 'Paid' for payment confirmation."); + + var updateOrderResult = await orderService.UpdateOrderStatusAsync(orderResult.Value.Id, OrderStatus.Completed, cancellationToken); + + if (updateOrderResult.IsFailed) + throw new Exception("Failed to update order status to 'Completed' for payment confirmation."); + + logger.LogInformation("Order payment verified secure and cleared successfully."); + } + else { - logger.LogError("Failed to complete payment for order ID: {OrderId}. Errors: {Errors}", orderResult.Value.Id, string.Join(", ", paymentCompletedResult.Errors.Select(e => e.Message))); - throw new Exception($"Failed to complete payment for order ID: {orderResult.Value.Id}."); + LedgerStatuses ledgerStatus; + + if (status.Equals("CANCELLED", StringComparison.OrdinalIgnoreCase)) + ledgerStatus = LedgerStatuses.Cancelled; + else + ledgerStatus = LedgerStatuses.Failed; + + var ledgerWriteResult = await paymentService.WriteLedgerEntryAsync(new CreateLedgerEntry + { + OrderId = orderResult.Value.Id, + PaymentId = paymentResult.Value.Id, + PaymentGatewayReference = payload.PaymentId!, + Status = ledgerStatus, + CustomerId = orderResult.Value.CustomerId, + }, cancellationToken); + + logger.LogInformation("Webhook validation pipeline passed checks successfully, logged entry to ledger with status: {Status}", status); } - - var orderCompletedResult = await orderService.UpdateOrderStatusAsync(orderResult.Value.Id, OrderStatus.Completed, cancellationToken); - if (orderCompletedResult.IsFailed) - { - logger.LogError("Failed to update order status to Completed for order ID: {OrderId}. Errors: {Errors}", orderResult.Value.Id, string.Join(", ", orderCompletedResult.Errors.Select(e => e.Message))); - throw new Exception($"Failed to update order status to Completed for order ID: {orderResult.Value.Id}."); - } - - logger.LogInformation("Received Payfast payment confirmation for payment ID: {PaymentId}", notification.Payload?.MPaymentId); - - // TODO: Publish MediatR notifications or queue downstream Quartz jobs (Discord, Shipping, Customer Email, Royalties) } } diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs index b322753..b95d292 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs @@ -1,5 +1,5 @@ using LiteCharms.Features.Abstractions; -using LiteCharms.Features.Models; +using LiteCharms.Features.MidrandBooks.Payments.Models; namespace LiteCharms.Features.MidrandBooks.Payments.Events; @@ -9,14 +9,19 @@ public sealed class PayfastPaymentConfirmationReceivedEvent : EventBase, IEvent public PayfastWebhookPayload? Payload { get; set; } + public string? RemoteIpAddress { get; set; } + + public bool PerformBackgroundChecks { get; set; } + public PayfastPaymentConfirmationReceivedEvent() { } - private PayfastPaymentConfirmationReceivedEvent(PayfastWebhookPayload? payload, string paymentId) + private PayfastPaymentConfirmationReceivedEvent(PayfastWebhookPayload? payload, string paymentId, bool performBackgroundChecks = true) { Payload = payload; CorrelationId = paymentId; + PerformBackgroundChecks = performBackgroundChecks; } - public static PayfastPaymentConfirmationReceivedEvent Create(PayfastWebhookPayload? payload, string paymentId) => - new(payload, paymentId); + public static PayfastPaymentConfirmationReceivedEvent Create(PayfastWebhookPayload? payload, string paymentId, bool performBackgroundChecks = true) => + new(payload, paymentId, performBackgroundChecks); } diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PayfastWebhookPayload.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PayfastWebhookPayload.cs new file mode 100644 index 0000000..b0d8da7 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PayfastWebhookPayload.cs @@ -0,0 +1,59 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public sealed class PayfastWebhookPayload +{ + public string? MerchantId { get; set; } + + public string? MerchantKey { get; set; } + + public string? Signature { get; set; } + + public string? MerchantPaymentId { get; set; } + + public string? PaymentId { get; set; } + + public string? PaymentStatus { get; set; } + + public string? ItemName { get; set; } + + public string? ItemDescription { get; set; } + + public string? AmountGross { get; set; } + + public string? AmountFee { get; set; } + + public string? AmountNet { get; set; } + + public string? NameFirst { get; set; } + + public string? NameLast { get; set; } + + public string? EmailAddress { get; set; } + + public string? CustomStr1 { get; set; } + + public string? CustomInt1 { get; set; } + + public string? Token { get; set; } + + public IDictionary ToParamDictionary() => new Dictionary + (StringComparer.Ordinal) + { + { "merchant_id", MerchantId }, + { "merchant_key", MerchantKey }, + { "m_payment_id", MerchantPaymentId }, + { "pf_payment_id", PaymentId }, + { "payment_status", PaymentStatus }, + { "item_name", ItemName }, + { "item_description", ItemDescription }, + { "amount_gross", AmountGross }, + { "amount_fee", AmountFee }, + { "amount_net", AmountNet }, + { "custom_str1", CustomStr1 }, + { "custom_int1", CustomInt1 }, + { "name_first", NameFirst }, + { "name_last", NameLast }, + { "email_address", EmailAddress }, + { "token", Token } + }; +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGatewayLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGatewayLedger.cs new file mode 100644 index 0000000..2e27057 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGatewayLedger.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class PaymentGatewayLedger +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public string? CustomerEmail { get; set; } + + public long OrderId { get; set; } + + public long PaymentId { get; set; } + + public string? MerchantPaymentId { get; set; } + + public string? PayfastPaymentId { get; set; } + + public string? PaymentStatus { get; set; } + + public decimal AmountGross { get; set; } + + public decimal AmountFee { get; set; } + + public decimal AmountNet { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs index 33f3d25..d650698 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs @@ -14,7 +14,5 @@ public class PaymentLedger public long CustomerId { get; set; } - public string? PaymentGatewayReference { get; set; } - - public long? PaymentGatewayId { get; set; } + public string? MerchantPaymentId { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs index 2326397..f403b20 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs @@ -1,5 +1,26 @@ namespace LiteCharms.Features.MidrandBooks.Payments.Models; +public sealed record CreateGatewayLedgerEntry +{ + public string? CustomerEmail { get; set; } + + public required long OrderId { get; set; } + + public required long PaymentId { get; set; } + + public string? MerchantPaymentId { get; set; } + + public string? PayfastPaymentId { get; set; } + + public string? PaymentStatus { get; set; } + + public decimal AmountGross { get; set; } + + public decimal AmountFee { get; set; } + + public decimal AmountNet { get; set; } +} + public sealed record UpdateRefund { public long OrderId { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs new file mode 100644 index 0000000..3329c99 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs @@ -0,0 +1,171 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Hasher; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Payments; + +public sealed partial class PayfastService(IDbContextFactory contextFactory, + ILogger logger, IHttpClientFactory httpClientFactory, IConfiguration configuration) : IService +{ + [GeneratedRegex(@"%[0-9A-Fa-f]{2}", RegexOptions.None, matchTimeoutMilliseconds: 1000)] + public static partial Regex PercentEncodingRegex { get; } + + public readonly string[] ValidHosts = configuration.GetSection("ValidPayfastHosts").Get() ?? []; + + public async ValueTask> WriteLedgerEntryAsync(CreateGatewayLedgerEntry request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) + return Result.Fail("Referenced order ID does not exist in database."); + + if(!await context.Payments.AnyAsync(p => p.Id == request.PaymentId, cancellationToken)) + return Result.Fail("Referenced payment ID does not exist in database."); + + var entry = context.GatewayLedger.Add(new Entities.PaymentGatewayLedger + { + CustomerEmail = request.CustomerEmail, + OrderId = request.OrderId, + PaymentId = request.PaymentId, + MerchantPaymentId = request.MerchantPaymentId, + PayfastPaymentId = request.PayfastPaymentId, + PaymentStatus = request.PaymentStatus, + AmountGross = request.AmountGross, + AmountFee = request.AmountFee, + AmountNet = request.AmountNet, + CreatedAt = DateTime.UtcNow, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(entry.Entity.Id) + : Result.Fail("Failed to save Payfast ledger entry to database."); + } + catch (Exception ex) + { + return Result.Fail(new Error("Failed to write Payfast ledger entry to database.").CausedBy(ex)); + } + } + + public async ValueTask> ValidateReferrerIpAsync(string remoteIpAddress, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(remoteIpAddress)) + return Result.Fail("Remote IP address is null or whitespace."); + + try + { + var validIps = new HashSet(); + + foreach (var host in ValidHosts) + { + try + { + var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken); + + foreach (var addr in addresses) validIps.Add(addr); + } + catch (SocketException ex) + { + logger.LogWarning(ex, "DNS warning: Failed to resolve Payfast node '{Host}'. It may be decommissioned or unreachable.", host); + } + } + + if (IPAddress.TryParse(remoteIpAddress, out var incomingIp)) + { + bool isValid = validIps.Contains(incomingIp); + + if (!isValid) + logger.LogWarning("SECURITY ALERT: Webhook IP '{RemoteIp}' originated from an unlisted host schema.", remoteIpAddress); + + return Result.Ok(isValid); + } + + return Result.Fail("Invalid remote IP address format."); + } + catch (Exception ex) + { + return Result.Fail(new Error("DNS Verification error while scanning Payfast IP nodes.").CausedBy(ex)); + } + } + + public Result ValidatePaymentAmount(decimal expectedTotal, string? amountGrossString) + { + if (!decimal.TryParse(amountGrossString, CultureInfo.InvariantCulture, out decimal grossAmount)) + return Result.Fail("Failed to parse payment amount."); + + decimal delta = Math.Abs(expectedTotal - grossAmount); + + bool isAmountValid = delta <= 0.01m; + + if (!isAmountValid) + logger.LogError("FINANCIAL DRIFT EXCEPTION: Expected order total R{Expected} but gateway cleared R{Cleared}.", expectedTotal, grossAmount); + + return Result.Ok(isAmountValid); + } + + public async ValueTask> ValidateServerConfirmationAsync(string rawQueryParamString, bool isSandbox, CancellationToken ct) + { + try + { + string host = isSandbox ? "sandbox.payfast.co.za" : "www.payfast.co.za"; + string targetUrl = $"https://{host}/eng/query/validate"; + + using var content = new StringContent(rawQueryParamString, Encoding.UTF8, "application/x-www-form-urlencoded"); + + var httpClient = httpClientFactory.CreateClient(); + + var response = await httpClient.PostAsync(targetUrl, content, ct); + + if (!response.IsSuccessStatusCode) return Result.Fail("Failed to validate server confirmation."); + + string responseText = await response.Content.ReadAsStringAsync(ct); + + bool isValidated = string.Equals(responseText.Trim(), "VALID", StringComparison.OrdinalIgnoreCase); + + if (!isValidated) + logger.LogWarning("SECURITY WARNING: Payfast back-channel returned validation response: '{Response}'", responseText); + + return Result.Ok(isValidated); + } + catch (Exception ex) + { + return Result.Fail(new Error("Failed to complete back-channel cURL verification handshakes with Payfast remote endpoints.").CausedBy(ex)); + } + } + + public static Result GenerateSignature(IDictionary data, string? passPhrase = null) + { + var pfOutput = new StringBuilder(); + + foreach (var kvp in data) + { + if (string.IsNullOrEmpty(kvp.Value)) + continue; + + string key = kvp.Key; + + string encodedVal = HttpUtility.UrlEncode(kvp.Value.Trim()); + + string val = PercentEncodingRegex.Replace(encodedVal, m => m.Value.ToLowerInvariant()); + + pfOutput.Append($"{key}={val}&"); + } + + string getString = pfOutput.Length > 0 + ? pfOutput.ToString()[..^1] + : string.Empty; + + if (!string.IsNullOrWhiteSpace(passPhrase)) + { + string encodedPassphrase = HttpUtility.UrlEncode(passPhrase.Trim()); + + string safePassphrase = PercentEncodingRegex.Replace(encodedPassphrase, m => m.Value.ToLowerInvariant()); + + getString += $"&passphrase={safePassphrase}"; + } + + return HashService.ToMd5Hash(getString); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs index 7904491..97e9e21 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -116,7 +116,7 @@ public sealed class PaymentService(IDbContextFactory cont } } - public async ValueTask> HasLedgerEntryAsync(long orderId, long paymentId, long gatewayId, CancellationToken cancellationToken = default) + public async ValueTask> HasLedgerEntryAsync(long orderId, long paymentId, CancellationToken cancellationToken = default) { try { @@ -124,8 +124,7 @@ public sealed class PaymentService(IDbContextFactory cont var exists = await context.Ledger.AnyAsync(l => l.OrderId == orderId && - l.PaymentId == paymentId && - l.PaymentGatewayId == gatewayId, cancellationToken); + l.PaymentId == paymentId, cancellationToken); return Result.Ok(exists); } @@ -162,8 +161,6 @@ public sealed class PaymentService(IDbContextFactory cont CreatedAt = DateTime.UtcNow, CustomerId = request.CustomerId, OrderId = request.OrderId, - PaymentGatewayId = request.PaymentGatewayId, - PaymentGatewayReference = request.PaymentGatewayReference, PaymentId = request.PaymentId, Status = request.Status, }); diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index 3d0b380..822b1c0 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -48,4 +48,6 @@ public sealed class MidrandBooksDbContext(DbContextOptions Gateways => Set(); public DbSet Ledger => Set(); + + public DbSet GatewayLedger => Set(); } diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.Designer.cs new file mode 100644 index 0000000..6266a05 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.Designer.cs @@ -0,0 +1,1291 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260602202421_AddedPaymentGatewayLedger")] + partial class AddedPaymentGatewayLedger + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountGross") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountNet") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerEmail") + .HasColumnType("text"); + + b.Property("MerchantPaymentId") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PayfastPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("GatewayLedger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("MerchantPaymentId") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.cs new file mode 100644 index 0000000..b4cc909 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.cs @@ -0,0 +1,108 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedPaymentGatewayLedger : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Ledger_Gateways_PaymentGatewayId", + table: "Ledger"); + + migrationBuilder.DropIndex( + name: "IX_Ledger_PaymentGatewayId", + table: "Ledger"); + + migrationBuilder.DropColumn( + name: "PaymentGatewayId", + table: "Ledger"); + + migrationBuilder.RenameColumn( + name: "PaymentGatewayReference", + table: "Ledger", + newName: "MerchantPaymentId"); + + migrationBuilder.CreateTable( + name: "GatewayLedger", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + CustomerEmail = table.Column(type: "text", nullable: true), + OrderId = table.Column(type: "bigint", nullable: false), + PaymentId = table.Column(type: "bigint", nullable: false), + MerchantPaymentId = table.Column(type: "text", nullable: true), + PayfastPaymentId = table.Column(type: "text", nullable: false), + PaymentStatus = table.Column(type: "text", nullable: true), + AmountGross = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + AmountFee = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + AmountNet = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GatewayLedger", x => x.Id); + table.ForeignKey( + name: "FK_GatewayLedger_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GatewayLedger_Payments_PaymentId", + column: x => x.PaymentId, + principalTable: "Payments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_GatewayLedger_OrderId", + table: "GatewayLedger", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_GatewayLedger_PaymentId", + table: "GatewayLedger", + column: "PaymentId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GatewayLedger"); + + migrationBuilder.RenameColumn( + name: "MerchantPaymentId", + table: "Ledger", + newName: "PaymentGatewayReference"); + + migrationBuilder.AddColumn( + name: "PaymentGatewayId", + table: "Ledger", + type: "bigint", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_PaymentGatewayId", + table: "Ledger", + column: "PaymentGatewayId"); + + migrationBuilder.AddForeignKey( + name: "FK_Ledger_Gateways_PaymentGatewayId", + table: "Ledger", + column: "PaymentGatewayId", + principalTable: "Gateways", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.Designer.cs new file mode 100644 index 0000000..6ccd5e5 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.Designer.cs @@ -0,0 +1,1292 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger")] + partial class AddedPayfastPaymentIdToPaymentGatewayLedger + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountGross") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountNet") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerEmail") + .HasColumnType("text"); + + b.Property("MerchantPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PayfastPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("GatewayLedger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("MerchantPaymentId") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.cs new file mode 100644 index 0000000..e0853da --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedPayfastPaymentIdToPaymentGatewayLedger : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "MerchantPaymentId", + table: "GatewayLedger", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "MerchantPaymentId", + table: "GatewayLedger", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 79de829..8ddc085 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -615,6 +615,60 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.ToTable("Gateways", (string)null); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountGross") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountNet") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerEmail") + .HasColumnType("text"); + + b.Property("MerchantPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PayfastPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("GatewayLedger", (string)null); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => { b.Property("Id") @@ -631,15 +685,12 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Property("CustomerId") .HasColumnType("bigint"); + b.Property("MerchantPaymentId") + .HasColumnType("text"); + b.Property("OrderId") .HasColumnType("bigint"); - b.Property("PaymentGatewayId") - .HasColumnType("bigint"); - - b.Property("PaymentGatewayReference") - .HasColumnType("text"); - b.Property("PaymentId") .HasColumnType("bigint"); @@ -652,8 +703,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.HasIndex("OrderId"); - b.HasIndex("PaymentGatewayId"); - b.HasIndex("PaymentId"); b.ToTable("Ledger", (string)null); @@ -1062,6 +1111,25 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") @@ -1076,11 +1144,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", "Gateway") - .WithMany() - .HasForeignKey("PaymentGatewayId") - .OnDelete(DeleteBehavior.Cascade); - b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") .WithMany() .HasForeignKey("PaymentId") @@ -1089,8 +1152,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Customer"); - b.Navigation("Gateway"); - b.Navigation("Order"); b.Navigation("Payment"); diff --git a/LiteCharms.Features.Tests/HashServiceFeatureTests.cs b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs index 6d85422..844c7fb 100644 --- a/LiteCharms.Features.Tests/HashServiceFeatureTests.cs +++ b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs @@ -1,13 +1,10 @@ using LiteCharms.Features.Hasher; -using LiteCharms.Features.Models; -using static LiteCharms.Features.Extensions.Hash; namespace LiteCharms.Features.Tests; public class HashServiceFeatureTests(Fixture fixture) : IClassFixture { private readonly HashService hashService = fixture.Services.GetRequiredService(); - private readonly string payfastPassphrase = fixture.Configuration.GetSection("HasherSettings:PayfastPassphrase").Value!; [Fact] public void StringToSha256Hash_Should_GenerateHash() @@ -62,28 +59,6 @@ public class HashServiceFeatureTests(Fixture fixture) : IClassFixture Assert.Equal(expectedMd5Lowercase, result.Value); } - [Fact] - public void VerifyPayfastWebhookSignature_Should_GenerateHash() - { - var paymentId = hashService.HashEncodeLongId(1001).Value; - - var payload = new PayfastWebhookPayload - { - Amount = "350.00", - ItemName = "System Architecture Book", - MPaymentId = paymentId, - }; - - var rawPayload = payload.ToRawPayfastPayload(payfastPassphrase); - - var generatedSignature = HashService.ToMd5Hash(rawPayload).Value; - - var result = hashService.VerifyPayfastWebhookSignature(payload, generatedSignature); - - Assert.True(result.IsSuccess); - Assert.True(result.Value); - } - [Fact] public void HashEncodeHex_Should_GenerateHash() { diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs index 786d3e5..296848e 100644 --- a/LiteCharms.Features/Enums.cs +++ b/LiteCharms.Features/Enums.cs @@ -19,6 +19,7 @@ public enum LedgerStatuses : int Cancelled = 4, Failed = 5, Partial = 6, + Completed = 7, } public enum PaymentStatuses : int diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs index 78f2c6c..555c423 100644 --- a/LiteCharms.Features/Extensions/Hash.cs +++ b/LiteCharms.Features/Extensions/Hash.cs @@ -1,6 +1,5 @@ using LiteCharms.Features.Hasher; using LiteCharms.Features.Hasher.Configuration; -using LiteCharms.Features.Models; namespace LiteCharms.Features.Extensions; @@ -20,83 +19,5 @@ public static class Hash services.AddSingleton(); return services; - } - - public static string ToRawPayfastPayload(this PayfastWebhookPayload input, string passphrase) - { - var parameters = new List(); - - if (!string.IsNullOrWhiteSpace(input.Amount)) - parameters.Add($"amount={WebUtility.UrlEncode(input.Amount)}"); - - if (!string.IsNullOrWhiteSpace(input.ItemName)) - parameters.Add($"item_name={WebUtility.UrlEncode(input.ItemName)}"); - - if (!string.IsNullOrWhiteSpace(input.MPaymentId)) - parameters.Add($"m_payment_id={WebUtility.UrlEncode(input.MPaymentId)}"); - - string payload = string.Join("&", parameters); - - if (!string.IsNullOrWhiteSpace(passphrase)) - payload += $"&passphrase={WebUtility.UrlEncode(passphrase)}"; - - return payload; - } - - public static (PayfastWebhookPayload Payload, string Passphrase) FromRawPayfastPayload(this string rawPayload) - { - string passphrase = string.Empty; - var payload = new PayfastWebhookPayload(); - - if (string.IsNullOrWhiteSpace(rawPayload)) return (payload, passphrase); - - var segments = rawPayload.Split('&', StringSplitOptions.RemoveEmptyEntries); - - foreach (var segment in segments) - { - int delimiterIndex = segment.IndexOf('='); - if (delimiterIndex == -1) - continue; - - string key = segment[..delimiterIndex].Trim(); - string rawValue = segment[(delimiterIndex + 1)..]; - - string decodedValue = WebUtility.UrlDecode(rawValue); - - switch (key.ToLowerInvariant()) - { - case "amount": - payload.Amount = decodedValue; - break; - case "item_name": - payload.ItemName = decodedValue; - break; - case "m_payment_id": - payload.MPaymentId = decodedValue; - break; - case "passphrase": - passphrase = decodedValue; - break; - } - } - - return (payload, passphrase); - } - - public static (PayfastWebhookPayload Payload, string Passphrase) FromRawPayfastPayload(this IFormCollection form) - { - string passphrase = string.Empty; - var payload = new PayfastWebhookPayload(); - - if (form.IsNullOrEmpty()) return (payload, passphrase); - - payload = new PayfastWebhookPayload - { - Amount = form.TryGetValue("amount", out var amountValues) ? amountValues.ToString() : null, - ItemName = form.TryGetValue("item_name", out var itemValues) ? itemValues.ToString() : null, - MPaymentId = form.TryGetValue("m_payment_id", out var paymentIdValues) ? paymentIdValues.ToString() : null, - }; - - return (payload, passphrase); - } + } } \ No newline at end of file diff --git a/LiteCharms.Features/Hasher/HashService.cs b/LiteCharms.Features/Hasher/HashService.cs index 1b38ed7..660c9e5 100644 --- a/LiteCharms.Features/Hasher/HashService.cs +++ b/LiteCharms.Features/Hasher/HashService.cs @@ -1,13 +1,9 @@ using LiteCharms.Features.Abstractions; -using LiteCharms.Features.Hasher.Configuration; -using LiteCharms.Features.Models; namespace LiteCharms.Features.Hasher; -public sealed partial class HashService(IHashids hasher, IOptions options) : IService +public sealed partial class HashService(IHashids hasher) : IService { - private readonly HasherSettings settings = options.Value; - [GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")] private static partial Regex HexHashRegex { get; } @@ -41,44 +37,6 @@ public sealed partial class HashService(IHashids hasher, IOptions VerifyPayfastWebhookSignature(PayfastWebhookPayload payload, string incomingSignature) - { - try - { - if (string.IsNullOrWhiteSpace(incomingSignature)) - return Result.Fail("Validation failed: Missing signature string parameter."); - - var parameters = new List(); - - if (!string.IsNullOrWhiteSpace(payload.Amount)) - parameters.Add($"amount={WebUtility.UrlEncode(payload.Amount)}"); - - if (!string.IsNullOrWhiteSpace(payload.ItemName)) - parameters.Add($"item_name={WebUtility.UrlEncode(payload.ItemName)}"); - - if (!string.IsNullOrWhiteSpace(payload.MPaymentId)) - parameters.Add($"m_payment_id={WebUtility.UrlEncode(payload.MPaymentId)}"); - - string signatureString = string.Join("&", parameters); - - if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase)) - signatureString += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}"; - - var localHashResult = ToMd5Hash(signatureString); - - if (!localHashResult.IsSuccess) - return Result.Fail(localHashResult.Errors); - - bool isValid = string.Equals(localHashResult.Value, incomingSignature, StringComparison.OrdinalIgnoreCase); - - return Result.Ok(isValid); - } - catch (Exception ex) - { - return Result.Fail(new Error("An error occurred during Payfast MD5 verification.").CausedBy(ex)); - } - } - public Result HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input) ? Result.Fail("Input must be a valid hexadecimal string.") : Result.Ok(hasher.EncodeHex(input)); diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 087c9e4..30e1c10 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -147,6 +147,7 @@ + diff --git a/LiteCharms.Features/Models/PayfastWebhookPayload.cs b/LiteCharms.Features/Models/PayfastWebhookPayload.cs deleted file mode 100644 index 6b0a5db..0000000 --- a/LiteCharms.Features/Models/PayfastWebhookPayload.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LiteCharms.Features.Models; - -public sealed class PayfastWebhookPayload -{ - public string? Amount { get; set; } - public string? ItemName { get; set; } - public string? MPaymentId { get; set; } -} From 763d24f11fcaadedacdc71b529c7c585e6f15112 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 2 Jun 2026 23:47:10 +0200 Subject: [PATCH 088/153] Updated nuget packages --- .../LiteCharms.Features.MidrandBooks.Seed.csproj | 4 ++-- .../LiteCharms.Features.MidrandBooks.Tests.csproj | 2 +- .../LiteCharms.Features.MidrandBooks.csproj | 4 ++-- .../LiteCharms.Features.TechShop.Tests.csproj | 2 +- .../LiteCharms.Features.TechShop.csproj | 10 +++++----- .../LiteCharms.Features.Tests.csproj | 2 +- LiteCharms.Features/LiteCharms.Features.csproj | 12 ++++++------ 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj index b9be4ff..d8ac700 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj +++ b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj @@ -116,8 +116,8 @@ - - + + diff --git a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj index 174bbd1..a4b86f0 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj +++ b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj @@ -17,7 +17,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 1206d52..3db773c 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -136,8 +136,8 @@ - - + + diff --git a/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj b/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj index 8ad847f..71063cb 100644 --- a/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj +++ b/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj @@ -17,7 +17,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all diff --git a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj index 0318fd4..b99f168 100644 --- a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj +++ b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj @@ -105,7 +105,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -116,8 +116,8 @@ - - + + @@ -136,8 +136,8 @@ - - + + diff --git a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj index 015c0b1..2540fae 100644 --- a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj +++ b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj @@ -17,7 +17,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 30e1c10..8e12ef0 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -32,7 +32,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -105,7 +105,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -116,8 +116,8 @@ - - + + @@ -136,8 +136,8 @@ - - + + From 4df903e45621ac96c749fa63635ed1033ebef260 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 00:20:46 +0200 Subject: [PATCH 089/153] Added shared api feature --- LiteCharms.Features/Abstractions/IEndpoint.cs | 6 ++ .../Api/ApiVersionTargetAttribute.cs | 7 ++ .../OpenApiBearerSecuritySchemeTransformer.cs | 16 ++++ .../Email/Models/EmailEnquiryModel.cs | 8 +- LiteCharms.Features/Extensions/Api.cs | 96 +++++++++++++++++++ .../LiteCharms.Features.csproj | 23 +++++ 6 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 LiteCharms.Features/Abstractions/IEndpoint.cs create mode 100644 LiteCharms.Features/Api/ApiVersionTargetAttribute.cs create mode 100644 LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs create mode 100644 LiteCharms.Features/Extensions/Api.cs diff --git a/LiteCharms.Features/Abstractions/IEndpoint.cs b/LiteCharms.Features/Abstractions/IEndpoint.cs new file mode 100644 index 0000000..25bb977 --- /dev/null +++ b/LiteCharms.Features/Abstractions/IEndpoint.cs @@ -0,0 +1,6 @@ +namespace LiteCharms.Features.Abstractions; + +public interface IEndpoint +{ + void Map(IEndpointRouteBuilder builder); +} diff --git a/LiteCharms.Features/Api/ApiVersionTargetAttribute.cs b/LiteCharms.Features/Api/ApiVersionTargetAttribute.cs new file mode 100644 index 0000000..63d1598 --- /dev/null +++ b/LiteCharms.Features/Api/ApiVersionTargetAttribute.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.Api; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class ApiVersionTargetAttribute(int majorVersion) : Attribute +{ + public int MajorVersion { get; } = majorVersion; +} diff --git a/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs b/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs new file mode 100644 index 0000000..41ef59a --- /dev/null +++ b/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Features.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. Example: \"Bearer {token}\"", + }; + + document.AddComponent("Bearer", bearerScheme); + } +} diff --git a/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs b/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs index b13e3a7..499f9c1 100644 --- a/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs +++ b/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs @@ -7,25 +7,25 @@ public sealed class EmailEnquiryModel [Required] [MinLength(2)] [MaxLength(255)] - [Display(Name = "Full Name")] + [System.ComponentModel.DataAnnotations.Display(Name = "Full Name")] public string? FullName { get; set; } [Required] [EmailAddress] [MinLength(5)] [MaxLength(255)] - [Display(Name = "Email Address")] + [System.ComponentModel.DataAnnotations.Display(Name = "Email Address")] public string? EmailAddress { get; set; } [Required] [MinLength(2)] [MaxLength(255)] - [Display(Name = "Subject")] + [System.ComponentModel.DataAnnotations.Display(Name = "Subject")] public string? EmailSubject { get; set; } [Required] [MinLength(2)] [MaxLength(2000)] - [Display(Name = "Message")] + [System.ComponentModel.DataAnnotations.Display(Name = "Message")] public string? Message { get; set; } } diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs new file mode 100644 index 0000000..df8fe23 --- /dev/null +++ b/LiteCharms.Features/Extensions/Api.cs @@ -0,0 +1,96 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Api; + +namespace LiteCharms.Features.Extensions; + +public static class Api +{ + public const string Books = nameof(Books); + public const string Payments = nameof(Payments); + + public static IApplicationBuilder MapEndpoints(this WebApplication app, IDictionary versionGroups) + { + var endpoints = app.Services.GetRequiredService>(); + + foreach (var endpoint in endpoints) + { + var versionAttributes = endpoint.GetType().GetCustomAttributes().ToList(); + + if (versionAttributes.Count != 0) + { + foreach (var attr in versionAttributes) + if (versionGroups.TryGetValue(attr.MajorVersion, out var targetGroup)) + endpoint.Map(targetGroup); + } + else + endpoint.Map(app); + } + + return app; + } + + public static IServiceCollection AddEndpoints(this IServiceCollection services, Assembly assembly) + { + ServiceDescriptor[] discriptors = [.. assembly.DefinedTypes + .Where(t => t is { IsInterface: false, IsAbstract: false }) + .Where(t => t.IsAssignableTo(typeof(IEndpoint))) + .Select(t => ServiceDescriptor.Transient(typeof(IEndpoint), t))]; + + services.TryAddEnumerable(discriptors); + + return services; + } + + public static string ToEndpointName(this Type target, string? annotation = "") => + $"{target.Name.Replace("Endpoint", string.Empty)}{annotation}".ToLower(CultureInfo.CurrentCulture); + + public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddHttpClient(); + + services.AddApiVersioning(options => + { + options.DefaultApiVersion = new ApiVersion(1); + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), + new QueryStringApiVersionReader("version"), + new QueryStringApiVersionReader("version"), + new MediaTypeApiVersionReader("version")); + }) + .AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }); + + var urls = configuration["ASPNETCORE_URLS"] ?? configuration["Urls"]; + var healthUrl = "http://localhost:8080/health"; + + if (!string.IsNullOrWhiteSpace(urls)) + { + string firstUrl = urls.Split(';').FirstOrDefault(s => s.Contains("http://"))! + .Replace("*", "localhost").Replace("+", "localhost"); + + healthUrl = $"{firstUrl.TrimEnd('/')}/health"; + } + + services.AddHealthChecksUI(setup => + { + setup.SetNotifyUnHealthyOneTimeUntilChange(); + setup.AddHealthCheckEndpoint("primary, heal", healthUrl); + setup.SetHeaderText("Midrand Books"); + }) + .AddInMemoryStorage(); + + services.AddOutputCache(options => + { + options.AddBasePolicy(builder => builder.Cache()); + options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); + }); + + services.AddOpenApi(options => options.AddDocumentTransformer()); + + return services; + } +} diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 8e12ef0..dd9ff14 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -29,6 +29,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -147,6 +167,9 @@ + + + From ad9fa0ab91ac38a3fd75ec637a1ffe9a86c5f09e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 00:21:57 +0200 Subject: [PATCH 090/153] Added http test folder to features test project --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9491a2f..6c9537f 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,5 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd +/LiteCharms.Features.Tests/http/http-client.env.json From 0a95df4c393aab953eb8445365ca76e0f0521406 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 00:22:44 +0200 Subject: [PATCH 091/153] Added midrand shop .http test folder --- .../http/midrandshop-api/app.http | 8 ++++++++ .../http/midrandshop-api/http-client.env.json | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 LiteCharms.Features.Tests/http/midrandshop-api/app.http create mode 100644 LiteCharms.Features.Tests/http/midrandshop-api/http-client.env.json diff --git a/LiteCharms.Features.Tests/http/midrandshop-api/app.http b/LiteCharms.Features.Tests/http/midrandshop-api/app.http new file mode 100644 index 0000000..7297a16 --- /dev/null +++ b/LiteCharms.Features.Tests/http/midrandshop-api/app.http @@ -0,0 +1,8 @@ +## Payfast Payment Confirmation +# This endpoint is used by Payfast to confirm the payment status of a transaction. +# It receives a POST request with the payment details and updates the order status accordingly. + +POST {{baseUrl}}/v1/payments/payfast/confirm +Content-Type: application/x-www-form-urlencoded + +amount={{amount}}&item_name={{item_name}}&m_payment_id={{paymentId}}&signature={{signature}} diff --git a/LiteCharms.Features.Tests/http/midrandshop-api/http-client.env.json b/LiteCharms.Features.Tests/http/midrandshop-api/http-client.env.json new file mode 100644 index 0000000..17e9789 --- /dev/null +++ b/LiteCharms.Features.Tests/http/midrandshop-api/http-client.env.json @@ -0,0 +1,16 @@ +{ + "local": { + "baseUrl": "https://localhost:7196", + "paymentId": "jdPB2zaKM3Z", + "signature": "6aeff59bb74f2448ff2c3d81b2ec95de", + "item_name": "System Architecture Book", + "amount": "350.00" + }, + "uat": { + "baseUrl": "https://api.uat.midrandbooks.co.za", + "paymentId": "jdPB2zaKM3Z", + "signature": "6aeff59bb74f2448ff2c3d81b2ec95de", + "item_name": "System Architecture Book", + "amount": "350.00" + } +} From 7961d934ba2d55dce7e5015a43c08b77929f3b58 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 00:37:59 +0200 Subject: [PATCH 092/153] Added loopback address whitelisting override --- .../PayfastPaymentConfirmationReceivedEventHandler.cs | 2 +- .../Events/PayfastPaymentConfirmationReceivedEvent.cs | 9 ++++++--- .../Payments/PayfastService.cs | 8 +++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs index fb785f8..dcd943a 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs @@ -61,7 +61,7 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi if (notification.PerformBackgroundChecks) { - var isHostValid = await payfastService.ValidateReferrerIpAsync(notification.RemoteIpAddress!, cancellationToken); + var isHostValid = await payfastService.ValidateReferrerIpAsync(notification.RemoteIpAddress!, notification.AllowLoopback, cancellationToken); if (isHostValid.IsFailed) throw new Exception("Security validation exception: Webhook packet source address failed cluster validation checks."); diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs index b95d292..edd0f71 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs @@ -13,15 +13,18 @@ public sealed class PayfastPaymentConfirmationReceivedEvent : EventBase, IEvent public bool PerformBackgroundChecks { get; set; } + public bool AllowLoopback { get; set; } + public PayfastPaymentConfirmationReceivedEvent() { } - private PayfastPaymentConfirmationReceivedEvent(PayfastWebhookPayload? payload, string paymentId, bool performBackgroundChecks = true) + private PayfastPaymentConfirmationReceivedEvent(PayfastWebhookPayload? payload, string paymentId, bool performBackgroundChecks = true, bool allowLoopback = false) { Payload = payload; CorrelationId = paymentId; PerformBackgroundChecks = performBackgroundChecks; + AllowLoopback = allowLoopback; } - public static PayfastPaymentConfirmationReceivedEvent Create(PayfastWebhookPayload? payload, string paymentId, bool performBackgroundChecks = true) => - new(payload, paymentId, performBackgroundChecks); + public static PayfastPaymentConfirmationReceivedEvent Create(PayfastWebhookPayload? payload, string paymentId, bool performBackgroundChecks = true, bool allowLoopback = false) => + new(payload, paymentId, performBackgroundChecks, allowLoopback); } diff --git a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs index 3329c99..7167285 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs @@ -49,7 +49,7 @@ public sealed partial class PayfastService(IDbContextFactory> ValidateReferrerIpAsync(string remoteIpAddress, CancellationToken cancellationToken = default) + public async ValueTask> ValidateReferrerIpAsync(string remoteIpAddress, bool allowLoopback = false, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(remoteIpAddress)) return Result.Fail("Remote IP address is null or whitespace."); @@ -74,6 +74,12 @@ public sealed partial class PayfastService(IDbContextFactory Date: Wed, 3 Jun 2026 00:41:02 +0200 Subject: [PATCH 093/153] Fixed tests --- .../PayfastServiceFeatureTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs index 66da983..65f4eed 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs @@ -37,7 +37,7 @@ public sealed class PayfastServiceFeatureTests(Fixture fixture) : IClassFixture< string liveTargetIp = addresses.First().ToString(); - var result = await payfastService.ValidateReferrerIpAsync(liveTargetIp, fixture.CancellationToken); + var result = await payfastService.ValidateReferrerIpAsync(liveTargetIp, true, fixture.CancellationToken); Assert.True(result.IsSuccess); Assert.True(result.Value); @@ -48,7 +48,7 @@ public sealed class PayfastServiceFeatureTests(Fixture fixture) : IClassFixture< { string rogueIp = "8.8.8.8"; - var result = await payfastService.ValidateReferrerIpAsync(rogueIp, fixture.CancellationToken); + var result = await payfastService.ValidateReferrerIpAsync(rogueIp, true, fixture.CancellationToken); Assert.True(result.IsSuccess); Assert.False(result.Value); @@ -87,7 +87,7 @@ public sealed class PayfastServiceFeatureTests(Fixture fixture) : IClassFixture< var result = await payfastService.ValidateServerConfirmationAsync(arbitraryParameters, isSandbox: true, fixture.CancellationToken); - Assert.True(result.IsSuccess); + Assert.True(result.IsSuccess); Assert.False(result.Value); // Handshake data rejected as fraudulent/unrecognized } From 4bac14881dea230152476d3c9c207164990a63e4 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 00:50:20 +0200 Subject: [PATCH 094/153] Excluded http environment from checkin --- .gitignore | 1 + .../http/midrandshop-api/http-client.env.json | 16 ---------------- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 LiteCharms.Features.Tests/http/midrandshop-api/http-client.env.json diff --git a/.gitignore b/.gitignore index 6c9537f..75bddf9 100644 --- a/.gitignore +++ b/.gitignore @@ -362,3 +362,4 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd /LiteCharms.Features.Tests/http/http-client.env.json +/LiteCharms.Features.Tests/http/midrandshop-api/http-client.env.json diff --git a/LiteCharms.Features.Tests/http/midrandshop-api/http-client.env.json b/LiteCharms.Features.Tests/http/midrandshop-api/http-client.env.json deleted file mode 100644 index 17e9789..0000000 --- a/LiteCharms.Features.Tests/http/midrandshop-api/http-client.env.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "local": { - "baseUrl": "https://localhost:7196", - "paymentId": "jdPB2zaKM3Z", - "signature": "6aeff59bb74f2448ff2c3d81b2ec95de", - "item_name": "System Architecture Book", - "amount": "350.00" - }, - "uat": { - "baseUrl": "https://api.uat.midrandbooks.co.za", - "paymentId": "jdPB2zaKM3Z", - "signature": "6aeff59bb74f2448ff2c3d81b2ec95de", - "item_name": "System Architecture Book", - "amount": "350.00" - } -} From a0cf847e51e42829fb7a44ed1a42dae04b8deb68 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 10:40:29 +0200 Subject: [PATCH 095/153] Added job interruption handling --- ...PaymentConfirmationReceivedEventHandler.cs | 20 ++++++++----- .../Abstractions/IJobOrchestrator.cs | 12 ++++++++ LiteCharms.Features/Extensions/Quartz.cs | 4 +-- .../Quartz/Abstractions/IJobOrchestrator.cs | 12 -------- LiteCharms.Features/Quartz/JobOrchestrator.cs | 28 ++++++++++++++++--- LiteCharms.Features/Quartz/MediatorJob.cs | 23 +++++++++++---- .../Quartz/RetryJobListener.cs | 3 ++ 7 files changed, 71 insertions(+), 31 deletions(-) create mode 100644 LiteCharms.Features/Abstractions/IJobOrchestrator.cs delete mode 100644 LiteCharms.Features/Quartz/Abstractions/IJobOrchestrator.cs diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs index dcd943a..cae4c21 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs @@ -1,17 +1,21 @@ using LiteCharms.Features.Hasher; using LiteCharms.Features.Hasher.Configuration; +using LiteCharms.Features.Mediator; using LiteCharms.Features.MidrandBooks.Orders; using LiteCharms.Features.MidrandBooks.Payments.Models; namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers; -public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, IOptions hasherOptions, ILogger logger) : +public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, IOptions hasherOptions, ILogger logger) : INotificationHandler { private readonly HasherSettings hasherSettings = hasherOptions.Value; public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken) { + using var activity = MediatorTelemetry.Source.StartActivity($"Quartz: {typeof(PayfastPaymentConfirmationReceivedEvent).Name}"); + activity?.SetTag("event.correlation_id", notification.CorrelationId); + await using var scope = services.CreateAsyncScope(); var hashService = scope.ServiceProvider.GetRequiredService(); var orderService = scope.ServiceProvider.GetRequiredService(); @@ -23,7 +27,7 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi var dict = payload.ToParamDictionary(); var localSignature = PayfastService.GenerateSignature(dict, hasherSettings.PayfastPassphrase); - if(localSignature.IsFailed) + if (localSignature.IsFailed) throw new Exception("Failed to generate local signature for incoming webhook payload."); if (!string.Equals(localSignature.Value, payload.Signature, StringComparison.OrdinalIgnoreCase)) @@ -63,7 +67,7 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi { var isHostValid = await payfastService.ValidateReferrerIpAsync(notification.RemoteIpAddress!, notification.AllowLoopback, cancellationToken); - if (isHostValid.IsFailed) + if (isHostValid.IsFailed) throw new Exception("Security validation exception: Webhook packet source address failed cluster validation checks."); if (!isHostValid.Value) @@ -71,7 +75,7 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi var isAmountValid = payfastService.ValidatePaymentAmount(orderResult.Value.Total, payload.AmountGross); - if (!isAmountValid.Value) + if (!isAmountValid.Value) throw new Exception("Security validation exception: Transaction cost variance bounds breached."); var paramList = new List(); @@ -91,8 +95,8 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi var serverConfirmation = await payfastService.ValidateServerConfirmationAsync(rawParamString, isSandbox: true, cancellationToken); - if (serverConfirmation.IsFailed) - throw new Exception("Security validation exception: Payfast central handshake server rejected payload legitimacy."); + if (serverConfirmation.IsFailed) + throw new Exception("Security validation exception: Payfast central handshake server rejected payload legitimacy."); } await payfastService.WriteLedgerEntryAsync(new CreateGatewayLedgerEntry @@ -105,7 +109,7 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi AmountFee = fee, AmountGross = gross, AmountNet = net, - PaymentStatus = status, + PaymentStatus = status, }, cancellationToken); if (status.Equals("COMPLETE", StringComparison.OrdinalIgnoreCase)) @@ -154,5 +158,7 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi logger.LogInformation("Webhook validation pipeline passed checks successfully, logged entry to ledger with status: {Status}", status); } + activity?.SetStatus(ActivityStatusCode.Ok); + } } diff --git a/LiteCharms.Features/Abstractions/IJobOrchestrator.cs b/LiteCharms.Features/Abstractions/IJobOrchestrator.cs new file mode 100644 index 0000000..c0afd08 --- /dev/null +++ b/LiteCharms.Features/Abstractions/IJobOrchestrator.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.Abstractions; + +public interface IJobOrchestrator +{ + ValueTask SendAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : IEvent; + + ValueTask ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) + where TNotification : IEvent; + + ValueTask InterruptAsync(string eventName, string? correlationId = null, CancellationToken cancellationToken = default); +} diff --git a/LiteCharms.Features/Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs index 315a973..341d8c8 100644 --- a/LiteCharms.Features/Extensions/Quartz.cs +++ b/LiteCharms.Features/Extensions/Quartz.cs @@ -1,5 +1,5 @@ -using LiteCharms.Features.Quartz; -using LiteCharms.Features.Quartz.Abstractions; +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Quartz; using static LiteCharms.Features.Extensions.Postgres; namespace LiteCharms.Features.Extensions; diff --git a/LiteCharms.Features/Quartz/Abstractions/IJobOrchestrator.cs b/LiteCharms.Features/Quartz/Abstractions/IJobOrchestrator.cs deleted file mode 100644 index 8ce2c33..0000000 --- a/LiteCharms.Features/Quartz/Abstractions/IJobOrchestrator.cs +++ /dev/null @@ -1,12 +0,0 @@ -using LiteCharms.Features.Abstractions; - -namespace LiteCharms.Features.Quartz.Abstractions; - -public interface IJobOrchestrator -{ - Task SendAsync(TNotification notification, CancellationToken cancellationToken = default) - where TNotification : IEvent; - - Task ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) - where TNotification : IEvent; -} diff --git a/LiteCharms.Features/Quartz/JobOrchestrator.cs b/LiteCharms.Features/Quartz/JobOrchestrator.cs index 7c79b8a..629365e 100644 --- a/LiteCharms.Features/Quartz/JobOrchestrator.cs +++ b/LiteCharms.Features/Quartz/JobOrchestrator.cs @@ -1,11 +1,10 @@ using LiteCharms.Features.Abstractions; -using LiteCharms.Features.Quartz.Abstractions; namespace LiteCharms.Features.Quartz; public sealed class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator { - public async Task SendAsync(TNotification notification, CancellationToken cancellationToken = default) + public async ValueTask SendAsync(TNotification notification, CancellationToken cancellationToken = default) where TNotification : IEvent { var chainedJobGroup = "onetime-jobs"; @@ -23,13 +22,13 @@ public sealed class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOr var trigger = global::Quartz.TriggerBuilder.Create() .WithIdentity(triggerKey) - .StartNow() + .StartNow() .Build(); await scheduler.ScheduleJob(job, new List { trigger }.AsReadOnly(), replace: true, cancellationToken); } - public async Task ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) + public async ValueTask ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) where TNotification : IEvent { var chainedJobGroup = "scheduled-jobs"; @@ -63,4 +62,25 @@ public sealed class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOr else await scheduler.ScheduleJob(job, new List { trigger }.AsReadOnly(), replace: true, cancellationToken); } + + public async ValueTask InterruptAsync(string eventName, string? correlationId = null, CancellationToken cancellationToken = default) + { + var scheduler = await schedulerFactory.GetScheduler(cancellationToken); + + var jobKeyName = string.Empty; + var jobGroup = string.Empty; + + if (!string.IsNullOrWhiteSpace(correlationId)) + { + jobKeyName = $"{eventName.ToLower(CultureInfo.InvariantCulture)}-{correlationId.ToLower(CultureInfo.InvariantCulture)}"; + jobGroup = "onetime-jobs"; + } + else + { + jobKeyName = eventName.ToLower(CultureInfo.InvariantCulture); + jobGroup = "scheduled-jobs"; + } + + return await scheduler.Interrupt(JobKey.Create(jobKeyName, jobGroup), cancellationToken); + } } diff --git a/LiteCharms.Features/Quartz/MediatorJob.cs b/LiteCharms.Features/Quartz/MediatorJob.cs index 5fc7649..878d988 100644 --- a/LiteCharms.Features/Quartz/MediatorJob.cs +++ b/LiteCharms.Features/Quartz/MediatorJob.cs @@ -21,17 +21,28 @@ public sealed class MediatorJob(IMediator mediator) : IJob where if (notification is null) { - Trace.WriteLine("Notification could not be JSon converted from data string, job ended"); + Trace.WriteLine("Notification could not be Json converted from data string, job ended"); return; } - - using var activity = MediatorTelemetry.Source.StartActivity($"Quartz: {typeof(TNotification).Name}"); - + + using var activity = MediatorTelemetry.Source.StartActivity(typeof(TNotification).Name); + activity?.SetTag("event.correlation_id", notification.CorrelationId); - await mediator.Publish(notification, context.CancellationToken); + try + { + await mediator.Publish(notification, context.CancellationToken); - Trace.WriteLine("Job published"); + Trace.WriteLine("Job published successfully"); + } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + Trace.WriteLine($"Job '{typeof(TNotification).Name}' was gracefully interrupted by the cluster control plane."); + + activity?.SetStatus(ActivityStatusCode.Ok); + + return; + } } } diff --git a/LiteCharms.Features/Quartz/RetryJobListener.cs b/LiteCharms.Features/Quartz/RetryJobListener.cs index cc12662..1de4161 100644 --- a/LiteCharms.Features/Quartz/RetryJobListener.cs +++ b/LiteCharms.Features/Quartz/RetryJobListener.cs @@ -12,6 +12,9 @@ public sealed class RetryJobListener : IJobListener public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default) { + if (context.CancellationToken.IsCancellationRequested) + return; + if (jobException is not null && context.RefireCount < RetryCount) jobException.RefireImmediately = true; } From 961f03c1c72c440575bbac6bdcd4a2c1e305a031 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 11:11:22 +0200 Subject: [PATCH 096/153] Added guardrails around the cluster as well as software level --- LiteCharms.Features/Quartz/JobOrchestrator.cs | 1 + LiteCharms.Features/Quartz/MediatorJob.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/LiteCharms.Features/Quartz/JobOrchestrator.cs b/LiteCharms.Features/Quartz/JobOrchestrator.cs index 629365e..e8cb77a 100644 --- a/LiteCharms.Features/Quartz/JobOrchestrator.cs +++ b/LiteCharms.Features/Quartz/JobOrchestrator.cs @@ -18,6 +18,7 @@ public sealed class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOr .WithDescription($"Correlation ID: {notification.CorrelationId}") .UsingJobData(new JobDataMap { ["Payload"] = JsonSerializer.Serialize(notification) }) .DisallowConcurrentExecution() + .RequestRecovery() .Build(); var trigger = global::Quartz.TriggerBuilder.Create() diff --git a/LiteCharms.Features/Quartz/MediatorJob.cs b/LiteCharms.Features/Quartz/MediatorJob.cs index 878d988..108b55c 100644 --- a/LiteCharms.Features/Quartz/MediatorJob.cs +++ b/LiteCharms.Features/Quartz/MediatorJob.cs @@ -8,6 +8,9 @@ public sealed class MediatorJob(IMediator mediator) : IJob where { public async Task Execute(IJobExecutionContext context) { + if (context.Recovering) + Trace.WriteLine($"CRITICAL RECOVERY: Resurrecting job '{typeof(TNotification).Name}' after a previous cluster node crashed mid-execution."); + var data = context.MergedJobDataMap["Payload"] as string; if (string.IsNullOrWhiteSpace(data)) From b4a48c9cbffd63804d347544636d0a6b0c3196e6 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 11:23:13 +0200 Subject: [PATCH 097/153] Added 0.0.0.0 health checks url transformation --- LiteCharms.Features/Extensions/Api.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index df8fe23..ef50663 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -69,8 +69,10 @@ public static class Api if (!string.IsNullOrWhiteSpace(urls)) { - string firstUrl = urls.Split(';').FirstOrDefault(s => s.Contains("http://"))! - .Replace("*", "localhost").Replace("+", "localhost"); + string firstUrl = urls.Split(';').FirstOrDefault(s => s.Contains("http://"))! + .Replace("0.0.0.0", "localhost") + .Replace("*", "localhost") + .Replace("+", "localhost"); healthUrl = $"{firstUrl.TrimEnd('/')}/health"; } From 29574f4df054d7494c94c892aaf001202eaa62ea Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 12:15:31 +0200 Subject: [PATCH 098/153] Refactored token message --- .../Api/OpenApiBearerSecuritySchemeTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs b/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs index 41ef59a..4f1f231 100644 --- a/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs +++ b/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs @@ -8,7 +8,7 @@ public sealed class OpenApiBearerSecuritySchemeTransformer : IOpenApiDocumentTra { Type = SecuritySchemeType.Http, Scheme = "bearer", - Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"", + Description = "JWT Authorization header using the Bearer scheme", }; document.AddComponent("Bearer", bearerScheme); From fda97db5fa5b1d1234d95abc10ace9d9261c4c92 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 12:52:59 +0200 Subject: [PATCH 099/153] Added authentik configuration and service registration --- .../Api/Configuration/AuthentikSettings.cs | 18 +++ LiteCharms.Features/Extensions/Api.cs | 132 +++++++++++------- .../LiteCharms.Features.csproj | 12 ++ 3 files changed, 111 insertions(+), 51 deletions(-) create mode 100644 LiteCharms.Features/Api/Configuration/AuthentikSettings.cs diff --git a/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs b/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs new file mode 100644 index 0000000..3e2fc3a --- /dev/null +++ b/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.Api.Configuration; + +public sealed class AuthentikSettings +{ + public string? Authority { get; set; } + + public string? ApiResourceName { get; set; } + + public string? ApiResourceSecret { get; set; } + + public string? RequiredClaimName { get; set; } + + public string? RequiredClaimNameValue { get; set; } + + public bool RequireHttpsMetadata { get; set; } + + public bool BypassSslErrors { get; set; } +} diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index ef50663..8bb3b69 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.Abstractions; using LiteCharms.Features.Api; +using LiteCharms.Features.Api.Configuration; namespace LiteCharms.Features.Extensions; @@ -8,6 +9,86 @@ public static class Api public const string Books = nameof(Books); public const string Payments = nameof(Payments); + public static IServiceCollection AddAuthentic(this IServiceCollection services, IConfiguration configuration) + { + var authOptions = new AuthentikSettings(); + + configuration.GetSection("Authentik").Bind(authOptions); + + services.Configure(configuration.GetSection(nameof(AuthentikSettings))); + + services.AddAuthentication(OAuth2IntrospectionDefaults.AuthenticationScheme) + .AddOAuth2Introspection(options => + { + options.Authority = options.Authority; + options.ClientId = options.ClientId; + options.ClientSecret = options.ClientSecret; + options.DiscoveryPolicy.RequireHttps = authOptions.RequireHttpsMetadata; + options.EnableCaching = true; + options.CacheDuration = TimeSpan.FromMinutes(10); + }); + + if (!string.IsNullOrWhiteSpace(authOptions.RequiredClaimName) && !string.IsNullOrWhiteSpace(authOptions.RequiredClaimNameValue)) + services.AddAuthorizationBuilder().AddPolicy("ApiScope", policy => + policy.RequireClaim(authOptions.RequiredClaimName, authOptions.RequiredClaimNameValue)); + else + services.AddAuthorization(); + + return services; + } + + public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddHttpClient(); + + services.AddApiVersioning(options => + { + options.DefaultApiVersion = new ApiVersion(1); + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), + new QueryStringApiVersionReader("version"), + new QueryStringApiVersionReader("version"), + new MediaTypeApiVersionReader("version")); + }) + .AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }); + + var urls = configuration["ASPNETCORE_URLS"] ?? configuration["Urls"]; + var healthUrl = "http://localhost:8080/health"; + + if (!string.IsNullOrWhiteSpace(urls)) + { + string firstUrl = urls.Split(';').FirstOrDefault(s => s.Contains("http://"))! + .Replace("0.0.0.0", "localhost") + .Replace("*", "localhost") + .Replace("+", "localhost"); + + healthUrl = $"{firstUrl.TrimEnd('/')}/health"; + } + + services.AddHealthChecksUI(setup => + { + setup.SetNotifyUnHealthyOneTimeUntilChange(); + setup.AddHealthCheckEndpoint("primary, heal", healthUrl); + setup.SetHeaderText("Midrand Books"); + }) + .AddInMemoryStorage(); + + services.AddOutputCache(options => + { + options.AddBasePolicy(builder => builder.Cache()); + options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); + }); + + services.AddOpenApi(options => options.AddDocumentTransformer()); + + return services; + } + public static IApplicationBuilder MapEndpoints(this WebApplication app, IDictionary versionGroups) { var endpoints = app.Services.GetRequiredService>(); @@ -44,55 +125,4 @@ public static class Api public static string ToEndpointName(this Type target, string? annotation = "") => $"{target.Name.Replace("Endpoint", string.Empty)}{annotation}".ToLower(CultureInfo.CurrentCulture); - public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration) - { - services.AddHttpClient(); - - services.AddApiVersioning(options => - { - options.DefaultApiVersion = new ApiVersion(1); - options.ReportApiVersions = true; - options.AssumeDefaultVersionWhenUnspecified = true; - options.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), - new QueryStringApiVersionReader("version"), - new QueryStringApiVersionReader("version"), - new MediaTypeApiVersionReader("version")); - }) - .AddApiExplorer(options => - { - options.GroupNameFormat = "'v'VVV"; - options.SubstituteApiVersionInUrl = true; - }); - - var urls = configuration["ASPNETCORE_URLS"] ?? configuration["Urls"]; - var healthUrl = "http://localhost:8080/health"; - - if (!string.IsNullOrWhiteSpace(urls)) - { - string firstUrl = urls.Split(';').FirstOrDefault(s => s.Contains("http://"))! - .Replace("0.0.0.0", "localhost") - .Replace("*", "localhost") - .Replace("+", "localhost"); - - healthUrl = $"{firstUrl.TrimEnd('/')}/health"; - } - - services.AddHealthChecksUI(setup => - { - setup.SetNotifyUnHealthyOneTimeUntilChange(); - setup.AddHealthCheckEndpoint("primary, heal", healthUrl); - setup.SetHeaderText("Midrand Books"); - }) - .AddInMemoryStorage(); - - services.AddOutputCache(options => - { - options.AddBasePolicy(builder => builder.Cache()); - options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); - }); - - services.AddOpenApi(options => options.AddDocumentTransformer()); - - return services; - } } diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index dd9ff14..4379e7c 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -29,6 +29,18 @@ + + + + + + + + + + + + From ee6f8a283ee1f3ae7b95609cd2a2731a951b03c6 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 17:37:33 +0200 Subject: [PATCH 100/153] Refactored oauth registration --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 75bddf9..ac5d4a4 100644 --- a/.gitignore +++ b/.gitignore @@ -363,3 +363,4 @@ MigrationBackup/ FodyWeavers.xsd /LiteCharms.Features.Tests/http/http-client.env.json /LiteCharms.Features.Tests/http/midrandshop-api/http-client.env.json +/LiteCharms.Features.Tests/http/authentik/http-client.env.json From a50830ffaa5d186477c23d7b19707e6e72ed5668 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 17:37:56 +0200 Subject: [PATCH 101/153] Refactored auth --- .../http/authentik/app.http | 6 ++++++ LiteCharms.Features/Extensions/Api.cs | 21 ++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 LiteCharms.Features.Tests/http/authentik/app.http diff --git a/LiteCharms.Features.Tests/http/authentik/app.http b/LiteCharms.Features.Tests/http/authentik/app.http new file mode 100644 index 0000000..93b1bc2 --- /dev/null +++ b/LiteCharms.Features.Tests/http/authentik/app.http @@ -0,0 +1,6 @@ +## Authentik Token Request +POST {{authority}} +Content-Type: application/x-www-form-urlencoded +Accept-Encoding: identity + +grant_type={{grantType}}&client_id={{clientId}}&client_secret={{clientSecret}}&username={{username}}&password={{password}}&scope={{scope}} \ No newline at end of file diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 8bb3b69..bf73b01 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -11,26 +11,31 @@ public static class Api public static IServiceCollection AddAuthentic(this IServiceCollection services, IConfiguration configuration) { + var configSection = configuration.GetSection(nameof(AuthentikSettings)); + var authOptions = new AuthentikSettings(); + configSection.Bind(authOptions); - configuration.GetSection("Authentik").Bind(authOptions); - - services.Configure(configuration.GetSection(nameof(AuthentikSettings))); + services.Configure(configSection); services.AddAuthentication(OAuth2IntrospectionDefaults.AuthenticationScheme) - .AddOAuth2Introspection(options => + .AddOAuth2Introspection(OAuth2IntrospectionDefaults.AuthenticationScheme, options => { - options.Authority = options.Authority; - options.ClientId = options.ClientId; - options.ClientSecret = options.ClientSecret; + options.Authority = authOptions.Authority; + options.ClientId = authOptions.ApiResourceName; + options.ClientSecret = authOptions.ApiResourceSecret; + options.DiscoveryPolicy.RequireHttps = authOptions.RequireHttpsMetadata; options.EnableCaching = true; options.CacheDuration = TimeSpan.FromMinutes(10); }); if (!string.IsNullOrWhiteSpace(authOptions.RequiredClaimName) && !string.IsNullOrWhiteSpace(authOptions.RequiredClaimNameValue)) - services.AddAuthorizationBuilder().AddPolicy("ApiScope", policy => + { + services.AddAuthorizationBuilder() + .AddPolicy("ApiScope", policy => policy.RequireClaim(authOptions.RequiredClaimName, authOptions.RequiredClaimNameValue)); + } else services.AddAuthorization(); From 652ca82a573113638c4ddf32031b40cea774dfe5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Wed, 3 Jun 2026 17:48:38 +0200 Subject: [PATCH 102/153] Disabled caching --- LiteCharms.Features/Api/Configuration/AuthentikSettings.cs | 2 -- LiteCharms.Features/Extensions/Api.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs b/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs index 3e2fc3a..e570cff 100644 --- a/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs +++ b/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs @@ -13,6 +13,4 @@ public sealed class AuthentikSettings public string? RequiredClaimNameValue { get; set; } public bool RequireHttpsMetadata { get; set; } - - public bool BypassSslErrors { get; set; } } diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index bf73b01..b53dc32 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -26,7 +26,7 @@ public static class Api options.ClientSecret = authOptions.ApiResourceSecret; options.DiscoveryPolicy.RequireHttps = authOptions.RequireHttpsMetadata; - options.EnableCaching = true; + options.EnableCaching = false; options.CacheDuration = TimeSpan.FromMinutes(10); }); From 8f97d7cf387696b34ebcfd6aecc8e9356e66e694 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 4 Jun 2026 08:47:18 +0200 Subject: [PATCH 103/153] Refactored authentication --- .../appsettings.json | 2 +- LiteCharms.Features.Tests/http/authentik/app.http | 4 ++-- .../Api/Configuration/AuthentikSettings.cs | 2 ++ LiteCharms.Features/Extensions/Api.cs | 15 +++++---------- LiteCharms.Features/Hasher/HashService.cs | 2 +- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json index b394b55..b7a0751 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json +++ b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json @@ -1,6 +1,6 @@ { "FeatureManagement": { - "CategorySeederService": true, + "CategorySeederService": false, "CustomerSeederService": false, "ProductsSeederService": false }, diff --git a/LiteCharms.Features.Tests/http/authentik/app.http b/LiteCharms.Features.Tests/http/authentik/app.http index 93b1bc2..9105f67 100644 --- a/LiteCharms.Features.Tests/http/authentik/app.http +++ b/LiteCharms.Features.Tests/http/authentik/app.http @@ -1,6 +1,6 @@ -## Authentik Token Request +### Authentik Token Request (Service Account Explicit) POST {{authority}} Content-Type: application/x-www-form-urlencoded Accept-Encoding: identity -grant_type={{grantType}}&client_id={{clientId}}&client_secret={{clientSecret}}&username={{username}}&password={{password}}&scope={{scope}} \ No newline at end of file +grant_type={{grantType}}&client_id={{clientId}}&username={{username}}&password={{password}}&scope={{scope}} diff --git a/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs b/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs index e570cff..113d0dd 100644 --- a/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs +++ b/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs @@ -4,6 +4,8 @@ public sealed class AuthentikSettings { public string? Authority { get; set; } + public string? IntrospectionUrl { get; set; } + public string? ApiResourceName { get; set; } public string? ApiResourceSecret { get; set; } diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index b53dc32..3883519 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -22,22 +22,17 @@ public static class Api .AddOAuth2Introspection(OAuth2IntrospectionDefaults.AuthenticationScheme, options => { options.Authority = authOptions.Authority; + options.IntrospectionEndpoint = authOptions.IntrospectionUrl; options.ClientId = authOptions.ApiResourceName; options.ClientSecret = authOptions.ApiResourceSecret; + options.NameClaimType = "sub"; options.DiscoveryPolicy.RequireHttps = authOptions.RequireHttpsMetadata; - options.EnableCaching = false; - options.CacheDuration = TimeSpan.FromMinutes(10); + options.DiscoveryPolicy.ValidateEndpoints = false; + options.EnableCaching = false; }); - if (!string.IsNullOrWhiteSpace(authOptions.RequiredClaimName) && !string.IsNullOrWhiteSpace(authOptions.RequiredClaimNameValue)) - { - services.AddAuthorizationBuilder() - .AddPolicy("ApiScope", policy => - policy.RequireClaim(authOptions.RequiredClaimName, authOptions.RequiredClaimNameValue)); - } - else - services.AddAuthorization(); + services.AddAuthorization(); return services; } diff --git a/LiteCharms.Features/Hasher/HashService.cs b/LiteCharms.Features/Hasher/HashService.cs index 660c9e5..b6b79e0 100644 --- a/LiteCharms.Features/Hasher/HashService.cs +++ b/LiteCharms.Features/Hasher/HashService.cs @@ -4,7 +4,7 @@ namespace LiteCharms.Features.Hasher; public sealed partial class HashService(IHashids hasher) : IService { - [GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")] + [GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] private static partial Regex HexHashRegex { get; } [GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] From eef1096ec5dba3fbd3020d6e7b01527490e16cdd Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 4 Jun 2026 08:57:16 +0200 Subject: [PATCH 104/153] Applied required scope policy --- LiteCharms.Features/Extensions/Api.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 3883519..58336d8 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -32,7 +32,14 @@ public static class Api options.EnableCaching = false; }); - services.AddAuthorization(); + if (!string.IsNullOrWhiteSpace(authOptions.RequiredClaimName) && !string.IsNullOrWhiteSpace(authOptions.RequiredClaimNameValue)) + { + services.AddAuthorizationBuilder() + .AddPolicy("RequiredScope", policy => + policy.RequireClaim(authOptions.RequiredClaimName, authOptions.RequiredClaimNameValue)); + } + else + services.AddAuthorization(); return services; } From 5666ffd4748e026363327cb400ecb92a1b9c38d3 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 4 Jun 2026 14:08:27 +0200 Subject: [PATCH 105/153] Added UI security --- .../Api/Configuration/AuthentikSettings.cs | 4 +- LiteCharms.Features/Extensions/Api.cs | 44 ++++++++++++++++--- .../LiteCharms.Features.csproj | 2 + 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs b/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs index 113d0dd..7422294 100644 --- a/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs +++ b/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs @@ -6,9 +6,9 @@ public sealed class AuthentikSettings public string? IntrospectionUrl { get; set; } - public string? ApiResourceName { get; set; } + public string? ClientId { get; set; } - public string? ApiResourceSecret { get; set; } + public string? ClientSecret { get; set; } public string? RequiredClaimName { get; set; } diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 58336d8..1fbfef0 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -9,7 +9,42 @@ public static class Api public const string Books = nameof(Books); public const string Payments = nameof(Payments); - public static IServiceCollection AddAuthentic(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddAuthentikUiSecurity(this IServiceCollection services, IConfiguration configuration) + { + var configSection = configuration.GetSection(nameof(AuthentikSettings)); + + var authOptions = new AuthentikSettings(); + configSection.Bind(authOptions); + + services.Configure(configSection); + + services.AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) + .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.Scope.Clear(); + options.Scope.Add("openid"); + options.Scope.Add("profile"); + options.Scope.Add("email"); + }); + + return services; + } + + public static IServiceCollection AddAuthentikApiSecurity(this IServiceCollection services, IConfiguration configuration) { var configSection = configuration.GetSection(nameof(AuthentikSettings)); @@ -23,13 +58,13 @@ public static class Api { options.Authority = authOptions.Authority; options.IntrospectionEndpoint = authOptions.IntrospectionUrl; - options.ClientId = authOptions.ApiResourceName; - options.ClientSecret = authOptions.ApiResourceSecret; + options.ClientId = authOptions.ClientId; + options.ClientSecret = authOptions.ClientSecret; options.NameClaimType = "sub"; options.DiscoveryPolicy.RequireHttps = authOptions.RequireHttpsMetadata; options.DiscoveryPolicy.ValidateEndpoints = false; - options.EnableCaching = false; + options.EnableCaching = false; }); if (!string.IsNullOrWhiteSpace(authOptions.RequiredClaimName) && !string.IsNullOrWhiteSpace(authOptions.RequiredClaimNameValue)) @@ -131,5 +166,4 @@ public static class Api public static string ToEndpointName(this Type target, string? annotation = "") => $"{target.Name.Replace("Endpoint", string.Empty)}{annotation}".ToLower(CultureInfo.CurrentCulture); - } diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 4379e7c..59556f8 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -38,6 +38,8 @@ + + From fa79bd802165f8827f49a6cb91e617e38d0716df Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 4 Jun 2026 14:39:14 +0200 Subject: [PATCH 106/153] Added signout functionality for user authentik link --- LiteCharms.Features/Extensions/Api.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 1fbfef0..05a843d 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -30,6 +30,8 @@ public static class Api options.ClientId = authOptions.ClientId; options.ClientSecret = authOptions.ClientSecret; + options.SignedOutCallbackPath = "/signout-callback-oidc"; + options.SignedOutRedirectUri = "/"; options.ResponseType = "code"; options.SaveTokens = true; From cf7eed0603b28c420e0bdc34c1c3d28cf13a599b Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 4 Jun 2026 16:02:29 +0200 Subject: [PATCH 107/153] Added a redirect packet attachment to UI signout process --- LiteCharms.Features/Extensions/Api.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 05a843d..4038d78 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -41,6 +41,16 @@ public static class Api options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); + + options.Events = new OpenIdConnectEvents + { + OnRedirectToIdentityProviderForSignOut = context => + { + context.ProtocolMessage.PostLogoutRedirectUri = context.Properties.RedirectUri; + + return Task.CompletedTask; + }, + }; }); return services; From 20c3ad956972628ae9a398ba69d6d700526f9836 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 5 Jun 2026 05:43:56 +0200 Subject: [PATCH 108/153] Refactored security components --- .../Api/Configuration/AuthentikSettings.cs | 6 +- LiteCharms.Features/Extensions/Api.cs | 55 +++++++++++++++---- .../LiteCharms.Features.csproj | 1 + 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs b/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs index 7422294..db03e12 100644 --- a/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs +++ b/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs @@ -4,7 +4,11 @@ public sealed class AuthentikSettings { public string? Authority { get; set; } - public string? IntrospectionUrl { get; set; } + public string? IntrospectionEndpoint { get; set; } + + public string? MetadataEndpoint { get; set; } + + public string? RevokationEndpoint { get; set; } public string? ClientId { get; set; } diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 4038d78..bb24786 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -27,11 +27,11 @@ public static class Api .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => { options.Authority = authOptions.Authority; + options.MetadataAddress = authOptions.MetadataEndpoint; options.ClientId = authOptions.ClientId; options.ClientSecret = authOptions.ClientSecret; options.SignedOutCallbackPath = "/signout-callback-oidc"; - options.SignedOutRedirectUri = "/"; options.ResponseType = "code"; options.SaveTokens = true; @@ -41,16 +41,6 @@ public static class Api options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); - - options.Events = new OpenIdConnectEvents - { - OnRedirectToIdentityProviderForSignOut = context => - { - context.ProtocolMessage.PostLogoutRedirectUri = context.Properties.RedirectUri; - - return Task.CompletedTask; - }, - }; }); return services; @@ -69,7 +59,7 @@ public static class Api .AddOAuth2Introspection(OAuth2IntrospectionDefaults.AuthenticationScheme, options => { options.Authority = authOptions.Authority; - options.IntrospectionEndpoint = authOptions.IntrospectionUrl; + options.IntrospectionEndpoint = authOptions.IntrospectionEndpoint; options.ClientId = authOptions.ClientId; options.ClientSecret = authOptions.ClientSecret; @@ -91,6 +81,47 @@ public static class Api return services; } + public static WebApplication AddSecurityEndpoints(this WebApplication app) + { + app.MapGet("/login", async (HttpContext context, string redirectUri = "/") => + { + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties + { + RedirectUri = redirectUri, + }); + }); + + app.MapGet("/logout", async (HttpContext context, IHttpClientFactory httpClientFactory, IOptions settings) => + { + var authOptions = settings.Value; + var accessToken = await context.GetTokenAsync("access_token"); + + if (!string.IsNullOrEmpty(accessToken)) + { + try + { + var client = httpClientFactory.CreateClient(); + + var requestContent = new FormUrlEncodedContent(new Dictionary(StringComparer.Ordinal) + { + { "token", accessToken }, + { "client_id", authOptions.ClientId! }, + { "client_secret", authOptions.ClientSecret! }, + }); + + await client.PostAsync(authOptions.RevokationEndpoint, requestContent, context.RequestAborted); + } + catch { } + } + + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + return Results.Redirect("/"); + }); + + return app; + } + public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration) { services.AddHttpClient(); diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 59556f8..49a14d5 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -38,6 +38,7 @@ + From e646d16053b66820603e3f43a5a357bdb83a438e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 5 Jun 2026 06:39:47 +0200 Subject: [PATCH 109/153] Forcing login https proto on redirect --- LiteCharms.Features/Extensions/Api.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index bb24786..3580fc8 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -41,6 +41,24 @@ public static class Api options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); + + options.Events = new OpenIdConnectEvents + { + OnRedirectToIdentityProvider = context => + { + if (!string.IsNullOrEmpty(context.ProtocolMessage.RedirectUri) && context.ProtocolMessage.RedirectUri.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) + { + var uriBuilder = new UriBuilder(context.ProtocolMessage.RedirectUri) + { + Scheme = "https" + }; + + context.ProtocolMessage.RedirectUri = uriBuilder.Uri.ToString(); + } + + return Task.CompletedTask; + }, + }; }); return services; @@ -116,7 +134,7 @@ public static class Api await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - return Results.Redirect("/"); + return Results.Redirect($"{authOptions.Authority}end-session/"); }); return app; From d38d1dd059fcb8178f6d126c5a815fac40c77bf5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 5 Jun 2026 07:36:41 +0200 Subject: [PATCH 110/153] Added port stripping --- LiteCharms.Features/Extensions/Api.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 3580fc8..d675013 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -50,7 +50,8 @@ public static class Api { var uriBuilder = new UriBuilder(context.ProtocolMessage.RedirectUri) { - Scheme = "https" + Scheme = "https", + Port = -1, }; context.ProtocolMessage.RedirectUri = uriBuilder.Uri.ToString(); @@ -105,7 +106,7 @@ public static class Api { await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { - RedirectUri = redirectUri, + RedirectUri = redirectUri, }); }); From 4576b5aa2b6c6034247d3cd136fe15c82d27a6f4 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 5 Jun 2026 08:15:13 +0200 Subject: [PATCH 111/153] Refactored logout endpoint --- LiteCharms.Features/Extensions/Api.cs | 28 ++++++--------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index d675013..608f84c 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -112,30 +112,14 @@ public static class Api app.MapGet("/logout", async (HttpContext context, IHttpClientFactory httpClientFactory, IOptions settings) => { - var authOptions = settings.Value; - var accessToken = await context.GetTokenAsync("access_token"); - - if (!string.IsNullOrEmpty(accessToken)) - { - try - { - var client = httpClientFactory.CreateClient(); - - var requestContent = new FormUrlEncodedContent(new Dictionary(StringComparer.Ordinal) - { - { "token", accessToken }, - { "client_id", authOptions.ClientId! }, - { "client_secret", authOptions.ClientSecret! }, - }); - - await client.PostAsync(authOptions.RevokationEndpoint, requestContent, context.RequestAborted); - } - catch { } - } - await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - return Results.Redirect($"{authOptions.Authority}end-session/"); + string currentBaseUrl = $"https://{context.Request.Host}{context.Request.PathBase}/"; + + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties + { + RedirectUri = currentBaseUrl + }); }); return app; From 65f102f18a3ef755b4f830090629e362c9b570f5 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 5 Jun 2026 08:17:32 +0200 Subject: [PATCH 112/153] Simplified login process --- LiteCharms.Features/Extensions/Api.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 608f84c..8a25d9b 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -46,19 +46,13 @@ public static class Api { OnRedirectToIdentityProvider = context => { - if (!string.IsNullOrEmpty(context.ProtocolMessage.RedirectUri) && context.ProtocolMessage.RedirectUri.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) - { - var uriBuilder = new UriBuilder(context.ProtocolMessage.RedirectUri) - { - Scheme = "https", - Port = -1, - }; + var fallbackUri = context.ProtocolMessage.RedirectUri; - context.ProtocolMessage.RedirectUri = uriBuilder.Uri.ToString(); - } + if (fallbackUri.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) + context.ProtocolMessage.RedirectUri = fallbackUri.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase); return Task.CompletedTask; - }, + } }; }); From cdf5cfb5cd07b4a19a779076afbfd15ccafe7ed1 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 5 Jun 2026 08:55:31 +0200 Subject: [PATCH 113/153] Removed login proto handling --- LiteCharms.Features/Extensions/Api.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 8a25d9b..6b14319 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -41,19 +41,6 @@ public static class Api options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); - - options.Events = new OpenIdConnectEvents - { - OnRedirectToIdentityProvider = context => - { - var fallbackUri = context.ProtocolMessage.RedirectUri; - - if (fallbackUri.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) - context.ProtocolMessage.RedirectUri = fallbackUri.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase); - - return Task.CompletedTask; - } - }; }); return services; From 90a11dc65e2921f959d0bd6a7966b2d103ae3a95 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 5 Jun 2026 09:19:32 +0200 Subject: [PATCH 114/153] Refactored to deal with cookie hell --- LiteCharms.Features/Extensions/Api.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 6b14319..30e76da 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -41,6 +41,14 @@ public static class Api options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); + + options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always; + options.CorrelationCookie.SameSite = SameSiteMode.None; + options.CorrelationCookie.HttpOnly = true; + + options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always; + options.NonceCookie.SameSite = SameSiteMode.None; + options.NonceCookie.HttpOnly = true; }); return services; From 5d5b59d610a2d69baea90372e91f03688558e565 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 6 Jun 2026 16:26:27 +0200 Subject: [PATCH 115/153] Updated API to use LiteCharms Security --- .../http/{authentik => litecharms}/app.http | 4 +- .../http/litecharms/http-client.env.json | 9 ++++ ...entikSettings.cs => LiteCharmsSettings.cs} | 12 ++---- LiteCharms.Features/Extensions/Api.cs | 41 +++++++++---------- 4 files changed, 34 insertions(+), 32 deletions(-) rename LiteCharms.Features.Tests/http/{authentik => litecharms}/app.http (50%) create mode 100644 LiteCharms.Features.Tests/http/litecharms/http-client.env.json rename LiteCharms.Features/Api/Configuration/{AuthentikSettings.cs => LiteCharmsSettings.cs} (54%) diff --git a/LiteCharms.Features.Tests/http/authentik/app.http b/LiteCharms.Features.Tests/http/litecharms/app.http similarity index 50% rename from LiteCharms.Features.Tests/http/authentik/app.http rename to LiteCharms.Features.Tests/http/litecharms/app.http index 9105f67..09e5d9f 100644 --- a/LiteCharms.Features.Tests/http/authentik/app.http +++ b/LiteCharms.Features.Tests/http/litecharms/app.http @@ -1,6 +1,6 @@ ### Authentik Token Request (Service Account Explicit) -POST {{authority}} +POST {{authority}}/connect/token Content-Type: application/x-www-form-urlencoded Accept-Encoding: identity -grant_type={{grantType}}&client_id={{clientId}}&username={{username}}&password={{password}}&scope={{scope}} +grant_type={{grantType}}&client_id={{clientId}}&client_secret={{clientSecret}}&scope={{scope}} diff --git a/LiteCharms.Features.Tests/http/litecharms/http-client.env.json b/LiteCharms.Features.Tests/http/litecharms/http-client.env.json new file mode 100644 index 0000000..36d0bd1 --- /dev/null +++ b/LiteCharms.Features.Tests/http/litecharms/http-client.env.json @@ -0,0 +1,9 @@ +{ + "uat": { + "authority": "https://sts.security.khongisa.co.za", + "grantType": "client_credentials", + "clientId": "midrandbooks-api-uat", + "clientSecret": "secret_5a36d0024980544c875447a4b052938becc3fbbb10b8b2c097310c1a53ba3c0a", + "scope": "midrandbooks-api" + } +} diff --git a/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs b/LiteCharms.Features/Api/Configuration/LiteCharmsSettings.cs similarity index 54% rename from LiteCharms.Features/Api/Configuration/AuthentikSettings.cs rename to LiteCharms.Features/Api/Configuration/LiteCharmsSettings.cs index db03e12..58f49bc 100644 --- a/LiteCharms.Features/Api/Configuration/AuthentikSettings.cs +++ b/LiteCharms.Features/Api/Configuration/LiteCharmsSettings.cs @@ -1,22 +1,16 @@ namespace LiteCharms.Features.Api.Configuration; -public sealed class AuthentikSettings +public sealed class LiteCharmsSettings { public string? Authority { get; set; } - public string? IntrospectionEndpoint { get; set; } - - public string? MetadataEndpoint { get; set; } - - public string? RevokationEndpoint { get; set; } - public string? ClientId { get; set; } public string? ClientSecret { get; set; } + public string? Audience { get; set; } + public string? RequiredClaimName { get; set; } public string? RequiredClaimNameValue { get; set; } - - public bool RequireHttpsMetadata { get; set; } } diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 30e76da..c66b25e 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -1,6 +1,7 @@ using LiteCharms.Features.Abstractions; using LiteCharms.Features.Api; using LiteCharms.Features.Api.Configuration; +using Microsoft.AspNetCore.Authentication.JwtBearer; namespace LiteCharms.Features.Extensions; @@ -9,14 +10,14 @@ public static class Api public const string Books = nameof(Books); public const string Payments = nameof(Payments); - public static IServiceCollection AddAuthentikUiSecurity(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddLiteCharmsUiSecurity(this IServiceCollection services, IConfiguration configuration) { - var configSection = configuration.GetSection(nameof(AuthentikSettings)); + var configSection = configuration.GetSection(nameof(LiteCharmsSettings)); - var authOptions = new AuthentikSettings(); + var authOptions = new LiteCharmsSettings(); configSection.Bind(authOptions); - services.Configure(configSection); + services.Configure(configSection); services.AddAuthentication(options => { @@ -26,8 +27,7 @@ public static class Api .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => { - options.Authority = authOptions.Authority; - options.MetadataAddress = authOptions.MetadataEndpoint; + options.Authority = authOptions.Authority; options.ClientId = authOptions.ClientId; options.ClientSecret = authOptions.ClientSecret; @@ -54,27 +54,26 @@ public static class Api return services; } - public static IServiceCollection AddAuthentikApiSecurity(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddLiteCharmsApiSecurity(this IServiceCollection services, IConfiguration configuration) { - var configSection = configuration.GetSection(nameof(AuthentikSettings)); + var configSection = configuration.GetSection(nameof(LiteCharmsSettings)); - var authOptions = new AuthentikSettings(); + var authOptions = new LiteCharmsSettings(); configSection.Bind(authOptions); - services.Configure(configSection); + services.Configure(configSection); - services.AddAuthentication(OAuth2IntrospectionDefaults.AuthenticationScheme) - .AddOAuth2Introspection(OAuth2IntrospectionDefaults.AuthenticationScheme, options => + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => { options.Authority = authOptions.Authority; - options.IntrospectionEndpoint = authOptions.IntrospectionEndpoint; - options.ClientId = authOptions.ClientId; - options.ClientSecret = authOptions.ClientSecret; - - options.NameClaimType = "sub"; - options.DiscoveryPolicy.RequireHttps = authOptions.RequireHttpsMetadata; - options.DiscoveryPolicy.ValidateEndpoints = false; - options.EnableCaching = false; + options.Audience = authOptions.Audience; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = authOptions.Authority, + ValidateAudience = true, + ValidateIssuer = true, + }; }); if (!string.IsNullOrWhiteSpace(authOptions.RequiredClaimName) && !string.IsNullOrWhiteSpace(authOptions.RequiredClaimNameValue)) @@ -99,7 +98,7 @@ public static class Api }); }); - app.MapGet("/logout", async (HttpContext context, IHttpClientFactory httpClientFactory, IOptions settings) => + app.MapGet("/logout", async (HttpContext context, IHttpClientFactory httpClientFactory, IOptions settings) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); From dc4addb43a1973f60386ed1905829226fab47edb Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 6 Jun 2026 16:44:22 +0200 Subject: [PATCH 116/153] Removed required scope policy, no longer needed, audience covers the intent --- .../Api/Configuration/LiteCharmsSettings.cs | 4 ---- LiteCharms.Features/Extensions/Api.cs | 9 +-------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/LiteCharms.Features/Api/Configuration/LiteCharmsSettings.cs b/LiteCharms.Features/Api/Configuration/LiteCharmsSettings.cs index 58f49bc..0bc1ecc 100644 --- a/LiteCharms.Features/Api/Configuration/LiteCharmsSettings.cs +++ b/LiteCharms.Features/Api/Configuration/LiteCharmsSettings.cs @@ -9,8 +9,4 @@ public sealed class LiteCharmsSettings public string? ClientSecret { get; set; } public string? Audience { get; set; } - - public string? RequiredClaimName { get; set; } - - public string? RequiredClaimNameValue { get; set; } } diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index c66b25e..66928bc 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -76,14 +76,7 @@ public static class Api }; }); - if (!string.IsNullOrWhiteSpace(authOptions.RequiredClaimName) && !string.IsNullOrWhiteSpace(authOptions.RequiredClaimNameValue)) - { - services.AddAuthorizationBuilder() - .AddPolicy("RequiredScope", policy => - policy.RequireClaim(authOptions.RequiredClaimName, authOptions.RequiredClaimNameValue)); - } - else - services.AddAuthorization(); + services.AddAuthorization(); return services; } From 3f3e0dbe88df924f27729fa3c67e87c9ae6ec6a6 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 6 Jun 2026 21:49:32 +0200 Subject: [PATCH 117/153] Disabled audience verification on jwt tokens --- .../LiteCharms.Features.MidrandBooks.Seed.csproj | 6 +++--- .../LiteCharms.Features.MidrandBooks.csproj | 6 +++--- .../LiteCharms.Features.TechShop.csproj | 4 ++-- .../http/litecharms/http-client.env.json | 4 ++-- LiteCharms.Features/Extensions/Api.cs | 2 +- LiteCharms.Features/LiteCharms.Features.csproj | 14 +++++++------- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj index d8ac700..bc9d6e2 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj +++ b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -116,8 +116,8 @@ - - + + diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 3db773c..09ba93d 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -32,7 +32,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -136,8 +136,8 @@ - - + + diff --git a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj index b99f168..4cd7426 100644 --- a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj +++ b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj @@ -136,8 +136,8 @@ - - + + diff --git a/LiteCharms.Features.Tests/http/litecharms/http-client.env.json b/LiteCharms.Features.Tests/http/litecharms/http-client.env.json index 36d0bd1..c14a05b 100644 --- a/LiteCharms.Features.Tests/http/litecharms/http-client.env.json +++ b/LiteCharms.Features.Tests/http/litecharms/http-client.env.json @@ -2,8 +2,8 @@ "uat": { "authority": "https://sts.security.khongisa.co.za", "grantType": "client_credentials", - "clientId": "midrandbooks-api-uat", - "clientSecret": "secret_5a36d0024980544c875447a4b052938becc3fbbb10b8b2c097310c1a53ba3c0a", + "clientId": "midrandbooks-api-scaler-uat", + "clientSecret": "secret_0a8dc1f99061590a52b1272db3a1871d2761c79fbd058b2a968911029e4b208a", "scope": "midrandbooks-api" } } diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 66928bc..5cff9de 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -71,7 +71,7 @@ public static class Api options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = authOptions.Authority, - ValidateAudience = true, + ValidateAudience = false, ValidateIssuer = true, }; }); diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 49a14d5..98a074e 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -38,10 +38,10 @@ - - - - + + + + @@ -67,7 +67,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -171,8 +171,8 @@ - - + + From c53434a578005a743957e7739b028f9348c0e3c4 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 6 Jun 2026 23:53:19 +0200 Subject: [PATCH 118/153] Enabled api audience verification --- LiteCharms.Features/Extensions/Api.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 5cff9de..66928bc 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -71,7 +71,7 @@ public static class Api options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = authOptions.Authority, - ValidateAudience = false, + ValidateAudience = true, ValidateIssuer = true, }; }); From a567fc7cd77e78916c6b98ae3223461dde58b8bf Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 7 Jun 2026 10:30:56 +0200 Subject: [PATCH 119/153] Refactored client auth method --- LiteCharms.Features/Extensions/Api.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 66928bc..c7c2851 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -31,9 +31,8 @@ public static class Api options.ClientId = authOptions.ClientId; options.ClientSecret = authOptions.ClientSecret; - options.SignedOutCallbackPath = "/signout-callback-oidc"; - options.ResponseType = "code"; + options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; @@ -41,16 +40,10 @@ public static class Api options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); - - options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always; - options.CorrelationCookie.SameSite = SameSiteMode.None; - options.CorrelationCookie.HttpOnly = true; - - options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always; - options.NonceCookie.SameSite = SameSiteMode.None; - options.NonceCookie.HttpOnly = true; }); + services.AddCascadingAuthenticationState(); + return services; } From c423f04b42ea5fbb6071fa08d088f8ba25cfffd4 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 7 Jun 2026 11:08:05 +0200 Subject: [PATCH 120/153] Refactored AddSecurityEndpoints --- LiteCharms.Features/Extensions/Api.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index c7c2851..c16c100 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -76,14 +76,6 @@ public static class Api public static WebApplication AddSecurityEndpoints(this WebApplication app) { - app.MapGet("/login", async (HttpContext context, string redirectUri = "/") => - { - await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties - { - RedirectUri = redirectUri, - }); - }); - app.MapGet("/logout", async (HttpContext context, IHttpClientFactory httpClientFactory, IOptions settings) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); @@ -92,7 +84,7 @@ public static class Api await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { - RedirectUri = currentBaseUrl + RedirectUri = currentBaseUrl, }); }); From b41136e2c7e451ae957fd2a60fee239723a22e5b Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 7 Jun 2026 12:00:07 +0200 Subject: [PATCH 121/153] Added AddSecurityEndpoints login endpoint --- LiteCharms.Features/Extensions/Api.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index c16c100..a94b29e 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -76,6 +76,14 @@ public static class Api public static WebApplication AddSecurityEndpoints(this WebApplication app) { + app.MapGet("/login", async (HttpContext context, string redirectUri = "/") => + { + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties + { + RedirectUri = redirectUri, + }); + }); + app.MapGet("/logout", async (HttpContext context, IHttpClientFactory httpClientFactory, IOptions settings) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); From 60095057b7310824ff4306fbf28ee8e37fc39aa3 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 7 Jun 2026 13:11:33 +0200 Subject: [PATCH 122/153] Added token hint to logout event --- LiteCharms.Features/Extensions/Api.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index a94b29e..28a8ca1 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -10,7 +10,7 @@ public static class Api public const string Books = nameof(Books); public const string Payments = nameof(Payments); - public static IServiceCollection AddLiteCharmsUiSecurity(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddLiteCharmsWebSecurity(this IServiceCollection services, IConfiguration configuration) { var configSection = configuration.GetSection(nameof(LiteCharmsSettings)); @@ -40,6 +40,24 @@ public static class Api 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(); From 840d4568e2e6068e600fac7b19ef55681a2fa46e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 7 Jun 2026 13:55:20 +0200 Subject: [PATCH 123/153] Refactored logout process --- LiteCharms.Features/Extensions/Api.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 28a8ca1..84c087d 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -27,7 +27,7 @@ public static class Api .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => { - options.Authority = authOptions.Authority; + options.Authority = authOptions.Authority; options.ClientId = authOptions.ClientId; options.ClientSecret = authOptions.ClientSecret; @@ -102,16 +102,15 @@ public static class Api }); }); - app.MapGet("/logout", async (HttpContext context, IHttpClientFactory httpClientFactory, IOptions settings) => + app.MapGet("/logout", async (HttpContext context) => { - await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + var schemesToSignOut = new[] { CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme, }; - string currentBaseUrl = $"https://{context.Request.Host}{context.Request.PathBase}/"; + string postLogoutRedirectDestination = "/"; - await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties - { - RedirectUri = currentBaseUrl, - }); + await context.SignOutAsync(scheme: null, properties: new AuthenticationProperties { RedirectUri = postLogoutRedirectDestination, }); + + foreach (var scheme in schemesToSignOut) await context.SignOutAsync(scheme); }); return app; From e193aa7c1cfce329b38594c3e50bc393b7856b20 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 7 Jun 2026 13:56:49 +0200 Subject: [PATCH 124/153] Adopted standard logout flow --- LiteCharms.Features/Extensions/Api.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 84c087d..f1b2bda 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -104,13 +104,12 @@ public static class Api app.MapGet("/logout", async (HttpContext context) => { - var schemesToSignOut = new[] { CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme, }; + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - string postLogoutRedirectDestination = "/"; - - await context.SignOutAsync(scheme: null, properties: new AuthenticationProperties { RedirectUri = postLogoutRedirectDestination, }); - - foreach (var scheme in schemesToSignOut) await context.SignOutAsync(scheme); + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties + { + RedirectUri = "/", + }); }); return app; From 02ff14ccc8fd65369921a148fbe9aed2b1f23661 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sun, 7 Jun 2026 14:09:02 +0200 Subject: [PATCH 125/153] Passing token hint during signout --- LiteCharms.Features/Extensions/Api.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index f1b2bda..1e60834 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -104,12 +104,15 @@ public static class Api app.MapGet("/logout", async (HttpContext context) => { - await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + var idToken = await context.GetTokenAsync("id_token"); - await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties - { - RedirectUri = "/", - }); + var authProperties = new AuthenticationProperties { RedirectUri = "/", }; + + if (!string.IsNullOrEmpty(idToken)) + authProperties.Parameters.Add("id_token_hint", idToken); + + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties); + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); }); return app; From 59af9a5406d18ced3f112f0e23ed08153830fe89 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 9 Jun 2026 09:08:46 +0200 Subject: [PATCH 126/153] Added CartService and LocalStorageService (browser) --- ...teCharms.Features.MidrandBooks.Seed.csproj | 6 +- .../LiteCharms.Features.MidrandBooks.csproj | 6 +- .../Orders/CartService.cs | 128 ++++++++++++++++++ .../Orders/Models/Cart.cs | 18 +++ .../Orders/Models/CartItem.cs | 12 ++ .../LiteCharms.Features.TechShop.csproj | 4 +- .../Browser/LocalStorageService.cs | 80 +++++++++++ .../LiteCharms.Features.csproj | 7 +- 8 files changed, 250 insertions(+), 11 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Orders/CartService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Models/Cart.cs create mode 100644 LiteCharms.Features.MidrandBooks/Orders/Models/CartItem.cs create mode 100644 LiteCharms.Features/Browser/LocalStorageService.cs diff --git a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj index bc9d6e2..86652ed 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj +++ b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -116,8 +116,8 @@ - - + + diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 09ba93d..a8bef3a 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -32,7 +32,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -136,8 +136,8 @@ - - + + diff --git a/LiteCharms.Features.MidrandBooks/Orders/CartService.cs b/LiteCharms.Features.MidrandBooks/Orders/CartService.cs new file mode 100644 index 0000000..ae3f8c7 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/CartService.cs @@ -0,0 +1,128 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.MidrandBooks.Orders.Models; +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.Orders; + +public sealed class CartService : IService +{ + private Cart cart = new(); + + public Cart GetCart() => cart; + + public void LoadCart(Cart savedCart) => cart = savedCart; + + public void AddItem(ProductPrice productPrice) + { + var itemExists = false; + + for (var i = 0; i < cart.Items.Count; i++) + { + if (cart.Items[i].Price!.Id == productPrice.Id) + { + cart.Items[i].Quantity++; + cart.Items[i].Amount += productPrice.Amount; + + itemExists = true; + + break; + } + } + + if (!itemExists) + cart.Items.Add(new CartItem + { + Price = productPrice, + Amount = productPrice.Amount, + Quantity = 1, + }); + + CalculateTotalPrice(); + } + + public void UpdateQuantity(long productPriceId, int newQuantity) + { + if (newQuantity <= 0) + { + RemoveAllSameItem(productPriceId); + + return; + } + + for (var i = 0; i < cart.Items.Count; i++) + { + if (cart.Items[i].Price!.Id == productPriceId) + { + var oldQuantity = cart.Items[i].Quantity; + var pricePerUnit = cart.Items[i].Price!.Amount; + + cart.Items[i].Quantity = newQuantity; + cart.Items[i].Amount = pricePerUnit * newQuantity; + break; + } + } + + CalculateTotalPrice(); + } + + public void RemoveOneItem(long productPriceId) + { + for (var i = 0; i < cart.Items.Count; i++) + { + if (cart.Items[i].Price!.Id == productPriceId) + { + if (cart.Items[i].Quantity <= 1) + { + cart.Items.RemoveAt(i); + } + else + { + cart.Items[i].Quantity--; + cart.Items[i].Amount -= cart.Items[i].Price!.Amount; + } + + break; + } + } + + CalculateTotalPrice(); + } + + public void RemoveAllSameItem(long productPriceId) + { + if (cart.Items.Count == 0) return; + + var item = cart.Items.FirstOrDefault(i => i.Price?.Id == productPriceId); + + if (item is not null) cart.Items.Remove(item); + + CalculateTotalPrice(); + } + + public void Clear() + { + if(cart.CustomerId is not null || cart.OrderId is not null) + { + cart.TotalPrice = 0; + cart.TotalVat = 0; + cart.Items.Clear(); + + return; + } + + cart = new Cart(); + } + + public decimal CalculateTotalPrice() + { + if (cart.Items.Count == 0) return 0; + + var gross = cart.Items.Sum(i => i.Amount); + + if (!cart.IsVatInclusive) cart.TotalVat = gross * cart.VatRate; + + cart.TotalPrice = gross + cart.TotalVat; + + return cart.TotalPrice; + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Cart.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Cart.cs new file mode 100644 index 0000000..1d1fd1e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Cart.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Models; + +public sealed class Cart +{ + public long? CustomerId { get; set; } + + public long? OrderId { get; set; } + + public decimal TotalPrice { get; set; } + + public decimal TotalVat { get; set; } + + public decimal VatRate { get; set; } = 0.15m; + + public bool IsVatInclusive { get; set; } = true; + + public IList Items { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/CartItem.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/CartItem.cs new file mode 100644 index 0000000..5d73afe --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/CartItem.cs @@ -0,0 +1,12 @@ +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.Orders.Models; + +public sealed class CartItem +{ + public ProductPrice? Price { get; set; } + + public long Quantity { get; set; } + + public decimal Amount { get; set; } +} diff --git a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj index 4cd7426..5f01765 100644 --- a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj +++ b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj @@ -136,8 +136,8 @@ - - + + diff --git a/LiteCharms.Features/Browser/LocalStorageService.cs b/LiteCharms.Features/Browser/LocalStorageService.cs new file mode 100644 index 0000000..f5168ac --- /dev/null +++ b/LiteCharms.Features/Browser/LocalStorageService.cs @@ -0,0 +1,80 @@ +using LiteCharms.Features.Abstractions; + +namespace LiteCharms.Features.Browser; + +public sealed class LocalStorageService(ProtectedLocalStorage storage) : IService +{ + public async ValueTask DeleteAsync(string key) + { + try + { + await storage.DeleteAsync(key); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask SaveAsync(string key, string value) + { + try + { + await storage.SetAsync(key, value); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask SaveAsync(string key, TValue value) where TValue : class + { + try + { + await storage.SetAsync(key, value); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAsync(string key) + { + try + { + var retrieval = await storage.GetAsync(key); + + return retrieval.Success && !string.IsNullOrWhiteSpace(retrieval.Value) + ? Result.Ok(retrieval.Value) + : Result.Fail($"Could not find object by key {key}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAsync(string key) where TValue : class + { + try + { + var retrieval = await storage.GetAsync(key); + + return retrieval.Success && retrieval.Value is not null + ? Result.Ok(retrieval.Value) + : Result.Fail($"Could not find object by key {key}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 98a074e..fc6cc2f 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -67,7 +67,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -171,8 +171,8 @@ - - + + @@ -182,6 +182,7 @@ + From e7f02eca9bad25e445dafa500d2f58d91a7e05de Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 9 Jun 2026 21:43:47 +0200 Subject: [PATCH 127/153] Refactored Shop services assembly scanner to be more generic --- LiteCharms.Features.MidrandBooks/Extensions/Shop.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs index fddb438..3c7d771 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -4,11 +4,11 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Shop { - public static IServiceCollection AddShopServices(this IServiceCollection services) + public static IServiceCollection AddShopServices(this IServiceCollection services, Assembly assembly) { var serviceType = typeof(IService); - var implementations = Assembly.GetExecutingAssembly().GetTypes() + var implementations = assembly.GetTypes() .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); foreach (var implementation in implementations) From 11dfd18a44e558427a8289cc1386bfc2de2c3a3e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 9 Jun 2026 21:52:31 +0200 Subject: [PATCH 128/153] Ensured the assembly scanner picks up shared and core components --- LiteCharms.Features.MidrandBooks.Seed/Program.cs | 2 +- .../Common/Fixture.cs | 2 ++ .../Abstractions/IMidrandBooks.cs | 3 +++ LiteCharms.Features.MidrandBooks/Extensions/Shop.cs | 13 +++++++++---- LiteCharms.Features/Abstractions/IFeatures.cs | 3 +++ 5 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Abstractions/IMidrandBooks.cs create mode 100644 LiteCharms.Features/Abstractions/IFeatures.cs diff --git a/LiteCharms.Features.MidrandBooks.Seed/Program.cs b/LiteCharms.Features.MidrandBooks.Seed/Program.cs index 10d3172..8483ab8 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/Program.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/Program.cs @@ -14,7 +14,7 @@ builder.Services.AddScopedFeatureManagement(); builder.Services .AddLogging() - .AddShopServices() + .AddShopServices() .AddHostedService() .AddHostedService() .AddHostedService() diff --git a/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs index 0732e5b..a5a759f 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs @@ -1,5 +1,7 @@ using LiteCharms.Features.Extensions; +using LiteCharms.Features.MidrandBooks.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; +using Microsoft.VisualStudio.TestPlatform.TestHost; namespace LiteCharms.Features.MidrandBooks.Tests.Common; diff --git a/LiteCharms.Features.MidrandBooks/Abstractions/IMidrandBooks.cs b/LiteCharms.Features.MidrandBooks/Abstractions/IMidrandBooks.cs new file mode 100644 index 0000000..3994e63 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Abstractions/IMidrandBooks.cs @@ -0,0 +1,3 @@ +namespace LiteCharms.Features.MidrandBooks.Abstractions; + +public interface IMidrandBooks; diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs index 3c7d771..254f236 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -1,18 +1,23 @@ using LiteCharms.Features.Abstractions; +using LiteCharms.Features.MidrandBooks.Abstractions; namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Shop { - public static IServiceCollection AddShopServices(this IServiceCollection services, Assembly assembly) + public static IServiceCollection AddShopServices(this IServiceCollection services) { var serviceType = typeof(IService); - var implementations = assembly.GetTypes() + var sharedImplementations = typeof(IFeatures).Assembly.GetTypes() .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); - foreach (var implementation in implementations) - services.AddScoped(implementation); + foreach (var sharedImplementation in sharedImplementations) services.AddScoped(sharedImplementation); + + var coreImplementations = typeof(IMidrandBooks).Assembly.GetTypes() + .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); + + foreach (var coreImplementation in coreImplementations) services.AddScoped(coreImplementation); return services; } diff --git a/LiteCharms.Features/Abstractions/IFeatures.cs b/LiteCharms.Features/Abstractions/IFeatures.cs new file mode 100644 index 0000000..48a49c5 --- /dev/null +++ b/LiteCharms.Features/Abstractions/IFeatures.cs @@ -0,0 +1,3 @@ +namespace LiteCharms.Features.Abstractions; + +public interface IFeatures; From 40f4656036c7315cf1334fc2cd18f746e55268b7 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Tue, 9 Jun 2026 22:32:54 +0200 Subject: [PATCH 129/153] Removed shopping cart objects --- .../Orders/CartService.cs | 128 ------------------ .../Orders/Models/Cart.cs | 18 --- .../Orders/Models/CartItem.cs | 12 -- 3 files changed, 158 deletions(-) delete mode 100644 LiteCharms.Features.MidrandBooks/Orders/CartService.cs delete mode 100644 LiteCharms.Features.MidrandBooks/Orders/Models/Cart.cs delete mode 100644 LiteCharms.Features.MidrandBooks/Orders/Models/CartItem.cs diff --git a/LiteCharms.Features.MidrandBooks/Orders/CartService.cs b/LiteCharms.Features.MidrandBooks/Orders/CartService.cs deleted file mode 100644 index ae3f8c7..0000000 --- a/LiteCharms.Features.MidrandBooks/Orders/CartService.cs +++ /dev/null @@ -1,128 +0,0 @@ -using LiteCharms.Features.Abstractions; -using LiteCharms.Features.MidrandBooks.Orders.Models; -using LiteCharms.Features.MidrandBooks.Products.Models; - -namespace LiteCharms.Features.MidrandBooks.Orders; - -public sealed class CartService : IService -{ - private Cart cart = new(); - - public Cart GetCart() => cart; - - public void LoadCart(Cart savedCart) => cart = savedCart; - - public void AddItem(ProductPrice productPrice) - { - var itemExists = false; - - for (var i = 0; i < cart.Items.Count; i++) - { - if (cart.Items[i].Price!.Id == productPrice.Id) - { - cart.Items[i].Quantity++; - cart.Items[i].Amount += productPrice.Amount; - - itemExists = true; - - break; - } - } - - if (!itemExists) - cart.Items.Add(new CartItem - { - Price = productPrice, - Amount = productPrice.Amount, - Quantity = 1, - }); - - CalculateTotalPrice(); - } - - public void UpdateQuantity(long productPriceId, int newQuantity) - { - if (newQuantity <= 0) - { - RemoveAllSameItem(productPriceId); - - return; - } - - for (var i = 0; i < cart.Items.Count; i++) - { - if (cart.Items[i].Price!.Id == productPriceId) - { - var oldQuantity = cart.Items[i].Quantity; - var pricePerUnit = cart.Items[i].Price!.Amount; - - cart.Items[i].Quantity = newQuantity; - cart.Items[i].Amount = pricePerUnit * newQuantity; - break; - } - } - - CalculateTotalPrice(); - } - - public void RemoveOneItem(long productPriceId) - { - for (var i = 0; i < cart.Items.Count; i++) - { - if (cart.Items[i].Price!.Id == productPriceId) - { - if (cart.Items[i].Quantity <= 1) - { - cart.Items.RemoveAt(i); - } - else - { - cart.Items[i].Quantity--; - cart.Items[i].Amount -= cart.Items[i].Price!.Amount; - } - - break; - } - } - - CalculateTotalPrice(); - } - - public void RemoveAllSameItem(long productPriceId) - { - if (cart.Items.Count == 0) return; - - var item = cart.Items.FirstOrDefault(i => i.Price?.Id == productPriceId); - - if (item is not null) cart.Items.Remove(item); - - CalculateTotalPrice(); - } - - public void Clear() - { - if(cart.CustomerId is not null || cart.OrderId is not null) - { - cart.TotalPrice = 0; - cart.TotalVat = 0; - cart.Items.Clear(); - - return; - } - - cart = new Cart(); - } - - public decimal CalculateTotalPrice() - { - if (cart.Items.Count == 0) return 0; - - var gross = cart.Items.Sum(i => i.Amount); - - if (!cart.IsVatInclusive) cart.TotalVat = gross * cart.VatRate; - - cart.TotalPrice = gross + cart.TotalVat; - - return cart.TotalPrice; - } -} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Cart.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Cart.cs deleted file mode 100644 index 1d1fd1e..0000000 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/Cart.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace LiteCharms.Features.MidrandBooks.Orders.Models; - -public sealed class Cart -{ - public long? CustomerId { get; set; } - - public long? OrderId { get; set; } - - public decimal TotalPrice { get; set; } - - public decimal TotalVat { get; set; } - - public decimal VatRate { get; set; } = 0.15m; - - public bool IsVatInclusive { get; set; } = true; - - public IList Items { get; set; } = []; -} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/CartItem.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/CartItem.cs deleted file mode 100644 index 5d73afe..0000000 --- a/LiteCharms.Features.MidrandBooks/Orders/Models/CartItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -using LiteCharms.Features.MidrandBooks.Products.Models; - -namespace LiteCharms.Features.MidrandBooks.Orders.Models; - -public sealed class CartItem -{ - public ProductPrice? Price { get; set; } - - public long Quantity { get; set; } - - public decimal Amount { get; set; } -} From f5f10355985150b7ac6febc5e30b36d3b8999f45 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 12 Jun 2026 08:43:58 +0200 Subject: [PATCH 130/153] Added CartService --- ...teCharms.Features.MidrandBooks.Seed.csproj | 36 ++-- .../LiteCharms.Features.MidrandBooks.csproj | 36 ++-- .../Payments/CartService.cs | 154 ++++++++++++++++++ .../Payments/Models/Cart.cs | 18 ++ .../Payments/Models/CartItem.cs | 17 ++ .../LiteCharms.Features.TechShop.csproj | 36 ++-- .../LiteCharms.Features.csproj | 44 ++--- 7 files changed, 265 insertions(+), 76 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Payments/CartService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/Cart.cs create mode 100644 LiteCharms.Features.MidrandBooks/Payments/Models/CartItem.cs diff --git a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj index 86652ed..aeaaf0b 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj +++ b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -30,11 +30,11 @@ - - - - - + + + + + @@ -47,9 +47,9 @@ - - - + + + @@ -58,12 +58,12 @@ - - + + - + @@ -75,13 +75,13 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -116,8 +116,8 @@ - - + + diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index a8bef3a..c2f0ac7 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -36,7 +36,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -50,11 +50,11 @@ - - - - - + + + + + @@ -67,9 +67,9 @@ - - - + + + @@ -78,12 +78,12 @@ - - + + - + @@ -95,13 +95,13 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -136,8 +136,8 @@ - - + + diff --git a/LiteCharms.Features.MidrandBooks/Payments/CartService.cs b/LiteCharms.Features.MidrandBooks/Payments/CartService.cs new file mode 100644 index 0000000..64d55bd --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/CartService.cs @@ -0,0 +1,154 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Browser; +using LiteCharms.Features.Hasher; +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.Payments; + +public sealed class CartService(LocalStorageService localStorage) : IService +{ + private readonly string CartStorageKey = HashService.ToMd5Hash(nameof(Cart)).Value; + + public Cart ShoppingCart { get; private set; } = new(); + + public event Action? OnCartChanged; + + public static Func GetCartItemQuantity = (shoppingCart, productPriceId) => + shoppingCart.Items.FirstOrDefault(p => p.Price!.Id == productPriceId)?.Quantity ?? 1; + + public Cart GetCart() => ShoppingCart; + + public void NotifyStateChanged() => OnCartChanged?.Invoke(); + + public async Task LoadCartFromStorageAsync() + { + var loadResult = await localStorage.GetAsync(CartStorageKey); + + if (loadResult.IsFailed) await localStorage.SaveAsync(CartStorageKey, ShoppingCart); + + if (loadResult.IsSuccess) ShoppingCart = loadResult.Value; + + NotifyStateChanged(); + } + + public async Task SaveCartToStorageAsync() => await localStorage.SaveAsync(CartStorageKey, ShoppingCart); + + public void AddItem(ProductPrice productPrice, Product product, Author author) + { + var itemExists = false; + + for (var i = 0; i < ShoppingCart.Items.Count; i++) + { + if (ShoppingCart.Items[i].Price!.Id == productPrice.Id) + { + ShoppingCart.Items[i].Quantity++; + ShoppingCart.Items[i].Amount += productPrice.Amount; + + itemExists = true; + + break; + } + } + + if (!itemExists) + ShoppingCart.Items.Add(new CartItem + { + Product = product, + Author = author, + Price = productPrice, + Amount = productPrice.Amount, + Quantity = 1, + }); + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void UpdateQuantity(long productPriceId, int delta) + { + for (var i = 0; i < ShoppingCart.Items.Count; i++) + { + if (ShoppingCart.Items[i].Price!.Id == productPriceId) + { + var oldQuantity = ShoppingCart.Items[i].Quantity; + var pricePerUnit = ShoppingCart.Items[i].Price!.Amount; + + ShoppingCart.Items[i].Quantity += delta; + ShoppingCart.Items[i].Amount = pricePerUnit * ShoppingCart.Items[i].Quantity; + break; + } + } + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void RemoveOneItem(long productPriceId) + { + for (var i = 0; i < ShoppingCart.Items.Count; i++) + { + if (ShoppingCart.Items[i].Price!.Id == productPriceId) + { + if (ShoppingCart.Items[i].Quantity <= 1) + { + ShoppingCart.Items.Remove(ShoppingCart.Items[i]); + + break; + } + else + { + ShoppingCart.Items[i].Quantity--; + ShoppingCart.Items[i].Amount -= ShoppingCart.Items[i].Price!.Amount; + } + + break; + } + } + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void RemoveAllSameItem(long productPriceId) + { + if (ShoppingCart.Items.Count == 0) return; + + var item = ShoppingCart.Items.FirstOrDefault(i => i.Price?.Id == productPriceId); + + if (item is not null) ShoppingCart.Items.Remove(item); + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void Clear() + { + if(ShoppingCart.CustomerId is not null || ShoppingCart.OrderId is not null) + { + ShoppingCart.TotalAmount = 0; + ShoppingCart.TotalVat = 0; + ShoppingCart.Items.Clear(); + + return; + } + + ShoppingCart = new Cart(); + + NotifyStateChanged(); + } + + public decimal CalculateTotalPrice() + { + if (ShoppingCart.Items.Count == 0) return 0; + + var gross = ShoppingCart.Items.Sum(i => i.Amount); + + if (!ShoppingCart.IsVatInclusive) ShoppingCart.TotalVat = gross * ShoppingCart.VatRate; + + ShoppingCart.TotalAmount = gross + ShoppingCart.TotalVat; + + return ShoppingCart.TotalAmount; + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/Cart.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Cart.cs new file mode 100644 index 0000000..3af0292 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Cart.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public sealed class Cart +{ + public long? CustomerId { get; set; } + + public long? OrderId { get; set; } + + public decimal TotalAmount { get; set; } + + public decimal TotalVat { get; set; } + + public decimal VatRate { get; set; } = 0.15m; + + public bool IsVatInclusive { get; set; } = true; + + public IList Items { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/CartItem.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/CartItem.cs new file mode 100644 index 0000000..a656420 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/CartItem.cs @@ -0,0 +1,17 @@ +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public sealed class CartItem +{ + public Author? Author { get; set; } + + public Product? Product { get; set; } + + public ProductPrice? Price { get; set; } + + public int Quantity { get; set; } + + public decimal Amount { get; set; } +} diff --git a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj index 5f01765..2b3ad94 100644 --- a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj +++ b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj @@ -36,7 +36,7 @@ - + @@ -50,11 +50,11 @@ - - - - - + + + + + @@ -67,9 +67,9 @@ - - - + + + @@ -78,12 +78,12 @@ - - + + - + @@ -95,13 +95,13 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -136,8 +136,8 @@ - - + + diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index fc6cc2f..b2374c2 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -35,8 +35,8 @@ - - + + @@ -56,8 +56,8 @@ - - + + @@ -71,7 +71,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -85,11 +85,11 @@ - - - - - + + + + + @@ -102,9 +102,9 @@ - - - + + + @@ -113,12 +113,12 @@ - - + + - + @@ -130,13 +130,13 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -171,8 +171,8 @@ - - + + From a98adea8f3e7c6b8544b94922a444e2e51e840a3 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 12 Jun 2026 16:09:51 +0200 Subject: [PATCH 131/153] Implemented LiteCharms Security TokenService --- .../AuthorServiceFeatureTests.cs | 2 +- .../BooksServiceFeatureTests.cs | 2 +- .../CategoryServiceFeatureTests.cs | 2 +- .../CustomerServiceFeatureTests.cs | 2 +- ...eCharms.Features.MidrandBooks.Tests.csproj | 7 +- .../OrderServiceFeatureTests.cs | 2 +- .../PageServiceFeatureTests.cs | 2 +- .../PayfastServiceFeatureTests.cs | 2 +- .../PaymentServiceFeatureTests.cs | 2 +- .../ProductServiceFeatureTests.cs | 2 +- .../Fixture.cs | 11 ++- .../IntegrationFactAttribute.cs | 2 +- .../LiteCharms.Features.Tests.Common.csproj | 80 +++++++++++++++++++ .../appsettings.json | 10 ++- LiteCharms.Features.Tests/Fixture.cs | 36 --------- .../HashServiceFeatureTests.cs | 1 + .../LiteCharms.Features.Tests.csproj | 7 +- .../LiteCharmsApiFeatureTests.cs | 19 +++++ .../S3ServiceFeatureTests.cs | 1 + LiteCharms.Features.Tests/appsettings.json | 34 -------- .../Configuration/LiteCharmsClientSettings.cs | 14 ++++ .../Api/Models/TokenErrorResponse.cs | 13 +++ .../Api/Models/TokenRequest.cs | 20 +++++ .../Api/Models/TokenResponse.cs | 17 ++++ LiteCharms.Features/Api/Sdk/IConnectApi.cs | 10 +++ LiteCharms.Features/Api/TokenService.cs | 67 ++++++++++++++++ LiteCharms.Features/Extensions/Api.cs | 32 +++++++- .../LiteCharms.Features.csproj | 13 +++ LiteCharmsShared.slnx | 1 + 29 files changed, 313 insertions(+), 100 deletions(-) rename {LiteCharms.Features.MidrandBooks.Tests/Common => LiteCharms.Features.Tests.Common}/Fixture.cs (85%) rename {LiteCharms.Features.MidrandBooks.Tests/Common => LiteCharms.Features.Tests.Common}/IntegrationFactAttribute.cs (77%) create mode 100644 LiteCharms.Features.Tests.Common/LiteCharms.Features.Tests.Common.csproj rename {LiteCharms.Features.MidrandBooks.Tests => LiteCharms.Features.Tests.Common}/appsettings.json (75%) delete mode 100644 LiteCharms.Features.Tests/Fixture.cs create mode 100644 LiteCharms.Features.Tests/LiteCharmsApiFeatureTests.cs delete mode 100644 LiteCharms.Features.Tests/appsettings.json create mode 100644 LiteCharms.Features/Api/Configuration/LiteCharmsClientSettings.cs create mode 100644 LiteCharms.Features/Api/Models/TokenErrorResponse.cs create mode 100644 LiteCharms.Features/Api/Models/TokenRequest.cs create mode 100644 LiteCharms.Features/Api/Models/TokenResponse.cs create mode 100644 LiteCharms.Features/Api/Sdk/IConnectApi.cs create mode 100644 LiteCharms.Features/Api/TokenService.cs diff --git a/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs index d74aac1..8116065 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs @@ -1,7 +1,7 @@ using LiteCharms.Features.MidrandBooks.Authors; using LiteCharms.Features.MidrandBooks.Authors.Models; -using LiteCharms.Features.MidrandBooks.Tests.Common; using LiteCharms.Features.Models; +using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.MidrandBooks.Tests; diff --git a/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs index d65e0d6..57e4474 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs @@ -1,5 +1,5 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks; -using LiteCharms.Features.MidrandBooks.Tests.Common; +using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.MidrandBooks.Tests; diff --git a/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs index c1f103c..f84b3f2 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs @@ -1,5 +1,5 @@ using LiteCharms.Features.MidrandBooks.Categories; -using LiteCharms.Features.MidrandBooks.Tests.Common; +using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.MidrandBooks.Tests; diff --git a/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs index 5edea77..f102a78 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs @@ -1,6 +1,6 @@ using LiteCharms.Features.MidrandBooks.Customers; using LiteCharms.Features.MidrandBooks.Customers.Models; -using LiteCharms.Features.MidrandBooks.Tests.Common; +using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.MidrandBooks.Tests; diff --git a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj index a4b86f0..b8084c2 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj +++ b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj @@ -35,6 +35,7 @@ + @@ -45,10 +46,4 @@ - - - Always - - - \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs index ef09044..1e01faa 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs @@ -1,7 +1,7 @@ using LiteCharms.Features.MidrandBooks.Orders; using LiteCharms.Features.MidrandBooks.Orders.Models; -using LiteCharms.Features.MidrandBooks.Tests.Common; using LiteCharms.Features.Models; +using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.MidrandBooks.Tests; diff --git a/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs index a98c368..afecd71 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs @@ -1,5 +1,5 @@ using LiteCharms.Features.MidrandBooks.Pages; -using LiteCharms.Features.MidrandBooks.Tests.Common; +using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.MidrandBooks.Tests; diff --git a/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs index 65f4eed..7923a6b 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs @@ -1,6 +1,6 @@ using LiteCharms.Features.MidrandBooks.Payments; using LiteCharms.Features.MidrandBooks.Payments.Models; -using LiteCharms.Features.MidrandBooks.Tests.Common; +using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.MidrandBooks.Tests; diff --git a/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs index 3681116..c1514b1 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs @@ -1,6 +1,6 @@ using LiteCharms.Features.MidrandBooks.Payments; using LiteCharms.Features.MidrandBooks.Payments.Models; -using LiteCharms.Features.MidrandBooks.Tests.Common; +using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.MidrandBooks.Tests; diff --git a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs index 827025f..860f059 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs @@ -1,7 +1,7 @@ using LiteCharms.Features.MidrandBooks.Products; using LiteCharms.Features.MidrandBooks.Products.Models; -using LiteCharms.Features.MidrandBooks.Tests.Common; using LiteCharms.Features.Models; +using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.MidrandBooks.Tests; diff --git a/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs b/LiteCharms.Features.Tests.Common/Fixture.cs similarity index 85% rename from LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs rename to LiteCharms.Features.Tests.Common/Fixture.cs index a5a759f..507d4e1 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/Common/Fixture.cs +++ b/LiteCharms.Features.Tests.Common/Fixture.cs @@ -1,9 +1,7 @@ using LiteCharms.Features.Extensions; -using LiteCharms.Features.MidrandBooks.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; -using Microsoft.VisualStudio.TestPlatform.TestHost; -namespace LiteCharms.Features.MidrandBooks.Tests.Common; +namespace LiteCharms.Features.Tests.Common; public class Fixture : IDisposable { @@ -27,9 +25,8 @@ public class Fixture : IDisposable .Build(); Services = new ServiceCollection() - .AddHttpClient() - .AddMediator() .AddLogging() + .AddMediator() .AddEmailServiceBus() .AddGarageS3(Configuration) .AddMidrandShopDatabase(Configuration) @@ -37,7 +34,9 @@ public class Fixture : IDisposable .AddSingleton(Configuration) .AddShopServices() .AddHashServices(Configuration) - .BuildServiceProvider(); + .AddLiteCharmsApiSecurity(Configuration) + .AddSecurityApiSdk(Configuration) + .BuildServiceProvider(); ; Mediator = Services.GetRequiredService(); } diff --git a/LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs b/LiteCharms.Features.Tests.Common/IntegrationFactAttribute.cs similarity index 77% rename from LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs rename to LiteCharms.Features.Tests.Common/IntegrationFactAttribute.cs index 304f78e..f62577c 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/Common/IntegrationFactAttribute.cs +++ b/LiteCharms.Features.Tests.Common/IntegrationFactAttribute.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Features.MidrandBooks.Tests.Common; +namespace LiteCharms.Features.Tests.Common; public class IntegrationFactAttribute : FactAttribute { diff --git a/LiteCharms.Features.Tests.Common/LiteCharms.Features.Tests.Common.csproj b/LiteCharms.Features.Tests.Common/LiteCharms.Features.Tests.Common.csproj new file mode 100644 index 0000000..d90376e --- /dev/null +++ b/LiteCharms.Features.Tests.Common/LiteCharms.Features.Tests.Common.csproj @@ -0,0 +1,80 @@ + + + + net10.0 + enable + enable + 0521f45a-eba0-457f-bb5e-c3680f65d8b1 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/LiteCharms.Features.MidrandBooks.Tests/appsettings.json b/LiteCharms.Features.Tests.Common/appsettings.json similarity index 75% rename from LiteCharms.Features.MidrandBooks.Tests/appsettings.json rename to LiteCharms.Features.Tests.Common/appsettings.json index b6f6ba7..634da64 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/appsettings.json +++ b/LiteCharms.Features.Tests.Common/appsettings.json @@ -1,4 +1,13 @@ { + "LiteCharmsSettings": { + "Authority": "https://sts.security.khongisa.co.za", + "Audience": "midrandbooks-api" + }, + "LiteCharmsClientSettings": { + "Authority": "https://sts.security.khongisa.co.za", + "GrantType": "client_credentials", + "Scope": "midrandbooks-api" + }, "ValidPayfastHosts": [ "www.payfast.co.za", "sandbox.payfast.co.za", @@ -26,7 +35,6 @@ "UseSsl": true }, "Monitoring": { - "ApiKey": "", "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", "ServiceName": "LiteCharms.LeadGenerator" }, diff --git a/LiteCharms.Features.Tests/Fixture.cs b/LiteCharms.Features.Tests/Fixture.cs deleted file mode 100644 index 1ad8e4a..0000000 --- a/LiteCharms.Features.Tests/Fixture.cs +++ /dev/null @@ -1,36 +0,0 @@ -using LiteCharms.Features.Extensions; - -namespace LiteCharms.Features.Tests; - -public class Fixture : IDisposable -{ - public IConfiguration Configuration { get; set; } - - public IServiceProvider Services { get; set; } - - public IMediator Mediator { get; set; } - - public Fixture() - { - Configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddUserSecrets() - .AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .Build(); - - Services = new ServiceCollection() - .AddMediator() - .AddLogging() - .AddEmailServiceBus() - .AddGarageS3(Configuration) - .AddEmailServices(Configuration) - .AddSingleton(Configuration) - .AddHashServices(Configuration) - .BuildServiceProvider(); - - Mediator = Services.GetRequiredService(); - } - - public void Dispose() { } -} diff --git a/LiteCharms.Features.Tests/HashServiceFeatureTests.cs b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs index 844c7fb..8f2a331 100644 --- a/LiteCharms.Features.Tests/HashServiceFeatureTests.cs +++ b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs @@ -1,4 +1,5 @@ using LiteCharms.Features.Hasher; +using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.Tests; diff --git a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj index 2540fae..ef7e0dc 100644 --- a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj +++ b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj @@ -36,6 +36,7 @@ + @@ -43,10 +44,4 @@ - - - Always - - - \ No newline at end of file diff --git a/LiteCharms.Features.Tests/LiteCharmsApiFeatureTests.cs b/LiteCharms.Features.Tests/LiteCharmsApiFeatureTests.cs new file mode 100644 index 0000000..d33f897 --- /dev/null +++ b/LiteCharms.Features.Tests/LiteCharmsApiFeatureTests.cs @@ -0,0 +1,19 @@ +using LiteCharms.Features.Api; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.Tests; + +public sealed class LiteCharmsApiFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly TokenService tokenService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task TokenService_GenerateTokenAsync_ShouldReturn_TokenInResult() + { + var result = await tokenService.GenerateAsync(fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + Assert.NotEmpty(result.Value.AccessToken!); + } +} diff --git a/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs b/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs index 8af9a34..b355eac 100644 --- a/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs +++ b/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs @@ -1,4 +1,5 @@ using LiteCharms.Features.S3.Abstractions; +using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.Tests; diff --git a/LiteCharms.Features.Tests/appsettings.json b/LiteCharms.Features.Tests/appsettings.json deleted file mode 100644 index 1066af9..0000000 --- a/LiteCharms.Features.Tests/appsettings.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "BookshopS3Settings": { - "ServiceUrl": "http://192.168.1.177:30900", - "Region": "garage", - "BucketName": "bookshop", - "CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za" - }, - "BookshopQuotesS3Settings": { - "ServiceUrl": "http://192.168.1.177:30900", - "Region": "garage", - "BucketName": "bookshop.quotes", - "CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za" - }, - "Email": { - "Credentials": { - "Username": "shop@litecharms.co.za" - }, - "Port": 465, - "Host": "mail.litecharms.co.za", - "UseSsl": true - }, - "Monitoring": { - "ApiKey": "", - "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", - "ServiceName": "LiteCharms.LeadGenerator" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/LiteCharms.Features/Api/Configuration/LiteCharmsClientSettings.cs b/LiteCharms.Features/Api/Configuration/LiteCharmsClientSettings.cs new file mode 100644 index 0000000..aff2931 --- /dev/null +++ b/LiteCharms.Features/Api/Configuration/LiteCharmsClientSettings.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.Api.Configuration; + +public sealed class LiteCharmsClientSettings +{ + 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; } +} diff --git a/LiteCharms.Features/Api/Models/TokenErrorResponse.cs b/LiteCharms.Features/Api/Models/TokenErrorResponse.cs new file mode 100644 index 0000000..0d31195 --- /dev/null +++ b/LiteCharms.Features/Api/Models/TokenErrorResponse.cs @@ -0,0 +1,13 @@ +namespace LiteCharms.Features.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; } +} diff --git a/LiteCharms.Features/Api/Models/TokenRequest.cs b/LiteCharms.Features/Api/Models/TokenRequest.cs new file mode 100644 index 0000000..68b1abc --- /dev/null +++ b/LiteCharms.Features/Api/Models/TokenRequest.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.Api.Models; + +public sealed class TokenRequest +{ + [JsonPropertyName("grant_type")] + [AliasAs("grant_type")] + public string? GrantType { get; set; } + + [JsonPropertyName("client_id")] + [AliasAs("client_id")] + public string? ClientId { get; set; } + + [JsonPropertyName("client_secret")] + [AliasAs("client_secret")] + public string? ClientSecret { get; set; } + + [JsonPropertyName("scope")] + [AliasAs("scope")] + public string? Scope { get; set; } +} diff --git a/LiteCharms.Features/Api/Models/TokenResponse.cs b/LiteCharms.Features/Api/Models/TokenResponse.cs new file mode 100644 index 0000000..3fb090a --- /dev/null +++ b/LiteCharms.Features/Api/Models/TokenResponse.cs @@ -0,0 +1,17 @@ +namespace LiteCharms.Features.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; } +} + diff --git a/LiteCharms.Features/Api/Sdk/IConnectApi.cs b/LiteCharms.Features/Api/Sdk/IConnectApi.cs new file mode 100644 index 0000000..b7d3430 --- /dev/null +++ b/LiteCharms.Features/Api/Sdk/IConnectApi.cs @@ -0,0 +1,10 @@ +using LiteCharms.Features.Api.Models; + +namespace LiteCharms.Features.Api.Sdk; + +public interface IConnectApi +{ + [Post("/connect/token")] + ValueTask GetToken([Body(BodySerializationMethod.UrlEncoded)] TokenRequest request, + CancellationToken cancellationToken = default); +} diff --git a/LiteCharms.Features/Api/TokenService.cs b/LiteCharms.Features/Api/TokenService.cs new file mode 100644 index 0000000..c2dcdfc --- /dev/null +++ b/LiteCharms.Features/Api/TokenService.cs @@ -0,0 +1,67 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Api.Configuration; +using LiteCharms.Features.Api.Models; +using LiteCharms.Features.Api.Sdk; + +namespace LiteCharms.Features.Api; + +public sealed class TokenService(IConnectApi connectApi, IOptions clientOptions) : IService +{ + private readonly LiteCharmsClientSettings clientSettings = clientOptions.Value; + + public async Task> GenerateAsync(CancellationToken cancellationToken = default) + { + try + { + var request = new TokenRequest + { + ClientId = clientSettings.ClientId, + ClientSecret = clientSettings.ClientSecret, + GrantType = clientSettings.GrantType, + Scope = clientSettings.Scope, + }; + + using var response = await connectApi.GetToken(request, cancellationToken); + + var contentRaw = await response.Content.ReadAsStringAsync(cancellationToken); + + if (string.IsNullOrWhiteSpace(contentRaw)) + return Result.Fail(new Error($"The authentication endpoint returned an empty payload. Status code: {response.StatusCode}")); + + if (response.IsSuccessStatusCode) + { + var tokenResponse = JsonSerializer.Deserialize(contentRaw); + + return !string.IsNullOrWhiteSpace(tokenResponse?.AccessToken) + ? Result.Ok(tokenResponse) + : Result.Fail(new Error("Authentication succeeded, but no access token was found in the response payload.")); + } + + try + { + var errorResult = JsonSerializer.Deserialize(contentRaw); + + if (errorResult != null) + { + string summary = $"{errorResult.Error}: {errorResult.ErrorDescription}"; + + return Result.Fail(new Error(summary)); + } + } + catch + { + return Result.Fail(new Error($"Authentication failed: {contentRaw}")); + } + + return Result.Fail(new Error($"Authentication failed with status code: {response.StatusCode}")); + } + catch (OperationCanceledException ex) + { + return Result.Fail(new Error("The token generation request was canceled.").CausedBy(ex)); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} \ No newline at end of file diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 1e60834..9fe6af4 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -1,7 +1,7 @@ using LiteCharms.Features.Abstractions; using LiteCharms.Features.Api; using LiteCharms.Features.Api.Configuration; -using Microsoft.AspNetCore.Authentication.JwtBearer; +using LiteCharms.Features.Api.Sdk; namespace LiteCharms.Features.Extensions; @@ -9,6 +9,36 @@ public static class Api { public const string Books = nameof(Books); public const string Payments = nameof(Payments); + + public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration) + { + var configSection = configuration.GetSection(nameof(LiteCharmsClientSettings)); + + var authOptions = new LiteCharmsClientSettings(); + configSection.Bind(authOptions); + + services.Configure(configSection); + + if (string.IsNullOrWhiteSpace(authOptions.Authority)) + return services; + + if (!authOptions.Authority.EndsWith("/", StringComparison.Ordinal)) authOptions.Authority += "/"; + + services.AddRefitClient() + .ConfigureHttpClient(config => + { + config.BaseAddress = new Uri(authOptions.Authority); + config.Timeout = TimeSpan.FromSeconds(15); + }) + .AddStandardResilienceHandler(options => + { + options.Retry.MaxRetryAttempts = 3; + options.Retry.Delay = TimeSpan.FromSeconds(1); + options.Retry.BackoffType = Polly.DelayBackoffType.Exponential; + }); + + return services; + } public static IServiceCollection AddLiteCharmsWebSecurity(this IServiceCollection services, IConfiguration configuration) { diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index b2374c2..f0ee577 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -37,11 +37,23 @@ + + + + + + + + + + + + @@ -183,6 +195,7 @@ + diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index be7cb5c..de05e99 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -12,6 +12,7 @@ + From 3daf192ce953733c6d4032a20b926ecf77951d65 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 12 Jun 2026 16:15:44 +0200 Subject: [PATCH 132/153] Fixd double package reference to Mediator.SourceGenerator --- .../LiteCharms.Features.Tests.Common.csproj | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/LiteCharms.Features.Tests.Common/LiteCharms.Features.Tests.Common.csproj b/LiteCharms.Features.Tests.Common/LiteCharms.Features.Tests.Common.csproj index d90376e..44d6d70 100644 --- a/LiteCharms.Features.Tests.Common/LiteCharms.Features.Tests.Common.csproj +++ b/LiteCharms.Features.Tests.Common/LiteCharms.Features.Tests.Common.csproj @@ -12,11 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + From 4d2b37ace7e54fdd94ca435c4221699a32a90fac Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 12 Jun 2026 20:48:12 +0200 Subject: [PATCH 133/153] Completed token service --- ...PaymentConfirmationReceivedEventHandler.cs | 12 +++++----- .../Payments/PayfastService.cs | 10 ++++---- LiteCharms.Features.Tests.Common/Fixture.cs | 1 + .../appsettings.json | 23 +++++++++++-------- .../LiteCharms.Features.Tests.csproj | 1 + .../PayfastFeatureTests.cs | 18 +++++++++++++++ .../Api/Configuration/PayfastSettings.cs | 14 +++++++++++ LiteCharms.Features/Extensions/Api.cs | 9 ++++++++ 8 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 LiteCharms.Features.Tests/PayfastFeatureTests.cs create mode 100644 LiteCharms.Features/Api/Configuration/PayfastSettings.cs diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs index cae4c21..8211b4e 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs @@ -1,15 +1,16 @@ -using LiteCharms.Features.Hasher; -using LiteCharms.Features.Hasher.Configuration; +using LiteCharms.Features.Api.Configuration; +using LiteCharms.Features.Hasher; using LiteCharms.Features.Mediator; using LiteCharms.Features.MidrandBooks.Orders; using LiteCharms.Features.MidrandBooks.Payments.Models; namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers; -public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, IOptions hasherOptions, ILogger logger) : +public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, + IOptions payfastOptions, ILogger logger) : INotificationHandler { - private readonly HasherSettings hasherSettings = hasherOptions.Value; + private readonly PayfastSettings pasfastSettings = payfastOptions.Value; public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken) { @@ -25,7 +26,7 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi var payload = notification.Payload ?? throw new Exception("Payload metadata context context is null."); var dict = payload.ToParamDictionary(); - var localSignature = PayfastService.GenerateSignature(dict, hasherSettings.PayfastPassphrase); + var localSignature = PayfastService.GenerateSignature(dict, pasfastSettings.Passphrase); if (localSignature.IsFailed) throw new Exception("Failed to generate local signature for incoming webhook payload."); @@ -159,6 +160,5 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi logger.LogInformation("Webhook validation pipeline passed checks successfully, logged entry to ledger with status: {Status}", status); } activity?.SetStatus(ActivityStatusCode.Ok); - } } diff --git a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs index 7167285..2d8ec63 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs @@ -1,4 +1,5 @@ using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Api.Configuration; using LiteCharms.Features.Hasher; using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Postgres; @@ -6,13 +7,11 @@ using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.Payments; public sealed partial class PayfastService(IDbContextFactory contextFactory, - ILogger logger, IHttpClientFactory httpClientFactory, IConfiguration configuration) : IService + IOptions payfastOptions, ILogger logger, IHttpClientFactory httpClientFactory) : IService { [GeneratedRegex(@"%[0-9A-Fa-f]{2}", RegexOptions.None, matchTimeoutMilliseconds: 1000)] public static partial Regex PercentEncodingRegex { get; } - public readonly string[] ValidHosts = configuration.GetSection("ValidPayfastHosts").Get() ?? []; - public async ValueTask> WriteLedgerEntryAsync(CreateGatewayLedgerEntry request, CancellationToken cancellationToken = default) { try @@ -51,6 +50,9 @@ public sealed partial class PayfastService(IDbContextFactory> ValidateReferrerIpAsync(string remoteIpAddress, bool allowLoopback = false, CancellationToken cancellationToken = default) { + if(payfastOptions.Value?.ValidHosts?.Length == 0) + return Result.Fail("Valid payfast hosts not configured."); + if (string.IsNullOrWhiteSpace(remoteIpAddress)) return Result.Fail("Remote IP address is null or whitespace."); @@ -58,7 +60,7 @@ public sealed partial class PayfastService(IDbContextFactory(); - foreach (var host in ValidHosts) + foreach (var host in payfastOptions.Value!.ValidHosts!) { try { diff --git a/LiteCharms.Features.Tests.Common/Fixture.cs b/LiteCharms.Features.Tests.Common/Fixture.cs index 507d4e1..5694f5f 100644 --- a/LiteCharms.Features.Tests.Common/Fixture.cs +++ b/LiteCharms.Features.Tests.Common/Fixture.cs @@ -36,6 +36,7 @@ public class Fixture : IDisposable .AddHashServices(Configuration) .AddLiteCharmsApiSecurity(Configuration) .AddSecurityApiSdk(Configuration) + .AddPayfastServices(Configuration) .BuildServiceProvider(); ; Mediator = Services.GetRequiredService(); diff --git a/LiteCharms.Features.Tests.Common/appsettings.json b/LiteCharms.Features.Tests.Common/appsettings.json index 634da64..cc03b14 100644 --- a/LiteCharms.Features.Tests.Common/appsettings.json +++ b/LiteCharms.Features.Tests.Common/appsettings.json @@ -1,22 +1,25 @@ { + "PayfastSettings": { + "CheckoutUrl": "https://sandbox.payfast.co.za/eng/process", + "ValidHosts": [ + "www.payfast.co.za", + "sandbox.payfast.co.za", + "w1w.payfast.co.za", + "w2w.payfast.co.za", + "ips.payfast.co.za", + "api.payfast.co.za", + "payment.payfast.io" + ] + }, "LiteCharmsSettings": { "Authority": "https://sts.security.khongisa.co.za", "Audience": "midrandbooks-api" }, "LiteCharmsClientSettings": { - "Authority": "https://sts.security.khongisa.co.za", + "Authority": "https://sts.security.khongisa.co.za", "GrantType": "client_credentials", "Scope": "midrandbooks-api" }, - "ValidPayfastHosts": [ - "www.payfast.co.za", - "sandbox.payfast.co.za", - "w1w.payfast.co.za", - "w2w.payfast.co.za", - "ips.payfast.co.za", - "api.payfast.co.za", - "payment.payfast.io" - ], "HasherSettings": { "MinHashLength": 11 }, diff --git a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj index ef7e0dc..239381b 100644 --- a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj +++ b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj @@ -31,6 +31,7 @@ + diff --git a/LiteCharms.Features.Tests/PayfastFeatureTests.cs b/LiteCharms.Features.Tests/PayfastFeatureTests.cs new file mode 100644 index 0000000..e1fec0f --- /dev/null +++ b/LiteCharms.Features.Tests/PayfastFeatureTests.cs @@ -0,0 +1,18 @@ +using LiteCharms.Features.Api.Configuration; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.Tests; + +public sealed class PayfastFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly PayfastSettings payfastSettings = fixture.Services.GetRequiredService>().Value; + + [IntegrationFact] + public void PayfastSettings_ShouldFail_IfNotLoaded() + { + Assert.NotEmpty(payfastSettings.CheckoutUrl!); + Assert.NotEmpty(payfastSettings.MerchantId!); + Assert.NotEmpty(payfastSettings.MerchantKey!); + Assert.NotEmpty(payfastSettings.Passphrase!); + } +} diff --git a/LiteCharms.Features/Api/Configuration/PayfastSettings.cs b/LiteCharms.Features/Api/Configuration/PayfastSettings.cs new file mode 100644 index 0000000..0a9d5c1 --- /dev/null +++ b/LiteCharms.Features/Api/Configuration/PayfastSettings.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.Api.Configuration; + +public sealed class PayfastSettings +{ + public string? CheckoutUrl { get; set; } + + public string? Passphrase { get; set; } + + public string? MerchantId { get; set; } + + public string? MerchantKey { get; set; } + + public string[]? ValidHosts { get; set; } +} diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index 9fe6af4..f6857bb 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -9,6 +9,15 @@ public static class Api { public const string Books = nameof(Books); public const string Payments = nameof(Payments); + + public static IServiceCollection AddPayfastServices(this IServiceCollection services, IConfiguration configuration) + { + var configSection = configuration.GetSection(nameof(PayfastSettings)); + + services.Configure(configSection); + + return services; + } public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration) { From b60b8236affebdc43fa055389058a0017f3a6d37 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 12 Jun 2026 21:57:34 +0200 Subject: [PATCH 134/153] Added GetBookByProductId() --- .../AuthorBooks/BooksService.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index 2ee001e..4051f8f 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -58,6 +58,30 @@ public sealed class BooksService(IDbContextFactory contex } } + public async ValueTask> GetBookByProductIdAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var book = await context.Books + .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product) + .ThenInclude(b => b!.Prices) + .Include(b => b.Pages) + .FirstOrDefaultAsync(b => b.ProductId == productId, cancellationToken); + + return book is null + ? Result.Fail(new Error($"Book with product ID {productId} not found")) + : Result.Ok(book.ToModel()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + public async ValueTask> GetBookAsync(long bookId, CancellationToken cancellationToken = default) { try From 618e57074aaa68fdc1c02a75202f1535881f2e54 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 12 Jun 2026 22:08:54 +0200 Subject: [PATCH 135/153] Added new service methods --- .../Customers/CustomerService.cs | 22 +++++++++++++++++++ .../Orders/OrderService.cs | 21 ++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs index 5350220..0bb50a5 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs @@ -334,6 +334,28 @@ public sealed class CustomerService(IDbContextFactory con } } + public async ValueTask> GetCustomerAsync(string email, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers + .AsNoTracking() + .Include(c => c.Contacts) + .Include(c => c.Addresses) + .FirstOrDefaultAsync(c => c.Email == email, cancellationToken); + + return customer is not null + ? Result.Ok(customer.ToModel()) + : Result.Fail(new Error($"Customer with email '{email}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + public async ValueTask> GetCustomerAsync(long customerId, CancellationToken cancellationToken = default) { try diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs index 4ea6252..e94cded 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs @@ -164,6 +164,27 @@ public sealed class OrderService(IDbContextFactory contex public async ValueTask CancelOrderAsync(long orderId, CancellationToken cancellationToken = default) => await UpdateOrderStatusAsync(orderId, OrderStatus.Cancelled, cancellationToken); + public async ValueTask> GetPendingOrderAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.AsNoTracking() + .Where(o => o.Status == OrderStatus.Pending && o.CustomerId == customerId) + .OrderByDescending(o => o.Id) + .FirstOrDefaultAsync(cancellationToken); + + return order is not null + ? Result.Ok(order.ToModel()) + : Result.Fail("Order not found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + public async ValueTask> GetOrderAsync(long orderId, CancellationToken cancellationToken = default) { try From 310c1237b164cf104e61a55548469c3b760f4902 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 12 Jun 2026 23:00:57 +0200 Subject: [PATCH 136/153] Changed optional fields on Customer entity --- .../CustomerServiceFeatureTests.cs | 14 +- .../Entities/CustomerConfiguration.cs | 4 +- ...OnlyEmailIsMandatoryOnCustomer.Designer.cs | 1290 +++++++++++++++++ ...12210020_OnlyEmailIsMandatoryOnCustomer.cs | 54 + .../MidrandBooksDbContextModelSnapshot.cs | 4 +- 5 files changed, 1358 insertions(+), 8 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.cs diff --git a/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs index f102a78..3e30c77 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs @@ -11,12 +11,20 @@ public class CustomerServiceFeatureTests(Fixture fixture) : IClassFixture builder.Property(c => c.Company).IsRequired(false); builder.Property(c => c.VatNumber).IsRequired(false); builder.Property(c => c.Email).IsRequired(); - builder.Property(c => c.Phone).IsRequired(); - builder.Property(c => c.Website).IsRequired(); + builder.Property(c => c.Phone).IsRequired(false); + builder.Property(c => c.Website).IsRequired(false); builder.Property(c => c.Enabled).HasDefaultValue(true); builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); }); diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.Designer.cs new file mode 100644 index 0000000..b0689aa --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.Designer.cs @@ -0,0 +1,1290 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260612210020_OnlyEmailIsMandatoryOnCustomer")] + partial class OnlyEmailIsMandatoryOnCustomer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountGross") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountNet") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerEmail") + .HasColumnType("text"); + + b.Property("MerchantPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PayfastPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("GatewayLedger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("MerchantPaymentId") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.cs new file mode 100644 index 0000000..a49fdcf --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class OnlyEmailIsMandatoryOnCustomer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Website", + table: "Customers", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Phone", + table: "Customers", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Website", + table: "Customers", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Phone", + table: "Customers", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 8ddc085..44c0efd 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("ProductVersion", "10.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -309,7 +309,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasDefaultValue(true); b.Property("Phone") - .IsRequired() .HasColumnType("text"); b.Property("UpdatedAt") @@ -321,7 +320,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("text"); b.Property("Website") - .IsRequired() .HasColumnType("text"); b.HasKey("Id"); From 5edff5e272d32eb64c21534b4fa8ff3b8dc9e28f Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 12 Jun 2026 23:19:40 +0200 Subject: [PATCH 137/153] Refactored GenerateSignature --- .../CustomerServiceFeatureTests.cs | 14 +++----------- .../Payments/PayfastService.cs | 10 ++++++---- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs index 3e30c77..f102a78 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs @@ -11,20 +11,12 @@ public class CustomerServiceFeatureTests(Fixture fixture) : IClassFixture m.Value.ToLowerInvariant()); @@ -167,13 +170,12 @@ public sealed partial class PayfastService(IDbContextFactory m.Value.ToLowerInvariant()); getString += $"&passphrase={safePassphrase}"; } return HashService.ToMd5Hash(getString); - } + } } From 7d2bc7f1f248edbc2e64b4c37197a61928ec6d58 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 12 Jun 2026 23:26:54 +0200 Subject: [PATCH 138/153] Refactored PayfastService.GenerateSignature() --- .../Payments/PayfastService.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs index 1208667..8aa1eec 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs @@ -157,9 +157,11 @@ public sealed partial class PayfastService(IDbContextFactory m.Value.ToLowerInvariant()); + // 2. Payfast requires all OTHER percent-encoded hex arrays to be UPPERCASE (e.g., %3A instead of %3a) + string val = Regex.Replace(encodedVal, "%[0-9A-Fa-f]{2}", m => m.Value.ToUpperInvariant()); pfOutput.Append($"{key}={val}&"); } @@ -170,8 +172,9 @@ public sealed partial class PayfastService(IDbContextFactory m.Value.ToLowerInvariant()); + // Apply the exact same encoding rule to your passphrase + string encodedPassphrase = HttpUtility.UrlEncode(passPhrase.Trim()); + string safePassphrase = Regex.Replace(encodedPassphrase, "%[0-9A-Fa-f]{2}", m => m.Value.ToUpperInvariant()); getString += $"&passphrase={safePassphrase}"; } From e3e49b8db2f9048e75eadb382fc1bede7c53258d Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 12 Jun 2026 23:30:13 +0200 Subject: [PATCH 139/153] Honoring the mandatory field sequence --- .../Payments/PayfastService.cs | 62 ++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs index 8aa1eec..8d440d8 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs @@ -147,23 +147,56 @@ public sealed partial class PayfastService(IDbContextFactory m.Value.ToUpperInvariant()); - string key = kvp.Key; - - // 1. Payfast requires spaces to be '+' signs. HttpUtility does this perfectly. - string encodedVal = HttpUtility.UrlEncode(kvp.Value.Trim()); - - // 2. Payfast requires all OTHER percent-encoded hex arrays to be UPPERCASE (e.g., %3A instead of %3a) - string val = Regex.Replace(encodedVal, "%[0-9A-Fa-f]{2}", m => m.Value.ToUpperInvariant()); - - pfOutput.Append($"{key}={val}&"); + pfOutput.Append($"{key}={val}&"); + } } string getString = pfOutput.Length > 0 @@ -172,7 +205,6 @@ public sealed partial class PayfastService(IDbContextFactory m.Value.ToUpperInvariant()); From 9296f0331ec01f42d6bd4a7b40b5cf4f3b863e7d Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 10:06:54 +0200 Subject: [PATCH 140/153] Refactored registration of Features service from Scoped to Transient --- LiteCharms.Features.MidrandBooks/Extensions/Shop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs index 254f236..c5d2666 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -12,7 +12,7 @@ public static class Shop var sharedImplementations = typeof(IFeatures).Assembly.GetTypes() .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); - foreach (var sharedImplementation in sharedImplementations) services.AddScoped(sharedImplementation); + foreach (var sharedImplementation in sharedImplementations) services.AddTransient(sharedImplementation); var coreImplementations = typeof(IMidrandBooks).Assembly.GetTypes() .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); From 8e1df7938b650e7d56db64cb02f67688bd976076 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 10:18:42 +0200 Subject: [PATCH 141/153] Removed automatic LocalStorageService registration --- LiteCharms.Features.MidrandBooks/Extensions/Shop.cs | 2 +- LiteCharms.Features/Browser/LocalStorageService.cs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs index c5d2666..254f236 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -12,7 +12,7 @@ public static class Shop var sharedImplementations = typeof(IFeatures).Assembly.GetTypes() .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); - foreach (var sharedImplementation in sharedImplementations) services.AddTransient(sharedImplementation); + foreach (var sharedImplementation in sharedImplementations) services.AddScoped(sharedImplementation); var coreImplementations = typeof(IMidrandBooks).Assembly.GetTypes() .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); diff --git a/LiteCharms.Features/Browser/LocalStorageService.cs b/LiteCharms.Features/Browser/LocalStorageService.cs index f5168ac..9abf93b 100644 --- a/LiteCharms.Features/Browser/LocalStorageService.cs +++ b/LiteCharms.Features/Browser/LocalStorageService.cs @@ -1,8 +1,6 @@ -using LiteCharms.Features.Abstractions; +namespace LiteCharms.Features.Browser; -namespace LiteCharms.Features.Browser; - -public sealed class LocalStorageService(ProtectedLocalStorage storage) : IService +public sealed class LocalStorageService(ProtectedLocalStorage storage) { public async ValueTask DeleteAsync(string key) { From 9b474a398bf82ebefbad28bc5801f560b7e7c4c4 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 10:22:24 +0200 Subject: [PATCH 142/153] Removed automatic service registration for the CartService --- LiteCharms.Features.MidrandBooks/Payments/CartService.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/CartService.cs b/LiteCharms.Features.MidrandBooks/Payments/CartService.cs index 64d55bd..b78479c 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/CartService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/CartService.cs @@ -1,5 +1,4 @@ -using LiteCharms.Features.Abstractions; -using LiteCharms.Features.Browser; +using LiteCharms.Features.Browser; using LiteCharms.Features.Hasher; using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Payments.Models; @@ -7,7 +6,7 @@ using LiteCharms.Features.MidrandBooks.Products.Models; namespace LiteCharms.Features.MidrandBooks.Payments; -public sealed class CartService(LocalStorageService localStorage) : IService +public sealed class CartService(LocalStorageService localStorage) { private readonly string CartStorageKey = HashService.ToMd5Hash(nameof(Cart)).Value; From 630e74814b0664c0a5f21fb0eacb6c09ee4bddce Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 10:45:31 +0200 Subject: [PATCH 143/153] Catering for service registration of non-UI apps --- .../Extensions/Shop.cs | 6 +- .../Payments/PayfastService.cs | 62 +++++++++---------- LiteCharms.Features/Api/TokenService.cs | 5 +- LiteCharms.Features/Extensions/Api.cs | 2 + 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs index 254f236..892c282 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -1,11 +1,12 @@ using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Browser; using LiteCharms.Features.MidrandBooks.Abstractions; namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Shop { - public static IServiceCollection AddShopServices(this IServiceCollection services) + public static IServiceCollection AddShopServices(this IServiceCollection services, bool includeLocalStorage = false) { var serviceType = typeof(IService); @@ -19,6 +20,9 @@ public static class Shop foreach (var coreImplementation in coreImplementations) services.AddScoped(coreImplementation); + if (includeLocalStorage) + services.AddScoped(); + return services; } } diff --git a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs index 8d440d8..5cb585c 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs @@ -147,8 +147,35 @@ public sealed partial class PayfastService(IDbContextFactory m.Value.ToUpperInvariant()); + + pfOutput.Append($"{key}={val}&"); + } + } + + var getString = pfOutput.Length > 0 + ? pfOutput.ToString()[..^1] + : string.Empty; + + if (!string.IsNullOrWhiteSpace(passPhrase)) + { + string encodedPassphrase = HttpUtility.UrlEncode(passPhrase.Trim()); + string safePassphrase = PercentEncodingRegex.Replace(encodedPassphrase, m => m.Value.ToUpperInvariant()); + + getString += $"&passphrase={safePassphrase}"; + } + + return HashService.ToMd5Hash(getString); + } + + private static string[] GetPayfastMandatoryFieldSequence() => [ "merchant_id", "merchant_key", @@ -182,35 +209,4 @@ public sealed partial class PayfastService(IDbContextFactory m.Value.ToUpperInvariant()); - - pfOutput.Append($"{key}={val}&"); - } - } - - string getString = pfOutput.Length > 0 - ? pfOutput.ToString()[..^1] - : string.Empty; - - if (!string.IsNullOrWhiteSpace(passPhrase)) - { - string encodedPassphrase = HttpUtility.UrlEncode(passPhrase.Trim()); - string safePassphrase = Regex.Replace(encodedPassphrase, "%[0-9A-Fa-f]{2}", m => m.Value.ToUpperInvariant()); - - getString += $"&passphrase={safePassphrase}"; - } - - return HashService.ToMd5Hash(getString); - } } diff --git a/LiteCharms.Features/Api/TokenService.cs b/LiteCharms.Features/Api/TokenService.cs index c2dcdfc..a1872c5 100644 --- a/LiteCharms.Features/Api/TokenService.cs +++ b/LiteCharms.Features/Api/TokenService.cs @@ -1,11 +1,10 @@ -using LiteCharms.Features.Abstractions; -using LiteCharms.Features.Api.Configuration; +using LiteCharms.Features.Api.Configuration; using LiteCharms.Features.Api.Models; using LiteCharms.Features.Api.Sdk; namespace LiteCharms.Features.Api; -public sealed class TokenService(IConnectApi connectApi, IOptions clientOptions) : IService +public sealed class TokenService(IConnectApi connectApi, IOptions clientOptions) { private readonly LiteCharmsClientSettings clientSettings = clientOptions.Value; diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs index f6857bb..ae57c25 100644 --- a/LiteCharms.Features/Extensions/Api.cs +++ b/LiteCharms.Features/Extensions/Api.cs @@ -46,6 +46,8 @@ public static class Api options.Retry.BackoffType = Polly.DelayBackoffType.Exponential; }); + services.AddScoped(); + return services; } From b984dab2be1b6051f371fb241fb508f142e42266 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 12:08:23 +0200 Subject: [PATCH 144/153] Updated valid payfast addresses --- LiteCharms.Features.Tests.Common/appsettings.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/LiteCharms.Features.Tests.Common/appsettings.json b/LiteCharms.Features.Tests.Common/appsettings.json index cc03b14..5bc3088 100644 --- a/LiteCharms.Features.Tests.Common/appsettings.json +++ b/LiteCharms.Features.Tests.Common/appsettings.json @@ -4,8 +4,6 @@ "ValidHosts": [ "www.payfast.co.za", "sandbox.payfast.co.za", - "w1w.payfast.co.za", - "w2w.payfast.co.za", "ips.payfast.co.za", "api.payfast.co.za", "payment.payfast.io" From 99c0508f6ff2775577c747611ad014d86ea57385 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 15:45:59 +0200 Subject: [PATCH 145/153] Implemented separate signature validator --- .../LiteCharms.Features.MidrandBooks.csproj | 1 + .../Payments/PayfastService.cs | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index c2f0ac7..564430a 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -148,6 +148,7 @@ + diff --git a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs index 5cb585c..cbca90d 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs @@ -3,6 +3,7 @@ using LiteCharms.Features.Api.Configuration; using LiteCharms.Features.Hasher; using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.AspNetCore.Http; namespace LiteCharms.Features.MidrandBooks.Payments; @@ -48,6 +49,35 @@ public sealed partial class PayfastService(IDbContextFactory x.Key, x => x.Value.ToString()); + + if (!formFields.TryGetValue("signature", out string? incomingSignature)) + return false; + + var stringBuilder = new StringBuilder(); + + foreach (var key in formFields.Keys) + { + if (key.Equals("signature", StringComparison.OrdinalIgnoreCase)) + continue; + + string encodedVal = HttpUtility.UrlEncode(formFields[key].Trim()); + string cleanVal = PercentEncodingRegex.Replace(encodedVal, m => m.Value.ToUpperInvariant()); + + stringBuilder.Append($"{key}={cleanVal}&"); + } + + string encodedPassphrase = HttpUtility.UrlEncode(passphrase.Trim()); + string safePassphrase = PercentEncodingRegex.Replace(encodedPassphrase, m => m.Value.ToUpperInvariant()); + + stringBuilder.Append($"passphrase={safePassphrase}"); + + string generatedSignature = HashService.ToMd5Hash(stringBuilder.ToString()).Value; + return incomingSignature.Equals(generatedSignature, StringComparison.OrdinalIgnoreCase); + } + public async ValueTask> ValidateReferrerIpAsync(string remoteIpAddress, bool allowLoopback = false, CancellationToken cancellationToken = default) { if(payfastOptions.Value?.ValidHosts?.Length == 0) From 59fc0432b4bee005999d3b860a887c648bb6664e Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 15:49:45 +0200 Subject: [PATCH 146/153] ensure alphabetical sorting --- .../Payments/PayfastService.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs index cbca90d..5ebaa7e 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs @@ -3,7 +3,6 @@ using LiteCharms.Features.Api.Configuration; using LiteCharms.Features.Hasher; using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Postgres; -using Microsoft.AspNetCore.Http; namespace LiteCharms.Features.MidrandBooks.Payments; @@ -51,7 +50,10 @@ public sealed partial class PayfastService(IDbContextFactory x.Key, x => x.Value.ToString()); + var formFields = new Dictionary(StringComparer.Ordinal); + + foreach (var file in request.Form) + formFields.Add(file.Key, file.Value.ToString()); if (!formFields.TryGetValue("signature", out string? incomingSignature)) return false; @@ -63,18 +65,21 @@ public sealed partial class PayfastService(IDbContextFactory m.Value.ToUpperInvariant()); + string rawValue = formFields[key] ?? string.Empty; + + string encodedVal = HttpUtility.UrlEncode(rawValue.Trim()); + string cleanVal = PercentEncodingRegex.Replace(encodedVal, m => m.Value.ToUpperInvariant()); stringBuilder.Append($"{key}={cleanVal}&"); } string encodedPassphrase = HttpUtility.UrlEncode(passphrase.Trim()); - string safePassphrase = PercentEncodingRegex.Replace(encodedPassphrase, m => m.Value.ToUpperInvariant()); + string safePassphrase = PercentEncodingRegex.Replace(encodedPassphrase, m => m.Value.ToUpperInvariant()); stringBuilder.Append($"passphrase={safePassphrase}"); string generatedSignature = HashService.ToMd5Hash(stringBuilder.ToString()).Value; + return incomingSignature.Equals(generatedSignature, StringComparison.OrdinalIgnoreCase); } From 02d89eec4fab66caa980417248e71f3cefb6d506 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 15:58:30 +0200 Subject: [PATCH 147/153] refactored incoming signature validator to use form fields instead of httprequest --- .../Payments/PayfastService.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs index 5ebaa7e..bb797da 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs @@ -48,13 +48,8 @@ public sealed partial class PayfastService(IDbContextFactory formFields, string passphrase) { - var formFields = new Dictionary(StringComparer.Ordinal); - - foreach (var file in request.Form) - formFields.Add(file.Key, file.Value.ToString()); - if (!formFields.TryGetValue("signature", out string? incomingSignature)) return false; From e4c3779092de00d8235e886d3ff0cf8dba146c17 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 16:03:31 +0200 Subject: [PATCH 148/153] Using IFormCollection for VerifyIncomingSignatureFromForm --- .../Payments/PayfastService.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs index bb797da..06bdbca 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs @@ -48,21 +48,24 @@ public sealed partial class PayfastService(IDbContextFactory formFields, string passphrase) + public static bool VerifyIncomingSignatureFromForm(IFormCollection formCollection, string passphrase) { - if (!formFields.TryGetValue("signature", out string? incomingSignature)) - return false; + var sortedFields = new Dictionary(StringComparer.Ordinal); + + foreach (var field in formCollection) + { + sortedFields.Add(field.Key, field.Value.ToString()); + } + + if (!sortedFields.TryGetValue("signature", out var incomingSignature)) return false; var stringBuilder = new StringBuilder(); - foreach (var key in formFields.Keys) + foreach (var key in sortedFields.Keys) { - if (key.Equals("signature", StringComparison.OrdinalIgnoreCase)) - continue; + if (key.Equals("signature", StringComparison.OrdinalIgnoreCase)) continue; - string rawValue = formFields[key] ?? string.Empty; - - string encodedVal = HttpUtility.UrlEncode(rawValue.Trim()); + string encodedVal = HttpUtility.UrlEncode(sortedFields[key].Trim()); string cleanVal = PercentEncodingRegex.Replace(encodedVal, m => m.Value.ToUpperInvariant()); stringBuilder.Append($"{key}={cleanVal}&"); From c1e52ea9086a48a9527f7b3992020567ebdc13e4 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 16:26:47 +0200 Subject: [PATCH 149/153] Simplified PayfastPaymentConfirmationReceivedEventHandler --- .../LiteCharms.Features.MidrandBooks.csproj | 6 +- ...PaymentConfirmationReceivedEventHandler.cs | 110 ++++-------------- 2 files changed, 28 insertions(+), 88 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 564430a..022093b 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -32,7 +32,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -136,8 +136,8 @@ - - + + diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs index 8211b4e..aeb7974 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs @@ -1,17 +1,13 @@ -using LiteCharms.Features.Api.Configuration; -using LiteCharms.Features.Hasher; +using LiteCharms.Features.Hasher; using LiteCharms.Features.Mediator; using LiteCharms.Features.MidrandBooks.Orders; using LiteCharms.Features.MidrandBooks.Payments.Models; namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers; -public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, - IOptions payfastOptions, ILogger logger) : +public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, ILogger logger) : INotificationHandler { - private readonly PayfastSettings pasfastSettings = payfastOptions.Value; - public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken) { using var activity = MediatorTelemetry.Source.StartActivity($"Quartz: {typeof(PayfastPaymentConfirmationReceivedEvent).Name}"); @@ -23,83 +19,34 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi var paymentService = scope.ServiceProvider.GetRequiredService(); var payfastService = scope.ServiceProvider.GetRequiredService(); - var payload = notification.Payload ?? throw new Exception("Payload metadata context context is null."); + var payload = notification.Payload ?? throw new Exception("Payload metadata context is null."); - var dict = payload.ToParamDictionary(); - var localSignature = PayfastService.GenerateSignature(dict, pasfastSettings.Passphrase); + var hashResult = hashService.DecodeLongIdHash(payload.MerchantPaymentId!); + if (hashResult.IsFailed) throw new Exception("Failed to decode application tracking hash key identifier."); - if (localSignature.IsFailed) - throw new Exception("Failed to generate local signature for incoming webhook payload."); + var orderResult = await orderService.GetOrderAsync(hashResult.Value, cancellationToken); + if (orderResult.IsFailed) throw new Exception("Target system order entity context cannot be traced."); - if (!string.Equals(localSignature.Value, payload.Signature, StringComparison.OrdinalIgnoreCase)) + var paymentResult = await paymentService.GetOrderPaymentAsync(orderResult.Value.Id, cancellationToken); + if (paymentResult.IsFailed) throw new Exception("Target payment ledger entity cannot be resolved."); + + var isAlreadyProcessed = await paymentService.HasLedgerEntryAsync(orderResult.Value.Id, paymentResult.Value.Id, cancellationToken); + if (isAlreadyProcessed.Value) { - logger.LogCritical("Incoming webhook signature verification failed. Possible payload tampering."); + logger.LogWarning("Webhook reference token '{Ref}' already verified. Skipping processing routines.", payload.MerchantPaymentId); return; } - var hashResult = hashService.DecodeLongIdHash(payload.MerchantPaymentId!); - - if (hashResult.IsFailed) throw new Exception("Failed to decode application tracking hash key identifier."); - - var orderResult = await orderService.GetOrderAsync(hashResult.Value, cancellationToken); - - if (orderResult.IsFailed) throw new Exception("Target system order entity context cannot be traced."); - - var paymentResult = await paymentService.GetOrderPaymentAsync(orderResult.Value.Id, cancellationToken); - - if (paymentResult.IsFailed) throw new Exception("Target payment ledger entity cannot be resolved."); + var isAmountValid = payfastService.ValidatePaymentAmount(orderResult.Value.Total, payload.AmountGross); + if (!isAmountValid.Value) + throw new Exception("Security validation exception: Transaction cost variance bounds breached (Price Tampering Detected)."); decimal.TryParse(payload.AmountGross, CultureInfo.InvariantCulture, out var gross); decimal.TryParse(payload.AmountFee, CultureInfo.InvariantCulture, out var fee); decimal.TryParse(payload.AmountNet, CultureInfo.InvariantCulture, out var net); string status = payload.PaymentStatus ?? "UNKNOWN"; - var isAlreadyProcessed = await paymentService.HasLedgerEntryAsync(orderResult.Value.Id, paymentResult.Value.Id, cancellationToken); - - if (isAlreadyProcessed.Value) - { - logger.LogWarning("Webhook reference token '{Ref}' already verified. Skipping validation routines.", payload.MerchantPaymentId); - - return; - } - - if (notification.PerformBackgroundChecks) - { - var isHostValid = await payfastService.ValidateReferrerIpAsync(notification.RemoteIpAddress!, notification.AllowLoopback, cancellationToken); - - if (isHostValid.IsFailed) - throw new Exception("Security validation exception: Webhook packet source address failed cluster validation checks."); - - if (!isHostValid.Value) - throw new Exception("Security validation exception: Webhook packet source address failed cluster validation checks."); - - var isAmountValid = payfastService.ValidatePaymentAmount(orderResult.Value.Total, payload.AmountGross); - - if (!isAmountValid.Value) - throw new Exception("Security validation exception: Transaction cost variance bounds breached."); - - var paramList = new List(); - - foreach (var kvp in dict) - { - if (!string.IsNullOrEmpty(kvp.Value)) - { - string encoded = HttpUtility.UrlEncode(kvp.Value.Trim()); - - string safeValue = PayfastService.PercentEncodingRegex.Replace(encoded, m => m.Value.ToLowerInvariant()); - paramList.Add($"{kvp.Key}={safeValue}"); - } - } - - string rawParamString = string.Join("&", paramList); - - var serverConfirmation = await payfastService.ValidateServerConfirmationAsync(rawParamString, isSandbox: true, cancellationToken); - - if (serverConfirmation.IsFailed) - throw new Exception("Security validation exception: Payfast central handshake server rejected payload legitimacy."); - } - await payfastService.WriteLedgerEntryAsync(new CreateGatewayLedgerEntry { OrderId = orderResult.Value.Id, @@ -124,31 +71,23 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi CustomerId = orderResult.Value.CustomerId, }, cancellationToken); - if (ledgerWriteResult.IsFailed) - throw new Exception("Failed to write ledger entry for payment confirmation."); + if (ledgerWriteResult.IsFailed) throw new Exception("Failed to write ledger entry for payment confirmation."); var completePaymentResult = await paymentService.CompletePaymentAsync(paymentResult.Value.Id, PaymentStatuses.Paid, cancellationToken); - - if (completePaymentResult.IsFailed) - throw new Exception("Failed to update payment status to 'Paid' for payment confirmation."); + if (completePaymentResult.IsFailed) throw new Exception("Failed to update payment status to 'Paid'."); var updateOrderResult = await orderService.UpdateOrderStatusAsync(orderResult.Value.Id, OrderStatus.Completed, cancellationToken); - - if (updateOrderResult.IsFailed) - throw new Exception("Failed to update order status to 'Completed' for payment confirmation."); + if (updateOrderResult.IsFailed) throw new Exception("Failed to update order status to 'Completed'."); logger.LogInformation("Order payment verified secure and cleared successfully."); } else { - LedgerStatuses ledgerStatus; + LedgerStatuses ledgerStatus = status.Equals("CANCELLED", StringComparison.OrdinalIgnoreCase) + ? LedgerStatuses.Cancelled + : LedgerStatuses.Failed; - if (status.Equals("CANCELLED", StringComparison.OrdinalIgnoreCase)) - ledgerStatus = LedgerStatuses.Cancelled; - else - ledgerStatus = LedgerStatuses.Failed; - - var ledgerWriteResult = await paymentService.WriteLedgerEntryAsync(new CreateLedgerEntry + await paymentService.WriteLedgerEntryAsync(new CreateLedgerEntry { OrderId = orderResult.Value.Id, PaymentId = paymentResult.Value.Id, @@ -157,8 +96,9 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi CustomerId = orderResult.Value.CustomerId, }, cancellationToken); - logger.LogInformation("Webhook validation pipeline passed checks successfully, logged entry to ledger with status: {Status}", status); + logger.LogInformation("Webhook pipeline logged non-success entry to ledger with status: {Status}", status); } + activity?.SetStatus(ActivityStatusCode.Ok); } } From 9997d4f0edef85f89bc9264fedcc4230432c4b60 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 16:32:39 +0200 Subject: [PATCH 150/153] Fixed package mismatches --- .../LiteCharms.Features.MidrandBooks.Seed.csproj | 6 +++--- .../LiteCharms.Features.TechShop.csproj | 4 ++-- LiteCharms.Features/LiteCharms.Features.csproj | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj index aeaaf0b..9318292 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj +++ b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -116,8 +116,8 @@ - - + + diff --git a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj index 2b3ad94..5d01984 100644 --- a/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj +++ b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj @@ -136,8 +136,8 @@ - - + + diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index f0ee577..60259b7 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -79,7 +79,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -183,8 +183,8 @@ - - + + From 8e2942487d11d0702af3d96d7e76bb1700279198 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 17:00:08 +0200 Subject: [PATCH 151/153] Ensured the merchant payment id makes it to the ledger --- LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs index 97e9e21..1f39876 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -162,7 +162,8 @@ public sealed class PaymentService(IDbContextFactory cont CustomerId = request.CustomerId, OrderId = request.OrderId, PaymentId = request.PaymentId, - Status = request.Status, + MerchantPaymentId = request.PaymentGatewayReference, + Status = request.Status, }); return await context.SaveChangesAsync(cancellationToken) > 0 From 94b0084d752b474f35ad7822534461c10a600f36 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 18:03:40 +0200 Subject: [PATCH 152/153] Refactored idempotency check to observe completed status --- LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs index 1f39876..b1a3531 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -123,8 +123,7 @@ public sealed class PaymentService(IDbContextFactory cont await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var exists = await context.Ledger.AnyAsync(l => - l.OrderId == orderId && - l.PaymentId == paymentId, cancellationToken); + l.OrderId == orderId && l.PaymentId == paymentId && l.Status == LedgerStatuses.Completed, cancellationToken); return Result.Ok(exists); } From 7c5b5f1728f2a835d7ab462b47466eb238d6c279 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 13 Jun 2026 21:20:30 +0200 Subject: [PATCH 153/153] Refactored usaged of merchant payment id usage --- .../PayfastPaymentConfirmationReceivedEventHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs index aeb7974..5e4fe07 100644 --- a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs @@ -66,7 +66,7 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi { OrderId = orderResult.Value.Id, PaymentId = paymentResult.Value.Id, - PaymentGatewayReference = payload.PaymentId!, + PaymentGatewayReference = payload.MerchantPaymentId!, Status = LedgerStatuses.Completed, CustomerId = orderResult.Value.CustomerId, }, cancellationToken); @@ -91,7 +91,7 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi { OrderId = orderResult.Value.Id, PaymentId = paymentResult.Value.Id, - PaymentGatewayReference = payload.PaymentId!, + PaymentGatewayReference = payload.MerchantPaymentId!, Status = ledgerStatus, CustomerId = orderResult.Value.CustomerId, }, cancellationToken);