Compare commits

..
40 Commits
Author SHA1 Message Date
khwezi 371f8e6eef Merge commit '26075cd9a7c04c294053afead63c46fff7b16cfc' 2026-05-10 15:33:23 +00:00
Khwezi Mngoma 26075cd9a7 Updated job scheduler
continuous-integration/drone/pr Build is passing
2026-05-10 17:32:09 +02:00
khwezi 4deb732804 Merge pull request 'emailjobs' (#16) from emailjobs into master
Reviewed-on: #16
2026-05-10 16:51:24 +02:00
Khwezi Mngoma cecd9f90e9 Implemented service bus handling of emails and notification processing
continuous-integration/drone/pr Build is passing
2026-05-10 16:50:36 +02:00
Khwezi Mngoma 73ba41beaf Added outgoing email notification processing event 2026-05-10 16:07:53 +02:00
Khwezi Mngoma e8e9a85c57 Migrated database changes after refactoring the Notification model 2026-05-10 15:27:26 +02:00
Khwezi Mngoma 394429677e Added package management 2026-05-10 14:18:56 +02:00
khwezi 20d9387d0b Merge pull request 'Migrated database changes' (#15) from develop into master
Reviewed-on: #15
2026-05-10 11:17:54 +02:00
Khwezi Mngoma 32d1019eb5 Migrated database changes
continuous-integration/drone/pr Build is passing
2026-05-10 11:16:52 +02:00
khwezi 9f6d0ccaa0 Merge pull request 'Populated README' (#14) from develop into master
Reviewed-on: #14
2026-05-10 09:48:35 +02:00
Khwezi Mngoma 275befc258 Populated README
continuous-integration/drone/pr Build is passing
2026-05-10 09:48:12 +02:00
khwezi 1acbc4d213 Merge pull request 'fixed git repo naming' (#13) from develop into master
Reviewed-on: #13
2026-05-10 09:22:37 +02:00
Khwezi Mngoma 193496bbf9 fixed git repo naming
continuous-integration/drone/pr Build is passing
2026-05-10 09:22:09 +02:00
khwezi 8c99668fac Merge pull request 'Added tag and release step after publish' (#12) from develop into master
Reviewed-on: #12
2026-05-10 09:19:33 +02:00
Khwezi Mngoma a015801423 Added tag and release step after publish
continuous-integration/drone/pr Build is failing
2026-05-10 09:18:54 +02:00
khwezi ad44f46204 Merge pull request 'Refactored Quartz instance id to AUTO, removed constant' (#11) from develop into master
Reviewed-on: #11
2026-05-10 08:21:08 +02:00
Khwezi Mngoma 9d64b5e76d Refactored Quartz instance id to AUTO, removed constant
continuous-integration/drone/pr Build is passing
2026-05-10 08:20:18 +02:00
khwezi 49d999c1e3 Merge pull request 'Refactored postgres extension' (#10) from develop into master
Reviewed-on: #10
2026-05-09 17:45:00 +02:00
Khwezi Mngoma fd6b8cd965 Refactored postgres extension
continuous-integration/drone/pr Build is passing
2026-05-09 17:44:16 +02:00
khwezi 9ed4777a18 Merge pull request 'Refactored database references' (#9) from develop into master
Reviewed-on: #9
2026-05-09 17:01:02 +02:00
Khwezi Mngoma ff34326a53 Refactored database references
continuous-integration/drone/pr Build is passing
2026-05-09 16:58:34 +02:00
khwezi 0cf44f68cc Merge pull request 'Added scheduler constants' (#8) from develop into master
Reviewed-on: #8
2026-05-09 15:27:50 +02:00
Khwezi Mngoma 6db6228bc9 Added scheduler constants
continuous-integration/drone/pr Build is passing
2026-05-09 15:27:07 +02:00
khwezi 41ed5a4288 Merge pull request 'Fixed quartz host config table prefix' (#7) from develop into master
Reviewed-on: #7
2026-05-09 13:30:33 +02:00
Khwezi Mngoma 61118c6223 Fixed quartz host config table prefix
continuous-integration/drone/pr Build is passing
2026-05-09 13:29:35 +02:00
khwezi bbcba5e06c Merge pull request 'Fixed quartz table name prefix' (#6) from develop into master
Reviewed-on: #6
2026-05-09 13:28:29 +02:00
Khwezi Mngoma bd852b30c3 Fixed quartz table name prefix
continuous-integration/drone/pr Build is passing
2026-05-09 13:27:54 +02:00
khwezi 502cc326dd Merge pull request 'Updated pipeline to use major version with minor always 0' (#5) from develop into master
Reviewed-on: #5
2026-05-09 12:02:19 +02:00
Khwezi Mngoma 37e2116aaf Updated pipeline to use major version with minor always 0
continuous-integration/drone/pr Build is passing
2026-05-09 12:01:52 +02:00
khwezi 4675d4c5fc Merge pull request 'Updated pipeline to use major versions only' (#4) from develop into master
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is failing
Reviewed-on: #4
2026-05-09 11:56:20 +02:00
Khwezi Mngoma 60980292a2 Updated pipeline to use major versions only
continuous-integration/drone/pr Build is failing
2026-05-09 11:55:43 +02:00
khwezi f80bb2fff9 Merge pull request 'Changed target branch from nain to master' (#3) from dronepipeline into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: #3
2026-05-09 11:37:39 +02:00
Khwezi Mngoma df1e31cc86 Changed target branch from nain to master
continuous-integration/drone/pr Build is passing
2026-05-09 11:37:11 +02:00
khwezi 6767906b0d Merge pull request 'Added pipeline separator' (#2) from dronepipeline into master
Reviewed-on: #2
2026-05-09 11:25:34 +02:00
Khwezi Mngoma 8cb1943ac8 Added pipeline separator 2026-05-09 11:25:08 +02:00
khwezi a344af4498 Merge pull request 'Added .drone.yml pipeline' (#1) from dronepipeline into master
Reviewed-on: #1
2026-05-09 11:12:38 +02:00
Khwezi Mngoma c95efd1faf Added .drone.yml pipeline 2026-05-09 11:11:29 +02:00
Khwezi Mngoma 3065d8e27a Refactored service bus lifetiemes to singleton 2026-05-09 09:03:12 +02:00
Khwezi Mngoma d998aac7c4 Rfactored service bus service lifetime 2026-05-09 08:37:45 +02:00
Khwezi Mngoma 33633502e2 Added quartz scheduler client 2026-05-07 16:24:10 +02:00
134 changed files with 2570 additions and 3858 deletions
+71
View File
@@ -0,0 +1,71 @@
---
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.${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
- 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:
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/components.git
- git tag $VERSION
- git push origin $VERSION
- |
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 "{
\"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:
event:
- pull_request
+248
View File
@@ -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
+4
View File
@@ -4,6 +4,10 @@ public static class Constants
{ {
public const int QueueBounds = 100000; 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 EmailServiceBus = nameof(EmailServiceBus);
public const string GeneralServiceBus = nameof(GeneralServiceBus); public const string GeneralServiceBus = nameof(GeneralServiceBus);
public const string SalesServiceBus = nameof(SalesServiceBus); public const string SalesServiceBus = nameof(SalesServiceBus);
+12
View File
@@ -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();
}
@@ -11,7 +11,7 @@
<!-- Nuget Package Details --> <!-- Nuget Package Details -->
<PropertyGroup> <PropertyGroup>
<PackageId>LiteCharms.Abstractions</PackageId> <PackageId>LiteCharms.Abstractions</PackageId>
<Version>1.0.17</Version> <Version>1.0.20</Version>
<Authors>Khwezi Mngoma</Authors> <Authors>Khwezi Mngoma</Authors>
<Company>Lite Charms (PTY) Ltd</Company> <Company>Lite Charms (PTY) Ltd</Company>
<Description>Shared abstractions for Lite Charms applications.</Description> <Description>Shared abstractions for Lite Charms applications.</Description>
@@ -1,4 +1,6 @@
namespace LiteCharms.Entities.Configuration; using LiteCharms.Models;
namespace LiteCharms.Entities.Configuration;
public class NotificationConfiguration : IEntityTypeConfiguration<Notification> public class NotificationConfiguration : IEntityTypeConfiguration<Notification>
{ {
@@ -8,15 +10,21 @@ public class NotificationConfiguration : IEntityTypeConfiguration<Notification>
builder.HasKey(f => f.Id); builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd();
builder.Property(f => f.Direction).IsRequired(); builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate();
builder.Property(f => f.Author).IsRequired(); builder.Property(f => f.Direction).IsRequired().HasConversion<int>();
builder.Property(f => f.Title).IsRequired(); builder.Property(f => f.Platform).IsRequired().HasConversion<int>();
builder.Property(f => f.Description).IsRequired(); builder.Property(f => f.Priority).IsRequired().HasConversion<int>();
builder.Property(f => f.Platform).IsRequired(); builder.Property(f => f.CorrelationIdType).IsRequired().HasConversion<int>();
builder.Property(f => f.PlatformAddress).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.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.IsInternal).HasDefaultValue(true);
builder.Property(f => f.Processed).HasDefaultValue(false); builder.Property(f => f.Processed).HasDefaultValue(false);
builder.Property(f => f.HasError).HasDefaultValue(false);
builder.Property(f => f.Errors).HasColumnType("jsonb").IsRequired(false);
} }
} }
@@ -14,7 +14,10 @@ public class OrderConfiguration : IEntityTypeConfiguration<Order>
builder.Property(f => f.RefundId).IsRequired(false); builder.Property(f => f.RefundId).IsRequired(false);
builder.Property(f => f.ShoppingCartId).IsRequired(); builder.Property(f => f.ShoppingCartId).IsRequired();
builder.Property(f => f.Status).HasConversion<int>().IsRequired(); builder.Property(f => f.Status).HasConversion<int>().IsRequired();
builder.Property(f => f.Requirements).HasColumnType("jsonb").IsRequired(false);
builder.Property(f => f.Notes).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) builder.HasOne(f => f.Quote)
.WithOne(f => f.Order) .WithOne(f => f.Order)
@@ -0,0 +1,16 @@
namespace LiteCharms.Entities.Configuration;
public class PackageConfirguration : IEntityTypeConfiguration<Package>
{
public void Configure(EntityTypeBuilder<Package> 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);
}
}
@@ -0,0 +1,20 @@
namespace LiteCharms.Entities.Configuration;
public class PackageItemConfiguration : IEntityTypeConfiguration<PackageItem>
{
public void Configure(EntityTypeBuilder<PackageItem> 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);
}
}
@@ -8,10 +8,10 @@ public class QuoteConfiguration : IEntityTypeConfiguration<Quote>
builder.HasKey(f => f.Id); builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); 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.ExpiredAt).IsRequired(false);
builder.Property(f => f.CustomerId).IsRequired(); builder.Property(f => f.CustomerId).IsRequired();
builder.Property(f => f.Status).IsRequired(); builder.Property(f => f.Status).IsRequired().HasConversion<int>();
builder.Property(f => f.ShoppingCartId).IsRequired(); builder.Property(f => f.ShoppingCartId).IsRequired();
builder.Property(f => f.Reason).IsRequired(false); builder.Property(f => f.Reason).IsRequired(false);
@@ -8,7 +8,7 @@ public class ShoppingCartConfiguration : IEntityTypeConfiguration<ShoppingCart>
builder.HasKey(f => f.Id); builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); 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.CustomerId).IsRequired(false);
builder.Property(f => f.OrderId).IsRequired(false); builder.Property(f => f.OrderId).IsRequired(false);
builder.Property(f => f.QuoteId).IsRequired(false); builder.Property(f => f.QuoteId).IsRequired(false);
@@ -8,7 +8,7 @@ public class ShoppingCartItemConfiguration : IEntityTypeConfiguration<ShoppingCa
builder.HasKey(f => f.Id); builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); 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.Quantity).IsRequired().HasDefaultValue(1);
builder.Property(f => f.ProductPriceId).IsRequired(); builder.Property(f => f.ProductPriceId).IsRequired();
@@ -0,0 +1,24 @@
namespace LiteCharms.Entities.Configuration;
public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration<ShoppingCartPackage>
{
public void Configure(EntityTypeBuilder<ShoppingCartPackage> 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);
}
}
@@ -11,7 +11,7 @@
<!-- Nuget Package Details --> <!-- Nuget Package Details -->
<PropertyGroup> <PropertyGroup>
<PackageId>LiteCharms.Entities</PackageId> <PackageId>LiteCharms.Entities</PackageId>
<Version>1.0.17</Version> <Version>1.0.20</Version>
<Authors>Khwezi Mngoma</Authors> <Authors>Khwezi Mngoma</Authors>
<Company>Lite Charms (PTY) Ltd</Company> <Company>Lite Charms (PTY) Ltd</Company>
<Description>Shared entities for Lite Charms applications.</Description> <Description>Shared entities for Lite Charms applications.</Description>
+9
View File
@@ -0,0 +1,9 @@
using LiteCharms.Entities.Configuration;
namespace LiteCharms.Entities;
[EntityTypeConfiguration<PackageConfirguration, Package>]
public class Package : Models.Package
{
public virtual ICollection<PackageItem>? PackageItems { get; set; }
}
+9
View File
@@ -0,0 +1,9 @@
using LiteCharms.Entities.Configuration;
namespace LiteCharms.Entities;
[EntityTypeConfiguration<PackageItemConfiguration, PackageItem>]
public class PackageItem : Models.PackageItem
{
public virtual Package? Package { get; set; }
}
+2
View File
@@ -12,4 +12,6 @@ public class ShoppingCart : Models.ShoppingCart
public virtual Quote? Quote { get; set; } public virtual Quote? Quote { get; set; }
public virtual ICollection<ShoppingCartItem>? ShoppingCartItems { get; set; } public virtual ICollection<ShoppingCartItem>? ShoppingCartItems { get; set; }
public virtual ICollection<Package>? Packages { get; set; }
} }
@@ -0,0 +1,11 @@
using LiteCharms.Entities.Configuration;
namespace LiteCharms.Entities;
[EntityTypeConfiguration<ShoppingCartPackageConfiguration, ShoppingCartPackage>]
public class ShoppingCartPackage : Models.ShoppingCartPackage
{
public virtual ShoppingCart? ShoppingCart { get; set; }
public virtual Package? Package { get; set; }
}
+46 -6
View File
@@ -4,6 +4,36 @@ namespace LiteCharms.Extensions;
public static class EntityModeMappers 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) => public static ShoppingCartItem ToModel(this Entities.ShoppingCartItem entity) =>
new() new()
{ {
@@ -44,16 +74,23 @@ public static class EntityModeMappers
{ {
Id = entity.Id, Id = entity.Id,
CreatedAt = entity.CreatedAt, CreatedAt = entity.CreatedAt,
Description = entity.Description, Message = entity.Message,
Direction = entity.Direction, Direction = entity.Direction,
CorrelationId = entity.CorrelationId, CorrelationId = entity.CorrelationId,
CorrelationIdType = entity.CorrelationIdType, CorrelationIdType = entity.CorrelationIdType,
IsInternal = entity.IsInternal, IsInternal = entity.IsInternal,
Author = entity.Author, Sender = entity.Sender,
Platform = entity.Platform, Platform = entity.Platform,
PlatformAddress = entity.PlatformAddress, Recipient = entity.Recipient,
Title = entity.Title, Subject = entity.Subject,
Processed = entity.Processed Processed = entity.Processed,
SenderName = entity.SenderName,
RecipientAddress = entity.RecipientAddress,
Priority = entity.Priority,
UpdatedAt = entity?.UpdatedAt,
IsHtml = entity!.IsHtml,
HasError = entity.HasError,
Errors = entity.Errors
}; };
public static Customer ToModel(this Entities.Customer entity) => public static Customer ToModel(this Entities.Customer entity) =>
@@ -113,7 +150,10 @@ public static class EntityModeMappers
RefundId = entity.RefundId, RefundId = entity.RefundId,
QuoteId = entity.QuoteId, QuoteId = entity.QuoteId,
Status = entity.Status, 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) => public static OrderRefund ToModel(this Entities.OrderRefund entity) =>
@@ -22,7 +22,7 @@
<!-- Nuget Package Details --> <!-- Nuget Package Details -->
<PropertyGroup> <PropertyGroup>
<PackageId>LiteCharms.Extensions</PackageId> <PackageId>LiteCharms.Extensions</PackageId>
<Version>1.0.17</Version> <Version>1.0.20</Version>
<Authors>Khwezi Mngoma</Authors> <Authors>Khwezi Mngoma</Authors>
<Company>Lite Charms (PTY) Ltd</Company> <Company>Lite Charms (PTY) Ltd</Company>
<Description>Extension components for Lite Charms applications.</Description> <Description>Extension components for Lite Charms applications.</Description>
@@ -49,6 +49,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.7" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageReference Include="Quartz.AspNetCore" Version="3.18.1" /> <PackageReference Include="Quartz.AspNetCore" Version="3.18.1" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.18.1" />
<!-- Global Usings --> <!-- Global Usings -->
<Using Include="Microsoft.Extensions.Diagnostics.HealthChecks" /> <Using Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
+3 -3
View File
@@ -4,10 +4,10 @@ namespace LiteCharms.Extensions;
public static class Postgres public static class Postgres
{ {
public static IServiceCollection AddLeadGeneratorDatabase(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddShopDatabase(this IServiceCollection services, IConfiguration configuration)
{ {
services.AddPooledDbContextFactory<LeadGeneratorDbContext>(options => services.AddPooledDbContextFactory<ShopDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString("PostgresLeadGenerator"))); options.UseNpgsql(configuration.GetConnectionString("PostgresShop")));
return services; return services;
} }
+38 -3
View File
@@ -7,7 +7,7 @@ public static class Quartz
{ {
private const string databaseConfigName = "PostgresScheduler"; private const string databaseConfigName = "PostgresScheduler";
public static IServiceCollection AddQuartzScheduler(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); var connectionString = configuration.GetConnectionString(databaseConfigName);
@@ -16,7 +16,42 @@ public static class Quartz
services.AddQuartz(config => services.AddQuartz(config =>
{ {
config.SchedulerName = schedulerName; config.SchedulerName = schedulerName;
config.SchedulerId = schedulerId; config.SchedulerId = "AUTO";
config.UseSimpleTypeLoader();
config.UseDefaultThreadPool(options => options.MaxConcurrency = 0);
config.UseTimeZoneConverter();
config.UsePersistentStore(storage =>
{
storage.PerformSchemaValidation = false;
storage.UseSystemTextJsonSerializer();
storage.SetProperty("quartz.jobStore.clustered", "true");
storage.SetProperty("quartz.jobStore.tablePrefix", "qrtz_");
storage.UsePostgres(connectionString!);
storage.UseClustering(cluster =>
{
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2);
});
});
});
return services;
}
public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(databaseConfigName);
services.ConfigureCommon();
services.AddQuartz(config =>
{
config.SchedulerName = schedulerName;
config.SchedulerId = "AUTO";
config.InterruptJobsOnShutdown = true; config.InterruptJobsOnShutdown = true;
config.InterruptJobsOnShutdownWithWait = true; config.InterruptJobsOnShutdownWithWait = true;
config.MaxBatchSize = 5; config.MaxBatchSize = 5;
@@ -31,7 +66,7 @@ public static class Quartz
storage.UseSystemTextJsonSerializer(); storage.UseSystemTextJsonSerializer();
storage.SetProperty("quartz.jobStore.clustered", "true"); storage.SetProperty("quartz.jobStore.clustered", "true");
storage.SetProperty("quartz.jobStore.tablePrefix", "quartz_"); storage.SetProperty("quartz.jobStore.tablePrefix", "qrtz_");
storage.UsePostgres(connectionString!); storage.UsePostgres(connectionString!);
storage.UseClustering(cluster => storage.UseClustering(cluster =>
+6 -5
View File
@@ -1,26 +1,27 @@
using LiteCharms.Abstractions; using LiteCharms.Abstractions;
using LiteCharms.Infrastructure.ServiceBus; using LiteCharms.Infrastructure.ServiceBus;
using LiteCharms.Infrastructure.ServiceBus.Exchanges; using LiteCharms.Infrastructure.ServiceBus.Exchanges;
using LiteCharms.Infrastructure.ServiceBus.Queues;
namespace LiteCharms.Extensions; namespace LiteCharms.Extensions;
public static class ServiceBus public static class ServiceBus
{ {
public static IServiceCollection AddGeneralServiceBus(this IServiceCollection services) => services public static IServiceCollection AddGeneralServiceBus(this IServiceCollection services) => services
.AddSingleton<GeneralServiceBus>() .AddSingleton<GeneralQueue>()
.AddHostedService<GeneralExchange>() .AddHostedService<GeneralExchange>()
.AddKeyedTransient<IEventBus, GeneralServiceBus>(Constants.GeneralServiceBus) .AddKeyedSingleton<IEventBus, GeneralServiceBus>(Constants.GeneralServiceBus)
.AddMemoryCache(); .AddMemoryCache();
public static IServiceCollection AddEmailServiceBus(this IServiceCollection services) => services public static IServiceCollection AddEmailServiceBus(this IServiceCollection services) => services
.AddSingleton<EmailServiceBus>() .AddSingleton<EmailQueue>()
.AddHostedService<EmailExchange>() .AddHostedService<EmailExchange>()
.AddKeyedTransient<IEventBus, EmailServiceBus>(Constants.EmailServiceBus) .AddKeyedTransient<IEventBus, EmailServiceBus>(Constants.EmailServiceBus)
.AddMemoryCache(); .AddMemoryCache();
public static IServiceCollection AddSalesServiceBus(this IServiceCollection services) => services public static IServiceCollection AddSalesServiceBus(this IServiceCollection services) => services
.AddSingleton<SalesServiceBus>() .AddSingleton<SalesQueue>()
.AddHostedService<SalesExchange>() .AddHostedService<SalesExchange>()
.AddKeyedTransient<IEventBus, SalesServiceBus>(Constants.SalesServiceBus) .AddKeyedSingleton<IEventBus, SalesServiceBus>(Constants.SalesServiceBus)
.AddMemoryCache(); .AddMemoryCache();
} }
@@ -0,0 +1,25 @@
namespace LiteCharms.Features.CartPackages.Commands;
public class AddPackageItemCommand : IRequest<Result<Guid>>
{
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);
}
}
@@ -0,0 +1,23 @@
namespace LiteCharms.Features.CartPackages.Commands;
public class CreatePackageCommand : IRequest<Result<Guid>>
{
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);
}
}
@@ -0,0 +1,25 @@
namespace LiteCharms.Features.CartPackages.Commands;
public class DeletePackageItemCommand : IRequest<Result>
{
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);
}
}
@@ -0,0 +1,16 @@
namespace LiteCharms.Features.CartPackages.Commands;
public class DeletePackageItemsCommand : IRequest<Result>
{
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);
}
}
@@ -0,0 +1,38 @@
using LiteCharms.Infrastructure.Database;
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
public class AddPackageItemCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<AddPackageItemCommand, Result<Guid>>
{
public async ValueTask<Result<Guid>> 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<Guid>($"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<Guid>($"Failed to add new package item by ID {request.ProductPriceId}");
}
catch (Exception ex)
{
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -0,0 +1,32 @@
using LiteCharms.Infrastructure.Database;
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
public class CreatePackageCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<CreatePackageCommand, Result<Guid>>
{
public async ValueTask<Result<Guid>> 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<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -0,0 +1,32 @@
using LiteCharms.Infrastructure.Database;
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
public class DeletePackageItemCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<DeletePackageItemCommand, Result>
{
public async ValueTask<Result> 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));
}
}
}
@@ -0,0 +1,29 @@
using LiteCharms.Infrastructure.Database;
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
public class DeletePackageItemsCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<DeletePackageItemsCommand, Result>
{
public async ValueTask<Result> 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));
}
}
}
@@ -0,0 +1,33 @@
using LiteCharms.Infrastructure.Database;
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
public class UpdatePackageCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdatePackageCommand, Result>
{
public async ValueTask<Result> 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));
}
}
}
@@ -0,0 +1,29 @@
using LiteCharms.Infrastructure.Database;
namespace LiteCharms.Features.CartPackages.Commands.Handlers;
public class UpdatePackageStatusCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdatePackageStatusCommand, Result>
{
public async ValueTask<Result> 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));
}
}
}
@@ -0,0 +1,28 @@
namespace LiteCharms.Features.CartPackages.Commands;
public class UpdatePackageCommand : IRequest<Result>
{
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);
}
}
@@ -0,0 +1,22 @@
namespace LiteCharms.Features.CartPackages.Commands;
public class UpdatePackageStatusCommand : IRequest<Result>
{
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);
}
}
@@ -0,0 +1,18 @@
using LiteCharms.Models;
namespace LiteCharms.Features.CartPackages.Queries;
public class GetPackageItemsQuery : IRequest<Result<PackageItem[]>>
{
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);
}
}
@@ -0,0 +1,18 @@
using LiteCharms.Models;
namespace LiteCharms.Features.CartPackages.Queries;
public class GetPackageQuery : IRequest<Result<Package>>
{
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);
}
}
@@ -0,0 +1,33 @@
using LiteCharms.Models;
namespace LiteCharms.Features.CartPackages.Queries;
public class GetPackagesQuery : IRequest<Result<Package[]>>
{
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);
}
}
@@ -0,0 +1,32 @@
using LiteCharms.Extensions;
using LiteCharms.Infrastructure.Database;
using LiteCharms.Models;
namespace LiteCharms.Features.CartPackages.Queries.Handlers;
public class GetPackageItemsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetPackageItemsQuery, Result<PackageItem[]>>
{
public async ValueTask<Result<PackageItem[]>> 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<PackageItem[]>($"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<PackageItem[]>($"Could not find package items by package ID {request.PackageId}");
}
catch (Exception ex)
{
return Result.Fail<PackageItem[]>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -0,0 +1,27 @@
using LiteCharms.Extensions;
using LiteCharms.Infrastructure.Database;
using LiteCharms.Models;
namespace LiteCharms.Features.CartPackages.Queries.Handlers;
public class GetPackageQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetPackageQuery, Result<Package>>
{
public async ValueTask<Result<Package>> 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<Package>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -0,0 +1,35 @@
using LiteCharms.Extensions;
using LiteCharms.Infrastructure.Database;
using LiteCharms.Models;
namespace LiteCharms.Features.CartPackages.Queries.Handlers;
public class GetPackagesQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetPackagesQuery, Result<Package[]>>
{
public async ValueTask<Result<Package[]>> 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<Package[]>(new Error($"No packages found for the specified date range {request.From} - {request.To}."));
}
catch (Exception ex)
{
return Result.Fail<Package[]>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Customers.Commands.Handlers; namespace LiteCharms.Features.Customers.Commands.Handlers;
public class CreateCustomerCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<CreateCustomerCommand, Result<Guid>> public class CreateCustomerCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<CreateCustomerCommand, Result<Guid>>
{ {
public async ValueTask<Result<Guid>> Handle(CreateCustomerCommand request, CancellationToken cancellationToken) public async ValueTask<Result<Guid>> Handle(CreateCustomerCommand request, CancellationToken cancellationToken)
{ {
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Customers.Commands.Handlers; namespace LiteCharms.Features.Customers.Commands.Handlers;
public class UpdateCustomerCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<UpdateCustomerCommand, Result> public class UpdateCustomerCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdateCustomerCommand, Result>
{ {
public async ValueTask<Result> Handle(UpdateCustomerCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(UpdateCustomerCommand request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Customers.Queries.Handlers; namespace LiteCharms.Features.Customers.Queries.Handlers;
public class GetCustomerQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetCustomerQuery, Result<Customer>> public class GetCustomerQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetCustomerQuery, Result<Customer>>
{ {
public async ValueTask<Result<Customer>> Handle(GetCustomerQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Customer>> Handle(GetCustomerQuery request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Customers.Queries.Handlers; namespace LiteCharms.Features.Customers.Queries.Handlers;
public class GetCustomersQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetCustomersQuery, Result<Customer[]>> public class GetCustomersQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetCustomersQuery, Result<Customer[]>>
{ {
public async ValueTask<Result<Customer[]>> Handle(GetCustomersQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Customer[]>> Handle(GetCustomersQuery request, CancellationToken cancellationToken)
{ {
@@ -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<SmtpSettings> smtpOptions) : IRequestHandler<SendEmailCommand, Result> public class SendEmailCommandHandler(IOptions<SmtpSettings> smtpOptions) : IRequestHandler<SendEmailCommand, Result>
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Utilities.Commands; namespace LiteCharms.Features.Email.Commands;
public class SendEmailCommand : IRequest<Result> public class SendEmailCommand : IRequest<Result>
{ {
@@ -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<SendShopEmailEnquiryEvent>
{
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);
}
}
@@ -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);
}
}
@@ -1,9 +1,9 @@
using LiteCharms.Features.Utilities.Commands; using LiteCharms.Features.Utilities.Hash.Commands;
using LiteCharms.Infrastructure.Database; using LiteCharms.Infrastructure.Database;
namespace LiteCharms.Features.Leads.Commands.Handlers; namespace LiteCharms.Features.Leads.Commands.Handlers;
public class CreateLeadCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory, ISender mediator) : IRequestHandler<CreateLeadCommand, Result<Guid>> public class CreateLeadCommandHandler(IDbContextFactory<ShopDbContext> contextFactory, ISender mediator) : IRequestHandler<CreateLeadCommand, Result<Guid>>
{ {
public async ValueTask<Result<Guid>> Handle(CreateLeadCommand request, CancellationToken cancellationToken) public async ValueTask<Result<Guid>> Handle(CreateLeadCommand request, CancellationToken cancellationToken)
{ {
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Leads.Commands.Handlers; namespace LiteCharms.Features.Leads.Commands.Handlers;
public class UpdateLeadCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<UpdateLeadCommand, Result> public class UpdateLeadCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdateLeadCommand, Result>
{ {
public async ValueTask<Result> Handle(UpdateLeadCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(UpdateLeadCommand request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Leads.Queries.Handlers; namespace LiteCharms.Features.Leads.Queries.Handlers;
public class GetCustomerLeadsQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetCustomerLeadsQuery, Result<Lead[]>> public class GetCustomerLeadsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetCustomerLeadsQuery, Result<Lead[]>>
{ {
public async ValueTask<Result<Lead[]>> Handle(GetCustomerLeadsQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Lead[]>> Handle(GetCustomerLeadsQuery request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Leads.Queries.Handlers; namespace LiteCharms.Features.Leads.Queries.Handlers;
public class GetLeadsQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetLeadsQuery, Result<Lead[]>> public class GetLeadsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetLeadsQuery, Result<Lead[]>>
{ {
public async ValueTask<Result<Lead[]>> Handle(GetLeadsQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Lead[]>> Handle(GetLeadsQuery request, CancellationToken cancellationToken)
{ {
@@ -11,7 +11,7 @@
<!-- Nuget Package Details --> <!-- Nuget Package Details -->
<PropertyGroup> <PropertyGroup>
<PackageId>LiteCharms.Features</PackageId> <PackageId>LiteCharms.Features</PackageId>
<Version>1.0.17</Version> <Version>1.0.20</Version>
<Authors>Khwezi Mngoma</Authors> <Authors>Khwezi Mngoma</Authors>
<Company>Lite Charms (PTY) Ltd</Company> <Company>Lite Charms (PTY) Ltd</Company>
<Description>Feature components for Lite Charms applications.</Description> <Description>Feature components for Lite Charms applications.</Description>
@@ -54,6 +54,7 @@
<Using Include="System.Security.Cryptography" /> <Using Include="System.Security.Cryptography" />
<Using Include="Microsoft.EntityFrameworkCore" /> <Using Include="Microsoft.EntityFrameworkCore" />
<Using Include="Microsoft.Extensions.Options" /> <Using Include="Microsoft.Extensions.Options" />
<Using Include="Microsoft.Extensions.Logging" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -6,58 +6,67 @@ public class CreateNotificationCommand : IRequest<Result<Guid>>
{ {
public NotificationDirection Direction { get; set; } 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? Platform { get; set; } public string? Message { get; set; }
public string? PlatformAddress { 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 string? CorrelationId { get; set; }
public string? CorrelationIdType { get; set; } public CorrelationIdTypes CorrelationIdType { get; set; }
public bool IsInternal { get; set; } 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) 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)
{ {
Direction = direction; Direction = direction;
Author = author; Sender = sender;
Title = title; SenderAddress = senderAddress;
Description = description; Subject = subject;
Message = message;
Platform = platform; Platform = platform;
PlatformAddress = platformAddress; Priority = priority;
Recipient = recipient;
RecipientAddress = recipientAddress;
CorrelationId = correlationId; CorrelationId = correlationId;
CorrelationIdType = correlationIdType; CorrelationIdType = correlationIdType;
IsInternal = isInternal; IsInternal = isInternal;
IsHtml = isHtml;
} }
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 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(author)) if (string.IsNullOrWhiteSpace(sender))
throw new ArgumentException("Author cannot be null or whitespace.", nameof(author)); throw new ArgumentException("Sender name is required.", nameof(sender));
if (string.IsNullOrWhiteSpace(title)) if (string.IsNullOrWhiteSpace(subject))
throw new ArgumentException("Title cannot be null or whitespace.", nameof(title)); throw new ArgumentException("Subject is required.", nameof(subject));
if (string.IsNullOrWhiteSpace(description)) if (string.IsNullOrWhiteSpace(message))
throw new ArgumentException("Description cannot be null or whitespace.", nameof(description)); throw new ArgumentException("Message is required.", nameof(message));
if (string.IsNullOrWhiteSpace(platform)) if (string.IsNullOrWhiteSpace(recipient))
throw new ArgumentException("Platform cannot be null or whitespace.", nameof(platform)); throw new ArgumentException("Recipient name is required.", nameof(recipient));
if (string.IsNullOrWhiteSpace(platformAddress)) if (string.IsNullOrWhiteSpace(recipientAddress))
throw new ArgumentException("PlatformAddress cannot be null or whitespace.", nameof(platformAddress)); throw new ArgumentException("Recipient address is required.", nameof(recipientAddress));
if (string.IsNullOrWhiteSpace(correlationId)) 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)) return new(direction, sender, senderAddress, subject, message, platform, priority, recipient, recipientAddress, correlationId, correlationIdType, isInternal, isHtml);
throw new ArgumentException("CorrelationIdType cannot be null or whitespace.", nameof(correlationIdType));
return new(direction, author, title, description, platform, platformAddress, correlationId, correlationIdType, isInternal);
} }
} }
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Notifications.Commands.Handlers; namespace LiteCharms.Features.Notifications.Commands.Handlers;
public class CreateNotificationCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<CreateNotificationCommand, Result<Guid>> public class CreateNotificationCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<CreateNotificationCommand, Result<Guid>>
{ {
public async ValueTask<Result<Guid>> Handle(CreateNotificationCommand request, CancellationToken cancellationToken) public async ValueTask<Result<Guid>> Handle(CreateNotificationCommand request, CancellationToken cancellationToken)
{ {
@@ -13,14 +13,19 @@ public class CreateNotificationCommandHandler(IDbContextFactory<LeadGeneratorDbC
var newNotification = context.Notifications.Add(new Entities.Notification var newNotification = context.Notifications.Add(new Entities.Notification
{ {
Direction = request.Direction, Direction = request.Direction,
Author = request.Author, SenderName = request.Sender,
Title = request.Title, Sender = request.SenderAddress,
Description = request.Description, Recipient = request.Recipient,
RecipientAddress = request.RecipientAddress,
Subject = request.Subject,
Message = request.Message,
Platform = request.Platform, Platform = request.Platform,
PlatformAddress = request.PlatformAddress, Priority = request.Priority,
CorrelationId = request.CorrelationId, CorrelationId = request.CorrelationId,
CorrelationIdType = request.CorrelationIdType, CorrelationIdType = request.CorrelationIdType,
IsInternal = request.IsInternal, IsInternal = request.IsInternal,
IsHtml = request.IsHtml,
Processed = false
}); });
return newNotification is not null return newNotification is not null
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Notifications.Commands.Handlers; namespace LiteCharms.Features.Notifications.Commands.Handlers;
public class UpdateNotificationCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<UpdateNotificationCommand, Result> public class UpdateNotificationCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdateNotificationCommand, Result>
{ {
public async ValueTask<Result> Handle(UpdateNotificationCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(UpdateNotificationCommand request, CancellationToken cancellationToken)
{ {
@@ -17,6 +17,12 @@ public class UpdateNotificationCommandHandler(IDbContextFactory<LeadGeneratorDbC
notification.Processed = request.Processed; notification.Processed = request.Processed;
if (request.HasError)
{
notification.HasError = request.HasError;
notification.Errors = request.Errors;
}
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail(new Error($"Failed to update notification with id {request.NotificationId}.")); : Result.Fail(new Error($"Failed to update notification with id {request.NotificationId}."));
@@ -6,13 +6,19 @@ public class UpdateNotificationCommand : IRequest<Result>
public bool Processed { get; set; } 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; NotificationId = notificationId;
Processed = processed; 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) if(notificationId == Guid.Empty)
throw new ArgumentException("Notification ID cannot be empty.", nameof(notificationId)); throw new ArgumentException("Notification ID cannot be empty.", nameof(notificationId));
@@ -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<ShopDbContext> contextFactory, ILogger<ProcessEmailNotificationsEvent> logger, ISender mediator) :
INotificationHandler<ProcessEmailNotificationsEvent>
{
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<string>(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<Result> 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));
}
}
}
@@ -0,0 +1,14 @@
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; }
private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords;
public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords);
}
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Notifications.Queries.Handlers; namespace LiteCharms.Features.Notifications.Queries.Handlers;
public class GetNotificationQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetNotificationQuery, Result<Notification>> public class GetNotificationQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetNotificationQuery, Result<Notification>>
{ {
public async ValueTask<Result<Notification>> Handle(GetNotificationQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Notification>> Handle(GetNotificationQuery request, CancellationToken cancellationToken)
{ {
@@ -12,7 +12,7 @@ public class GetNotificationQueryHandler(IDbContextFactory<LeadGeneratorDbContex
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); 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 return notification is not null
? Result.Ok(notification.ToModel()) ? Result.Ok(notification.ToModel())
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Notifications.Queries.Handlers; namespace LiteCharms.Features.Notifications.Queries.Handlers;
public class GetNotificationsQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetNotificationsQuery, Result<Notification[]>> public class GetNotificationsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetNotificationsQuery, Result<Notification[]>>
{ {
public async ValueTask<Result<Notification[]>> Handle(GetNotificationsQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Notification[]>> Handle(GetNotificationsQuery request, CancellationToken cancellationToken)
{ {
@@ -8,14 +8,26 @@ public class CreateOrderCommand : IRequest<Result<Guid>>
public Guid? QuoteId { get; set; } 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; CustomerId = customerId;
ShoppingCartId = shoppingCartId; ShoppingCartId = shoppingCartId;
DepositRequired = depositRequired;
QuoteId = quoteId; 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) if (customerId == Guid.Empty)
throw new ArgumentException("CustomerId is required.", nameof(customerId)); throw new ArgumentException("CustomerId is required.", nameof(customerId));
@@ -23,6 +35,6 @@ public class CreateOrderCommand : IRequest<Result<Guid>>
if (shoppingCartId == Guid.Empty) if (shoppingCartId == Guid.Empty)
throw new ArgumentException("ShoppingCartId is required.", nameof(shoppingCartId)); throw new ArgumentException("ShoppingCartId is required.", nameof(shoppingCartId));
return new(customerId, shoppingCartId, quoteId); return new(customerId, shoppingCartId, depositRequired, quoteId, requirements, notes, terms);
} }
} }
@@ -1,8 +1,9 @@
using LiteCharms.Infrastructure.Database; using LiteCharms.Infrastructure.Database;
using LiteCharms.Models;
namespace LiteCharms.Features.Orders.Commands.Handlers; namespace LiteCharms.Features.Orders.Commands.Handlers;
public class CreateOrderCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<CreateOrderCommand, Result<Guid>> public class CreateOrderCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<CreateOrderCommand, Result<Guid>>
{ {
public async ValueTask<Result<Guid>> Handle(CreateOrderCommand request, CancellationToken cancellationToken) public async ValueTask<Result<Guid>> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{ {
@@ -21,10 +22,15 @@ public class CreateOrderCommandHandler(IDbContextFactory<LeadGeneratorDbContext>
var newOrder = context.Orders.Add(new Entities.Order var newOrder = context.Orders.Add(new Entities.Order
{ {
CreatedAt = DateTime.UtcNow,
Status = OrderStatus.Pending,
CustomerId = request.CustomerId, CustomerId = request.CustomerId,
ShoppingCartId = request.ShoppingCartId,
QuoteId = request.QuoteId, 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 return await context.SaveChangesAsync(cancellationToken) > 0
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Orders.Commands.Handlers; namespace LiteCharms.Features.Orders.Commands.Handlers;
public class UpdateOrderStatusCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<UpdateOrderStatusCommand, Result> public class UpdateOrderStatusCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdateOrderStatusCommand, Result>
{ {
public async ValueTask<Result> Handle(UpdateOrderStatusCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(UpdateOrderStatusCommand request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Orders.Queries.Handlers; namespace LiteCharms.Features.Orders.Queries.Handlers;
public class GetCustomerOrdersQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetCustomerOrdersQuery, Result<Order[]>> public class GetCustomerOrdersQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetCustomerOrdersQuery, Result<Order[]>>
{ {
public async ValueTask<Result<Order[]>> Handle(GetCustomerOrdersQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Order[]>> Handle(GetCustomerOrdersQuery request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Orders.Queries.Handlers; namespace LiteCharms.Features.Orders.Queries.Handlers;
public class GetOrderRefundQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetOrderRefundQuery, Result<OrderRefund>> public class GetOrderRefundQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetOrderRefundQuery, Result<OrderRefund>>
{ {
public async ValueTask<Result<OrderRefund>> Handle(GetOrderRefundQuery request, CancellationToken cancellationToken) public async ValueTask<Result<OrderRefund>> Handle(GetOrderRefundQuery request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Orders.Queries.Handlers; namespace LiteCharms.Features.Orders.Queries.Handlers;
public class GetOrdersQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetOrdersQuery, Result<Order[]>> public class GetOrdersQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetOrdersQuery, Result<Order[]>>
{ {
public async ValueTask<Result<Order[]>> Handle(GetOrdersQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Order[]>> Handle(GetOrdersQuery request, CancellationToken cancellationToken)
{ {
@@ -16,6 +16,7 @@ public class GetOrdersQueryHandler(IDbContextFactory<LeadGeneratorDbContext> con
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var orders = await context.Orders var orders = await context.Orders
.AsNoTracking()
.OrderByDescending(o => o.CreatedAt) .OrderByDescending(o => o.CreatedAt)
.Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate)
.Take(request.MaxRecords) .Take(request.MaxRecords)
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Products.Queries.Handlers; namespace LiteCharms.Features.Products.Queries.Handlers;
public class GetProductPriceQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetProductPriceQuery, Result<ProductPrice>> public class GetProductPriceQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetProductPriceQuery, Result<ProductPrice>>
{ {
public async ValueTask<Result<ProductPrice>> Handle(GetProductPriceQuery request, CancellationToken cancellationToken) public async ValueTask<Result<ProductPrice>> Handle(GetProductPriceQuery request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Products.Queries.Handlers; namespace LiteCharms.Features.Products.Queries.Handlers;
public class GetProductPricesQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetProductPricesQuery, Result<ProductPrice[]>> public class GetProductPricesQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetProductPricesQuery, Result<ProductPrice[]>>
{ {
public async ValueTask<Result<ProductPrice[]>> Handle(GetProductPricesQuery request, CancellationToken cancellationToken) public async ValueTask<Result<ProductPrice[]>> Handle(GetProductPricesQuery request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Products.Queries.Handlers; namespace LiteCharms.Features.Products.Queries.Handlers;
public class GetProductQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetProductQuery, Result<Product>> public class GetProductQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetProductQuery, Result<Product>>
{ {
public async ValueTask<Result<Product>> Handle(GetProductQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Product>> Handle(GetProductQuery request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Products.Queries.Handlers; namespace LiteCharms.Features.Products.Queries.Handlers;
public class GetProductsQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetProductsQuery, Result<Product[]>> public class GetProductsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetProductsQuery, Result<Product[]>>
{ {
public async ValueTask<Result<Product[]>> Handle(GetProductsQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Product[]>> Handle(GetProductsQuery request, CancellationToken cancellationToken)
{ {
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Quotes.Commands.Handlers; namespace LiteCharms.Features.Quotes.Commands.Handlers;
public class AssignQuoteToOrderCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<AssignQuoteToOrderCommand, Result> public class AssignQuoteToOrderCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<AssignQuoteToOrderCommand, Result>
{ {
public async ValueTask<Result> Handle(AssignQuoteToOrderCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(AssignQuoteToOrderCommand request, CancellationToken cancellationToken)
{ {
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Quotes.Commands.Handlers; namespace LiteCharms.Features.Quotes.Commands.Handlers;
public class AssignQuoteToShoppingCartCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<AssignQuoteToShoppingCartCommand, Result> public class AssignQuoteToShoppingCartCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<AssignQuoteToShoppingCartCommand, Result>
{ {
public async ValueTask<Result> Handle(AssignQuoteToShoppingCartCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(AssignQuoteToShoppingCartCommand request, CancellationToken cancellationToken)
{ {
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Quotes.Commands.Handlers; namespace LiteCharms.Features.Quotes.Commands.Handlers;
public class UpdateQuoteStatusCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<UpdateQuoteStatusCommand, Result> public class UpdateQuoteStatusCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdateQuoteStatusCommand, Result>
{ {
public async ValueTask<Result> Handle(UpdateQuoteStatusCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(UpdateQuoteStatusCommand request, CancellationToken cancellationToken)
{ {
@@ -5,7 +5,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Quotes.Queries.Handlers; namespace LiteCharms.Features.Quotes.Queries.Handlers;
public class GetCustomerQuotesQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetCustomerQuotesQuery, Result<Quote[]>> public class GetCustomerQuotesQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetCustomerQuotesQuery, Result<Quote[]>>
{ {
public async ValueTask<Result<Quote[]>> Handle(GetCustomerQuotesQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Quote[]>> Handle(GetCustomerQuotesQuery request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Quotes.Queries.Handlers; namespace LiteCharms.Features.Quotes.Queries.Handlers;
public class GetQuoteQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetQuoteQuery, Result<Quote>> public class GetQuoteQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetQuoteQuery, Result<Quote>>
{ {
public async ValueTask<Result<Quote>> Handle(GetQuoteQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Quote>> Handle(GetQuoteQuery request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Quotes.Queries.Handlers; namespace LiteCharms.Features.Quotes.Queries.Handlers;
public class GetQuotesHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetQuotesQuery, Result<Quote[]>> public class GetQuotesHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetQuotesQuery, Result<Quote[]>>
{ {
public async ValueTask<Result<Quote[]>> Handle(GetQuotesQuery request, CancellationToken cancellationToken) public async ValueTask<Result<Quote[]>> Handle(GetQuotesQuery request, CancellationToken cancellationToken)
{ {
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Refunds.Commands.Handlers; namespace LiteCharms.Features.Refunds.Commands.Handlers;
public class RefundCustomerCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<RefundCustomerCommand, Result<Guid>> public class RefundCustomerCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<RefundCustomerCommand, Result<Guid>>
{ {
public async ValueTask<Result<Guid>> Handle(RefundCustomerCommand request, CancellationToken cancellationToken) public async ValueTask<Result<Guid>> Handle(RefundCustomerCommand request, CancellationToken cancellationToken)
{ {
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Refunds.Commands.Handlers; namespace LiteCharms.Features.Refunds.Commands.Handlers;
public class UpdateOrderRefundCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<UpdateOrderRefundCommand, Result> public class UpdateOrderRefundCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdateOrderRefundCommand, Result>
{ {
public async ValueTask<Result> Handle(UpdateOrderRefundCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(UpdateOrderRefundCommand request, CancellationToken cancellationToken)
{ {
@@ -5,7 +5,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Refunds.Queries.Handlers; namespace LiteCharms.Features.Refunds.Queries.Handlers;
public class GetCustomerRefundsQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetCustomerRefundsQuery, Result<OrderRefund[]>> public class GetCustomerRefundsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetCustomerRefundsQuery, Result<OrderRefund[]>>
{ {
public async ValueTask<Result<OrderRefund[]>> Handle(GetCustomerRefundsQuery request, CancellationToken cancellationToken) public async ValueTask<Result<OrderRefund[]>> Handle(GetCustomerRefundsQuery request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.Refunds.Queries.Handlers; namespace LiteCharms.Features.Refunds.Queries.Handlers;
public class GetRefundQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetRefundQuery, Result<OrderRefund>> public class GetRefundQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetRefundQuery, Result<OrderRefund>>
{ {
public async ValueTask<Result<OrderRefund>> Handle(GetRefundQuery request, CancellationToken cancellationToken) public async ValueTask<Result<OrderRefund>> Handle(GetRefundQuery request, CancellationToken cancellationToken)
{ {
@@ -0,0 +1,25 @@
namespace LiteCharms.Features.ShoppingCarts.Commands;
public class AddPackageToShoppingCartCommand : IRequest<Result>
{
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);
}
}
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers;
public class AddItemToShoppingCartCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<AddItemToShoppingCartCommand, Result> public class AddItemToShoppingCartCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<AddItemToShoppingCartCommand, Result>
{ {
public async ValueTask<Result> Handle(AddItemToShoppingCartCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(AddItemToShoppingCartCommand request, CancellationToken cancellationToken)
{ {
@@ -0,0 +1,39 @@
using LiteCharms.Infrastructure.Database;
namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers;
public class AddPackageToShoppingCartCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<AddPackageToShoppingCartCommand, Result>
{
public async ValueTask<Result> 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));
}
}
}
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers;
public class CreateShoppingCartCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<CreateShoppingCartCommand, Result<Guid>> public class CreateShoppingCartCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<CreateShoppingCartCommand, Result<Guid>>
{ {
public async ValueTask<Result<Guid>> Handle(CreateShoppingCartCommand request, CancellationToken cancellationToken) public async ValueTask<Result<Guid>> Handle(CreateShoppingCartCommand request, CancellationToken cancellationToken)
{ {
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers;
public class EmptyShoppingCartCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<EmptyShoppingCartCommand, Result> public class EmptyShoppingCartCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<EmptyShoppingCartCommand, Result>
{ {
public async ValueTask<Result> Handle(EmptyShoppingCartCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(EmptyShoppingCartCommand request, CancellationToken cancellationToken)
{ {
@@ -0,0 +1,35 @@
using LiteCharms.Infrastructure.Database;
namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers;
public class RemovePackageFromShoppingCartCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<RemovePackageFromShoppingCartCommand, Result>
{
public async ValueTask<Result> 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));
}
}
}
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers;
public class RemoveShoppingCartItemCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<RemoveShoppingCartItemCommand, Result> public class RemoveShoppingCartItemCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<RemoveShoppingCartItemCommand, Result>
{ {
public async ValueTask<Result> Handle(RemoveShoppingCartItemCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(RemoveShoppingCartItemCommand request, CancellationToken cancellationToken)
{ {
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers;
public class UpdateShoppingCartItemCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<UpdateShoppingCartItemCommand, Result> public class UpdateShoppingCartItemCommandHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<UpdateShoppingCartItemCommand, Result>
{ {
public async ValueTask<Result> Handle(UpdateShoppingCartItemCommand request, CancellationToken cancellationToken) public async ValueTask<Result> Handle(UpdateShoppingCartItemCommand request, CancellationToken cancellationToken)
{ {
@@ -0,0 +1,25 @@
namespace LiteCharms.Features.ShoppingCarts.Commands;
public class RemovePackageFromShoppingCartCommand : IRequest<Result>
{
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);
}
}
@@ -0,0 +1,18 @@
using LiteCharms.Models;
namespace LiteCharms.Features.ShoppingCarts.Queries;
public class GetShoppingCartPackagesQuery : IRequest<Result<ShoppingCartPackage[]>>
{
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);
}
}
@@ -5,7 +5,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers;
public class GetCustomerShoppingCartsQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetCustomerShoppingCartsQuery, Result<ShoppingCart[]>> public class GetCustomerShoppingCartsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetCustomerShoppingCartsQuery, Result<ShoppingCart[]>>
{ {
public async ValueTask<Result<ShoppingCart[]>> Handle(GetCustomerShoppingCartsQuery request, CancellationToken cancellationToken) public async ValueTask<Result<ShoppingCart[]>> Handle(GetCustomerShoppingCartsQuery request, CancellationToken cancellationToken)
{ {
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers;
public class GetShoppingCartItemsQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetShoppingCartItemsQuery, Result<ShoppingCartItem[]>> public class GetShoppingCartItemsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetShoppingCartItemsQuery, Result<ShoppingCartItem[]>>
{ {
public async ValueTask<Result<ShoppingCartItem[]>> Handle(GetShoppingCartItemsQuery request, CancellationToken cancellationToken) public async ValueTask<Result<ShoppingCartItem[]>> Handle(GetShoppingCartItemsQuery request, CancellationToken cancellationToken)
{ {
@@ -0,0 +1,32 @@
using LiteCharms.Extensions;
using LiteCharms.Infrastructure.Database;
using LiteCharms.Models;
namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers;
public class GetShoppingCartPackagesQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetShoppingCartPackagesQuery, Result<ShoppingCartPackage[]>>
{
public async ValueTask<Result<ShoppingCartPackage[]>> 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<ShoppingCartPackage[]>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -4,7 +4,7 @@ using LiteCharms.Models;
namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers;
public class GetShoppingCartQueryHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<GetShoppingCartQuery, Result<ShoppingCart>> public class GetShoppingCartQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetShoppingCartQuery, Result<ShoppingCart>>
{ {
public async ValueTask<Result<ShoppingCart>> Handle(GetShoppingCartQuery request, CancellationToken cancellationToken) public async ValueTask<Result<ShoppingCart>> Handle(GetShoppingCartQuery request, CancellationToken cancellationToken)
{ {
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Utilities.Commands; namespace LiteCharms.Features.Utilities.Hash.Commands;
public class ComputeHashCommand : IRequest<Result<string>> public class ComputeHashCommand : IRequest<Result<string>>
{ {
@@ -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<ComputeHashCommand, Result<string>> public class ComputeHashCommandHandler : IRequestHandler<ComputeHashCommand, Result<string>>
{ {
@@ -1,19 +0,0 @@
namespace LiteCharms.Infrastructure.Database;
public class LeadGeneratorDbContextFactory : IDesignTimeDbContextFactory<LeadGeneratorDbContext>
{
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<LeadGeneratorDbContext>();
optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgresLeadGenerator"));
return new LeadGeneratorDbContext(optionsBuilder.Options);
}
}
@@ -1,272 +0,0 @@
// <auto-generated />
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(LeadGeneratorDbContext))]
[Migration("20260502231708_Init")]
partial class Init
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTimeOffset?>("UpdatedAt")
.ValueGeneratedOnUpdate()
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customer", (string)null);
});
modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<long?>("AdGroupId")
.HasColumnType("bigint");
b.Property<long?>("AdName")
.HasColumnType("bigint");
b.Property<string>("AppClickId")
.HasColumnType("text");
b.Property<string>("AttribusionHash")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("CampaignId")
.HasColumnType("bigint");
b.Property<string>("ClickLocation")
.HasColumnType("text");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone");
b.Property<long?>("FeedItemId")
.HasColumnType("bigint");
b.Property<string>("GoogleClickId")
.HasColumnType("text");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<DateTimeOffset?>("UpdatedAt")
.ValueGeneratedOnUpdate()
.HasColumnType("timestamp with time zone");
b.Property<string>("WebClickId")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Lead", (string)null);
});
modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone");
b.Property<Guid>("LeadId")
.HasColumnType("uuid");
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTimeOffset?>("UpdatedAt")
.ValueGeneratedOnUpdate()
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Order", (string)null);
});
modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone");
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("OrderRefund", (string)null);
});
modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Product", (string)null);
});
modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Discount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("UpdatedAt")
.ValueGeneratedOnUpdate()
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("ProductPrice", (string)null);
});
#pragma warning restore 612, 618
}
}
}

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