diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..12c4fd6 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,55 @@ +--- +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: + - dotnet pack LiteCharms.Features/LiteCharms.Features.csproj -c Release -p:PackageVersion=$VERSION -o dist/ + - dotnet nuget push dist/LiteCharms.Features.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + - dotnet pack LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj -c Release -p:PackageVersion=$VERSION -o dist/ + - dotnet nuget push dist/LiteCharms.Features.TechShop.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + - dotnet pack LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj -c Release -p:PackageVersion=$VERSION -o dist/ + - dotnet nuget push dist/LiteCharms.Features.MidrandBooks.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL + + - name: gitea-tag-release + image: alpine/git + 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: + - echo "169.255.58.144 gitea.khongisa.co.za" >> /etc/hosts + - apk add --no-cache curl + - git remote set-url origin https://$${GITEA_USER}:$${GITEA_PASS}@gitea.khongisa.co.za/litecharms/components.git + - git tag $VERSION + - 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.Features\n* LiteCharms.Features.TechShop\n* LiteCharms.Features.MidrandBooks\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\", + \"draft\": false, + \"prerelease\": false + }" + +trigger: + event: + - pull_request \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6ab8a85 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,287 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] +# IDE0250: Prefer make struct 'readonly' +dotnet_diagnostic.IDE0250.severity = warning + +# IDE0060: Remove unused parameters (Good cleanup pairing) +dotnet_diagnostic.IDE0060.severity = warning + +# CA1852: Seal internal types (Available in modern .NET) +dotnet_diagnostic.CA1852.severity = warning + +# MA0018: Add sealed modifier to types that are never inherited +dotnet_diagnostic.MA0018.severity = warning + +# Enforce that classes should be sealed +dotnet_diagnostic.MA0053.severity = warning + +# CRITICAL: Force the analyzer to also flag PUBLIC classes, not just internal ones +meziantou_analyzer.MA0053.public_class_should_be_sealed = true +MA0053.public_class_should_be_sealed = true + +# Keep the rule active as a warning by default +dotnet_diagnostic.MA0048.severity = warning + +# Specific exclusions for Meziantou.Analyzer MA0048 +# Disable the rule for enums +meziantou_analyzer.MA0048.exclude_enums = true + +# Disable the rule for records +meziantou_analyzer.MA0048.exclude_records = true + +#EXCLUDE specific files that are meant to hold grouped enums/records +dotnet_diagnostic.MA0048.severity = warning + +# Disable the requirement to specify ConfigureAwait(false) +dotnet_diagnostic.MA0004.severity = none + +# ALTERNATIVE: Exclude any file ending with 'Enums.cs' or 'Records.cs' +# (e.g., BillingEnums.cs, CustomerRecords.cs) +[**/*{Enums,Records}.cs] +dotnet_diagnostic.MA0048.severity = none + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Code Actions #### + +# Type members +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties + +# Symbol search +dotnet_search_reference_assemblies = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_prefer_system_hash_code = true +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_non_hidden_explicit_cast_in_source = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = true +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_prefer_system_threading_lock = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_simple_property_accessors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_implicitly_typed_lambda_expression = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_unbound_generic_type_in_nameof = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/.gitignore b/.gitignore index 9491a2f..ac5d4a4 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd +/LiteCharms.Features.Tests/http/http-client.env.json +/LiteCharms.Features.Tests/http/midrandshop-api/http-client.env.json +/LiteCharms.Features.Tests/http/authentik/http-client.env.json diff --git a/LiteCharms.Abstractions/IJobOrchestrator.cs b/LiteCharms.Abstractions/IJobOrchestrator.cs deleted file mode 100644 index d98c155..0000000 --- a/LiteCharms.Abstractions/IJobOrchestrator.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace LiteCharms.Abstractions; - -public interface IJobOrchestrator -{ - Task SendAsync(TNotification notification, CancellationToken cancellationToken = default) - where TNotification : IEvent; - - Task ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) - where TNotification : IEvent; -} diff --git a/LiteCharms.Entities/Configuration/NotificationConfiguration.cs b/LiteCharms.Entities/Configuration/NotificationConfiguration.cs deleted file mode 100644 index c5879f9..0000000 --- a/LiteCharms.Entities/Configuration/NotificationConfiguration.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class NotificationConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(Notification)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.Direction).IsRequired(); - builder.Property(f => f.Author).IsRequired(); - builder.Property(f => f.Title).IsRequired(); - builder.Property(f => f.Description).IsRequired(); - builder.Property(f => f.Platform).IsRequired(); - builder.Property(f => f.PlatformAddress).IsRequired(); - builder.Property(f => f.CorrelationId).IsRequired(); - builder.Property(f => f.CorrelationIdType).IsRequired(); - builder.Property(f => f.IsInternal).HasDefaultValue(true); - builder.Property(f => f.Processed).HasDefaultValue(false); - } -} \ No newline at end of file diff --git a/LiteCharms.Entities/Configuration/OrderConfiguration.cs b/LiteCharms.Entities/Configuration/OrderConfiguration.cs deleted file mode 100644 index 5db15cd..0000000 --- a/LiteCharms.Entities/Configuration/OrderConfiguration.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class OrderConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(Order)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); - builder.Property(f => f.CustomerId).IsRequired(); - builder.Property(f => f.QuoteId).IsRequired(false); - builder.Property(f => f.RefundId).IsRequired(false); - builder.Property(f => f.ShoppingCartId).IsRequired(); - builder.Property(f => f.Status).HasConversion().IsRequired(); - builder.Property(f => f.Notes).HasColumnType("jsonb").IsRequired(false); - - builder.HasOne(f => f.Quote) - .WithOne(f => f.Order) - .HasForeignKey(f => f.QuoteId) - .OnDelete(DeleteBehavior.Restrict); - - builder.HasOne(f => f.Customer) - .WithMany(f => f.Orders) - .HasForeignKey(f => f.CustomerId) - .OnDelete(DeleteBehavior.Restrict); - } -} diff --git a/LiteCharms.Entities/Configuration/ProductConfiguration.cs b/LiteCharms.Entities/Configuration/ProductConfiguration.cs deleted file mode 100644 index 3b5ca6d..0000000 --- a/LiteCharms.Entities/Configuration/ProductConfiguration.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class ProductConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(Product)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.Name).IsRequired(); - builder.Property(f => f.Description).IsRequired(); - builder.Property(f => f.Active).HasDefaultValue(true); - } -} diff --git a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs b/LiteCharms.Entities/Configuration/QuoteConfiguration.cs deleted file mode 100644 index 37125ae..0000000 --- a/LiteCharms.Entities/Configuration/QuoteConfiguration.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class QuoteConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(Quote)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired().ValueGeneratedOnAddOrUpdate(); - builder.Property(f => f.ExpiredAt).IsRequired(false); - builder.Property(f => f.CustomerId).IsRequired(); - builder.Property(f => f.Status).IsRequired(); - builder.Property(f => f.ShoppingCartId).IsRequired(); - builder.Property(f => f.Reason).IsRequired(false); - - builder.HasOne(f => f.Customer) - .WithMany() - .HasForeignKey(f => f.CustomerId) - .OnDelete(DeleteBehavior.Cascade); - } -} diff --git a/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs b/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs deleted file mode 100644 index 3a1926b..0000000 --- a/LiteCharms.Entities/Configuration/ShoppingCartConfiguration.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class ShoppingCartConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(ShoppingCart)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired().ValueGeneratedOnAddOrUpdate(); - builder.Property(f => f.CustomerId).IsRequired(false); - builder.Property(f => f.OrderId).IsRequired(false); - builder.Property(f => f.QuoteId).IsRequired(false); - - builder.HasOne(f => f.Customer) - .WithMany(c => c.ShoppingCarts) - .HasForeignKey(f => f.CustomerId) - .OnDelete(DeleteBehavior.NoAction); - - builder.HasOne(f => f.Order) - .WithOne(o => o.ShoppingCart) - .HasForeignKey(o => o.ShoppingCartId) - .OnDelete(DeleteBehavior.NoAction); - - builder.HasOne(f => f.Quote) - .WithOne(o => o.ShoppingCart) - .HasForeignKey(o => o.ShoppingCartId) - .OnDelete(DeleteBehavior.NoAction); - } -} diff --git a/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs b/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs deleted file mode 100644 index 00fc2ab..0000000 --- a/LiteCharms.Entities/Configuration/ShoppingCartItemConfiguration.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Entities.Configuration; - -public class ShoppingCartItemConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(nameof(ShoppingCartItem)); - - builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired().ValueGeneratedOnAddOrUpdate(); - builder.Property(f => f.Quantity).IsRequired().HasDefaultValue(1); - builder.Property(f => f.ProductPriceId).IsRequired(); - - builder.HasOne(f => f.ProductPrice) - .WithMany() - .HasForeignKey(f => f.ProductPriceId) - .OnDelete(DeleteBehavior.NoAction); - - builder.HasOne(f => f.ShoppingCart) - .WithMany(f => f.ShoppingCartItems) - .HasForeignKey(f => f.ShoppingCartId) - .OnDelete(DeleteBehavior.NoAction); - } -} diff --git a/LiteCharms.Entities/Order.cs b/LiteCharms.Entities/Order.cs deleted file mode 100644 index 5f7d5dc..0000000 --- a/LiteCharms.Entities/Order.cs +++ /dev/null @@ -1,15 +0,0 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; - -[EntityTypeConfiguration] -public class Order : Models.Order -{ - public virtual OrderRefund? Refund { get; set; } - - public virtual Customer? Customer { get; set; } - - public virtual Quote? Quote { get; set; } - - public virtual ShoppingCart? ShoppingCart { get; set; } -} diff --git a/LiteCharms.Entities/ShoppingCartItem.cs b/LiteCharms.Entities/ShoppingCartItem.cs deleted file mode 100644 index d5e5634..0000000 --- a/LiteCharms.Entities/ShoppingCartItem.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LiteCharms.Entities; - -public class ShoppingCartItem : Models.ShoppingCartItem -{ - public virtual ShoppingCart? ShoppingCart { get; set; } - - public virtual ProductPrice? ProductPrice { get; set; } -} diff --git a/LiteCharms.Extensions/Email.cs b/LiteCharms.Extensions/Email.cs deleted file mode 100644 index e081f4e..0000000 --- a/LiteCharms.Extensions/Email.cs +++ /dev/null @@ -1,13 +0,0 @@ -using LiteCharms.Models.Configuraton.Email; - -namespace LiteCharms.Extensions; - -public static class Email -{ - public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration.GetSection("Email")); - - return services; - } -} diff --git a/LiteCharms.Extensions/EntityModeMappers.cs b/LiteCharms.Extensions/EntityModeMappers.cs deleted file mode 100644 index 8d41155..0000000 --- a/LiteCharms.Extensions/EntityModeMappers.cs +++ /dev/null @@ -1,149 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Extensions; - -public static class EntityModeMappers -{ - public static ShoppingCartItem ToModel(this Entities.ShoppingCartItem entity) => - new() - { - Id = entity.Id, - CreatedAt = entity.CreatedAt, - UpdatedAt = entity.UpdatedAt, - ProductPriceId = entity.ProductPriceId, - Quantity = entity.Quantity, - ShoppingCartId = entity.ShoppingCartId - }; - - public static ShoppingCart ToModel(this Entities.ShoppingCart entity) => - new() - { - Id = entity.Id, - CreatedAt = entity.CreatedAt, - UpdatedAt = entity.UpdatedAt, - CustomerId = entity.CustomerId, - OrderId = entity.OrderId, - QuoteId = entity.QuoteId - }; - - public static Quote ToModel(this Entities.Quote entity) => - new() - { - Id = entity.Id, - CreatedAt = entity.CreatedAt, - UpdatedAt = entity.UpdatedAt, - CustomerId = entity.CustomerId, - ExpiredAt = entity.ExpiredAt, - Reason = entity.Reason, - ShoppingCartId = entity.ShoppingCartId, - Status = entity.Status - }; - - public static Notification ToModel(this Entities.Notification entity) => - new() - { - Id = entity.Id, - CreatedAt = entity.CreatedAt, - Description = entity.Description, - Direction = entity.Direction, - CorrelationId = entity.CorrelationId, - CorrelationIdType = entity.CorrelationIdType, - IsInternal = entity.IsInternal, - Author = entity.Author, - Platform = entity.Platform, - PlatformAddress = entity.PlatformAddress, - Title = entity.Title, - Processed = entity.Processed - }; - - public static Customer ToModel(this Entities.Customer entity) => - new() - { - Id = entity.Id, - CreatedAt = entity.CreatedAt, - UpdatedAt = entity.UpdatedAt, - Active = entity.Active, - Address = entity.Address, - City = entity.City, - Company = entity.Company, - Country = entity.Country, - Discord = entity.Discord, - Email = entity.Email, - LastName = entity.LastName, - LinkedIn = entity.LinkedIn, - Name = entity.Name, - Phone = entity.Phone, - PostalCode = entity.PostalCode, - Region = entity.Region, - Slack = entity.Slack, - Tax = entity.Tax, - Website = entity.Website, - Whatsapp = entity.Whatsapp - }; - - public static Lead ToModel(this Entities.Lead entity) => - new() - { - Id = entity.Id, - CreatedAt = entity.CreatedAt, - UpdatedAt = entity.UpdatedAt, - AdGroupId = entity.AdGroupId, - AdName = entity.AdName, - AppClickId = entity.AppClickId, - AttributionHash = entity.AttributionHash, - CampaignId = entity.CampaignId, - ClickLocation = entity.ClickLocation, - CustomerId = entity.CustomerId, - FeedItemId = entity.FeedItemId, - Source = entity.Source, - ClickId = entity.ClickId, - TargetId = entity.TargetId, - WebClickId = entity.WebClickId, - Status = entity.Status - }; - - public static Order ToModel(this Entities.Order entity) => - new() - { - Id = entity.Id, - CreatedAt = entity.CreatedAt, - UpdatedAt = entity.UpdatedAt, - CustomerId = entity.CustomerId, - Notes = entity.Notes, - RefundId = entity.RefundId, - QuoteId = entity.QuoteId, - Status = entity.Status, - ShoppingCartId = entity.ShoppingCartId - }; - - public static OrderRefund ToModel(this Entities.OrderRefund entity) => - new() - { - Id = entity.Id, - CreatedAt = entity.CreatedAt, - OrderId = entity.OrderId, - Reason = entity.Reason, - Amount = entity.Amount - }; - - public static Product ToModel(this Entities.Product entity) => - new() - { - Id = entity.Id, - Name = entity.Name, - Description = entity.Description, - Active = entity.Active - }; - - public static ProductPrice ToModel(this Entities.ProductPrice entity) => - new() - { - Id = entity.Id, - ProductId = entity.ProductId, - Price = entity.Price, - Active = entity.Active, - CreatedAt = entity.CreatedAt, - Discount = entity.Discount, - UpdatedAt = entity.UpdatedAt - }; -} diff --git a/LiteCharms.Extensions/HealthChecks.cs b/LiteCharms.Extensions/HealthChecks.cs deleted file mode 100644 index 01bb1ae..0000000 --- a/LiteCharms.Extensions/HealthChecks.cs +++ /dev/null @@ -1,34 +0,0 @@ -using LiteCharms.Infrastructure.HealthChecks; - -namespace LiteCharms.Extensions; - -public static class HealthChecks -{ - public static IServiceCollection AddQuartzHealtchCheck(this IServiceCollection services) - { - services.AddHealthChecks().AddCheck("Quartz"); - - return services; - } - - public static IServiceCollection AddPostgresHealtchCheck(this IServiceCollection services) - { - services.AddHealthChecks().AddCheck("PostgreSQL"); - - return services; - } - - public static IServiceCollection AddHealthChecksSupport(this IServiceCollection services, IConfiguration configuration) - { - services.AddHealthChecks() - .AddCheck("Self", () => HealthCheckResult.Healthy()); - - //services.AddHealthChecksUI(setup => - //{ - // setup.AddHealthCheckEndpoint("Lead Generator", $"{configuration["ASPNETCORE_URLS"]}/health"); - // setup.SetEvaluationTimeInSeconds(15); - //}).AddInMemoryStorage(databaseName: "healthuidb"); - - return services; - } -} diff --git a/LiteCharms.Extensions/Postgres.cs b/LiteCharms.Extensions/Postgres.cs deleted file mode 100644 index d46bdaa..0000000 --- a/LiteCharms.Extensions/Postgres.cs +++ /dev/null @@ -1,14 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Extensions; - -public static class Postgres -{ - public static IServiceCollection AddLeadGeneratorDatabase(this IServiceCollection services, IConfiguration configuration) - { - services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString("PostgresLeadGenerator"))); - - return services; - } -} diff --git a/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs new file mode 100644 index 0000000..c4a2aae --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs @@ -0,0 +1,133 @@ +using LiteCharms.Features.MidrandBooks.Categories; +using LiteCharms.Features.MidrandBooks.Products; + +namespace LiteCharms.Features.MidrandBooks.Seed; + +public sealed class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!await features.IsEnabledAsync("CategorySeederService")) return; + + logger.LogInformation("Category and Product-Tag Mapping Seeding started (15-char limit applied)"); + + // Initialize Bogus to ensure repeatable distribution matrix pathing + var faker = new Faker(); + Randomizer.Seed = new Random(101); + + // 1. Curate Broad Book Categories (IsMain = true, Max 20, Max 15 chars) + var broadMainCategories = new[] + { + "Fiction", "Non-Fiction", "Youth & Kids", "Academic", + "Biographies", "Business", "Sci-Fi & Fantasy", + "Thrillers", "Self-Help", "History", + "Spirituality", "Arts & Photo", "Technology", + "Cookbooks", "Travel & Maps", "Poetry & Drama", "Graphic Novels" + }; + + // 2. Curate Niche Subcategories/Tags (IsMain = false, Max 15 chars) + var specializedSubCategories = new[] + { + "Cyberpunk", "Space Opera", "Historical Fix", "Cozy Mystery", "True Crime", + "Agile Project", "Software Eng", "AI & ML", "Cloud Comput", + "SA History", "African Lit", "Apartheid Era", "Mandela Legacy", + "Finance", "Investments", "Startup", "Leadership", + "CBT Therapy", "Mindfulness", "Yoga & Health", + "Baking Basics", "African Food", "Vegan Recipes", + "Ancient World", "WWII History", "Geopolitics", + "Writing Guides", "Criticism", "Classic Poetry", + "Early Learning", "Teen Romance", "Survival", + "Urban Fantasy", "Dark Fantasy", "Psych Thriller", "Hard Sci-Fi", + "Data Science", "DevOps", "Cybersecurity", + "Economics", "Real Estate", "Governance", + "Essays", "Memoirs", "Art History", + "Architecture", "Photography", "Travel Writing", + "Gaming Culture", "Philosophy", "Ethics", + "DIY Home", "SA Gardening", "Parenting" + }; + + // 3. Seed Main Categories into the System + logger.LogInformation("Seeding broad main categories..."); + foreach (var mainCat in broadMainCategories) + { + if (stoppingToken.IsCancellationRequested) return; + + // Defensive truncation fallback just in case strings get modified later + string safeName = mainCat.Length > 15 ? mainCat.Substring(0, 15) : mainCat; + + var result = await categoryService.CreateCategoryAsync(safeName, isMain: true, stoppingToken); + if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase)) + { + logger.LogWarning("Notice while adding main category '{Name}': {Msg}", safeName, result.Errors[0].Message); + } + } + + // 4. Seed Subcategories into the System + logger.LogInformation("Seeding boundless specialized niche tags..."); + foreach (var subCat in specializedSubCategories) + { + if (stoppingToken.IsCancellationRequested) return; + + string safeName = subCat.Length > 15 ? subCat.Substring(0, 15) : subCat; + + var result = await categoryService.CreateCategoryAsync(safeName, isMain: false, stoppingToken); + if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase)) + { + logger.LogWarning("Notice while adding subcategory '{Name}': {Msg}", safeName, result.Errors[0].Message); + } + } + + // 5. Query back all enabled categories to extract active IDs for junction mapping + var fetchMainResult = await categoryService.GetCategoriesAsync(isMain: true, stoppingToken); + var fetchSubResult = await categoryService.GetCategoriesAsync(isMain: false, stoppingToken); + + if (fetchMainResult.IsFailed || fetchSubResult.IsFailed) + { + logger.LogError("Aborting junction seeding: Could not retrieve categories from data store."); + return; + } + + var mainCategoryIds = fetchMainResult.Value.Select(c => c.Id).ToArray(); + var subCategoryIds = fetchSubResult.Value.Select(c => c.Id).ToArray(); + + // 6. Map Categories to your Product Collection (Product IDs 0 - 21) + logger.LogInformation("Beginning Product-Category mapping assignments for Product IDs 0 through 21..."); + + for (long productId = 0; productId <= 21; productId++) + { + if (stoppingToken.IsCancellationRequested) break; + + // Every book belongs to 1 or 2 main categories + int mainCategoriesToAssign = faker.Random.Number(1, 2); + var chosenMainIds = faker.PickRandom(mainCategoryIds, mainCategoriesToAssign).Distinct(); + + foreach (var mainId in chosenMainIds) + { + var linkResult = await productService.AddProductCategoryAsync(productId, mainId, stoppingToken); + if (linkResult.IsFailed) + { + if (!linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug("Junction note for Product {PId} and Main Category {CId}: {Msg}", productId, mainId, linkResult.Errors[0].Message); + } + } + } + + // Every book gets 1 to 4 granular subgenre tags + int subCategoriesToAssign = faker.Random.Number(1, 4); + var chosenSubIds = faker.PickRandom(subCategoryIds, subCategoriesToAssign).Distinct(); + + foreach (var subId in chosenSubIds) + { + var linkResult = await productService.AddProductCategoryAsync(productId, subId, stoppingToken); + if (linkResult.IsFailed && !linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug("Junction note for Product {PId} and Sub Category {CId}: {Msg}", productId, subId, linkResult.Errors[0].Message); + } + } + } + + logger.LogInformation("Category and Product-Tag Mapping Seeding completed successfully."); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs new file mode 100644 index 0000000..7b3a904 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.MidrandBooks.Seed.Configuration; + +public sealed class CdnSettings +{ + public string? BaseCdn { get; set; } + + public string[]? BookCovers { get; set; } + + public string[]? Authors { get; set; } + + public string[]? AuthorThumbnails { get; set; } + + public string[]? BookThumbnails { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs new file mode 100644 index 0000000..0a22738 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs @@ -0,0 +1,275 @@ +using LiteCharms.Features.MidrandBooks.Customers; +using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.MidrandBooks.Orders; +using LiteCharms.Features.MidrandBooks.Orders.Models; + +namespace LiteCharms.Features.MidrandBooks.Seed; + +public sealed class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!await features.IsEnabledAsync("CustomerSeederService")) return; + + logger.LogInformation("Customer Seeding started"); + + // 1: Add shipping providers (shippingProvider IDs will be created in sequence: 1, 2, 3) + await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.FastWay, "FastWay Couriers", 39, "https://www.fastway.co.za/our-services/track-your-parcel"), stoppingToken); + await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.DHL, "DHL Couriers", 60, "https://www.dhl.com/za-en/home/tracking.html"), stoppingToken); + await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.PostNet, "Postnet Overnight Mail", 45, "https://www.postnet.co.za/tracker"), stoppingToken); + + // Initialize Bogus Faker engine + var faker = new Faker(); + var culture = CultureInfo.InvariantCulture; + + // Ensure repeatable datasets across executions + Randomizer.Seed = new Random(84); + + // South African Provinces array lookup helper + var southAfricanProvinces = new[] + { + "Gauteng", "Western Cape", "KwaZulu-Natal", "Eastern Cape", + "Free State", "Limpopo", "Mpumalanga", "North West", "Northern Cape" + }; + + // South African major towns matching geographic boundaries roughly + var southAfricanCities = new[] { "Midrand", "Johannesburg", "Pretoria", "Cape Town", "Durban", "Gqeberha", "Polokwane", "Nelspruit", "Bloemfontein" }; + + // Tracks sequential Address IDs added globally to the system across all loops + long addressSequenceCounter = 0; + + // 2: Create 15 customers with resources sequentially + for (int c = 0; c < 15; c++) + { + if (stoppingToken.IsCancellationRequested) break; + + // Determine if this specific iteration represents a Corporate Client or an Individual Consumer + bool isCompanyCustomer = faker.Random.Bool(0.4f); // 40% chance of seeding a corporate entity + + string customerFirstName = faker.Name.FirstName(); + string customerLastName = faker.Name.LastName(); + + string companyName = isCompanyCustomer ? faker.Company.CompanyName() : ""; + string companySuffix = isCompanyCustomer ? faker.Company.CompanySuffix() : ""; + string fullCompanyName = isCompanyCustomer ? $"{companyName} {companySuffix}" : ""; + + string customerEmail = isCompanyCustomer + ? faker.Internet.Email(firstName: companyName, provider: "co.za").ToLower(culture) + : faker.Internet.Email(customerFirstName, customerLastName).ToLower(culture); + + string customerPhone = faker.Phone.PhoneNumber("087#######"); // Corporate VOIP / Personal South African cell line format + string customerWebsite = isCompanyCustomer ? faker.Internet.Url().Replace("www.", $"www.{companyName.ToLower(culture)}.") : ""; + string customerVat = isCompanyCustomer ? faker.Phone.PhoneNumber("4#########") : ""; // SA VAT registration starts with a 4 + + // Randomly select distinct Social Media channels + var chosenSocialType = faker.PickRandom(); + string socialMediaUrl = chosenSocialType switch + { + SocialMediaTypes.LinkedIn => isCompanyCustomer ? $"https://linkedin.com/company/{companyName.ToLower(culture)}" : $"https://linkedin.com/in/{customerFirstName.ToLower(culture)}-{customerLastName.ToLower(culture)}", + SocialMediaTypes.GitHub => $"https://github.com/{(isCompanyCustomer ? "orgs/" + companyName.ToLower(culture) : customerFirstName.ToLower(culture))}", + _ => $"https://x.com/{(isCompanyCustomer ? companyName.ToLower(culture) : customerFirstName.ToLower(culture))}" + }; + + // 3: Create customer + var createCustomerResult = await customerService.CreateCustomerAsync(new CreateCustomer + { + Company = fullCompanyName, + Email = customerEmail, + Phone = customerPhone, + Website = customerWebsite, + SocialMedia = + [ + new Models.SocialMedia + { + Name = chosenSocialType.ToString(), + Type = chosenSocialType, + ImageUrl = $"https://cdn.example.com/icons/{chosenSocialType.ToString().ToLower(culture)}.png", + Url = socialMediaUrl + } + ], + VatNumber = customerVat + }, stoppingToken); + + if (createCustomerResult.IsFailed) + { + logger.LogError("Failed to create customer record at index {Index}: {Error}", c, createCustomerResult.Errors[0].Message); + break; + } + + var assignedCustomerId = createCustomerResult.Value; + + // 4: Create customer contact (only if customer is a company entity) + if (isCompanyCustomer) + { + var contactFirstName = faker.Name.FirstName(); + var contactLastName = faker.Name.LastName(); + + var createContactResult = await customerService.CreateCustomerContactAsync(assignedCustomerId, new CreateCustomerContact + { + Name = contactFirstName, + LastName = contactLastName, + Phone = faker.Phone.PhoneNumber("082#######"), // Typical South African mobile prefix format + Email = faker.Internet.Email(contactFirstName, contactLastName, provider: "company.co.za").ToLower(culture), + Type = ContactTypes.Business + }, stoppingToken); + + if (createContactResult.IsFailed) + { + logger.LogError("Failed to create company customer contact relation: {Error}", createContactResult.Errors[0].Message); + break; + } + } + + // Shared Randomizations for Regional Postal/Building details + var primaryState = faker.PickRandom(southAfricanProvinces); + var primaryCity = faker.PickRandom(southAfricanCities); + var shippingPostalCode = faker.Random.Replace("####"); + + var billingState = faker.PickRandom(southAfricanProvinces); + var billingCity = faker.PickRandom(southAfricanCities); + var billingPostalCode = faker.Random.Replace("####"); + + // 5: Create customer address - SHIPPING + var createShippingAddressResult = await customerService.CreateCustomerAddressAsync(assignedCustomerId, new CreateCustomerAddress + { + Name = isCompanyCustomer ? "Head Office Distribution" : "My Home Residence", + BuildingType = faker.PickRandom(), + Type = AddressType.Shipping, + Street = $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Street", + City = primaryCity, + State = primaryState, + Country = "South Africa", + IsPrimary = true, + Enabled = true, + PostalCode = shippingPostalCode + }, stoppingToken); + + long currentCustomerShippingAddressId = 0; + if (createShippingAddressResult.IsSuccess) + { + addressSequenceCounter++; + currentCustomerShippingAddressId = addressSequenceCounter; + } + else + { + logger.LogWarning("Failed to attach Shipping address profile: {Error}", createShippingAddressResult.Errors[0].Message); + } + + // 6: Create customer address - BILLING + var createBillingAddressResult = await customerService.CreateCustomerAddressAsync(assignedCustomerId, new CreateCustomerAddress + { + Name = isCompanyCustomer ? "Accounts Payable Department" : "Billing Address", + BuildingType = faker.PickRandom(), + Type = AddressType.Billing, + Street = isCompanyCustomer ? $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Boulevard" : $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Street", + City = billingCity, + State = billingState, + Country = "South Africa", + IsPrimary = false, + Enabled = true, + PostalCode = billingPostalCode + }, stoppingToken); + + long currentCustomerBillingAddressId = 0; + if (createBillingAddressResult.IsSuccess) + { + addressSequenceCounter++; + currentCustomerBillingAddressId = addressSequenceCounter; + } + else + { + logger.LogError("Failed to attach Billing address profile: {Error}", createBillingAddressResult.Errors[0].Message); + break; + } + + // 7: Challenge Extrapolation — Create a random number of orders (0 to 4 orders) per customer + int ordersToGenerate = faker.Random.Number(0, 4); + for (int o = 0; o < ordersToGenerate; o++) + { + var deliveryInstructions = faker.PickRandom( + "Leave at reception desk", + "Please call before delivery", + "At the intercom, dial 1 then option 2", + "Leave with security guard at front gate", + "Deliver to back delivery bay" + ); + + // Use the calculated sequential Billing Address Id for order creation + var orderResult = await orderService.CreateOrderAsync( + assignedCustomerId, + new CreateOrder(currentCustomerBillingAddressId, deliveryInstructions), + stoppingToken + ); + + if (orderResult.IsFailed) + { + logger.LogWarning("Failed to create purchase order shell context: {Error}", orderResult.Errors[0].Message); + continue; + } + + long seededOrderId = orderResult.Value; + + // Build a varying array of items using valid product bounds (IDs: 0 to 21) + int lineItemsCount = faker.Random.Number(1, 5); + var itemsList = new List(); + + for (int i = 0; i < lineItemsCount; i++) + { + long randomProductId = faker.Random.Number(0, 21); + long randomProductPriceId = faker.Random.Number(0, 21); + int itemQuantity = faker.Random.Number(1, 3); + + itemsList.Add(new CreateOrderItem(randomProductId, randomProductPriceId, itemQuantity)); + } + + // Push bulk items payload into order via matching test framework signatures + var addItemsResult = await orderService.AddItemsToOrderAsync(seededOrderId, [.. itemsList], stoppingToken); + if (addItemsResult.IsFailed) + { + logger.LogWarning("Failed to link item collections to Order Id {Id}", seededOrderId); + continue; + } + + // Randomly select an order status matrix pathing + var targetedOrderStatus = faker.PickRandom(); + await orderService.UpdateOrderStatusAsync(seededOrderId, targetedOrderStatus, stoppingToken); + + // Check lifecycle workflow criteria: Attach dynamic shipping if status warrants it + if (targetedOrderStatus != OrderStatus.Pending && + targetedOrderStatus != OrderStatus.Cancelled && + targetedOrderStatus != OrderStatus.Failed && + currentCustomerShippingAddressId > 0) + { + // Select from seeded Shipping Providers in step 1 (IDs: 1, 2, or 3) + long randomShippingProviderId = faker.Random.Number(1, 3); + + var addShippingResult = await orderService.AddShippingToOrderAsync( + seededOrderId, + new CreateShipping(currentCustomerShippingAddressId, randomShippingProviderId), + stoppingToken + ); + + if (addShippingResult.IsSuccess) + { + long assignedShippingId = addShippingResult.Value; + + // Transition logistics flags matching delivery metrics + var shippingStatus = faker.PickRandom(); + await orderService.UpdateShippingStatusAsync(seededOrderId, shippingStatus, stoppingToken); + + if (shippingStatus == ShippingStatuses.Shipped || shippingStatus == ShippingStatuses.Delivered) + { + string rawTrackingCode = $"ZA{faker.Random.Replace("#########")}NV"; + await orderService.UpdateShippingTrackingNumberAsync(seededOrderId, assignedShippingId, rawTrackingCode); + } + } + } + } + + logger.LogInformation("Successfully seeded customer profile #{Index}: {Name} alongside {Count} orders.", c, isCompanyCustomer ? fullCompanyName : $"{customerFirstName} {customerLastName}", ordersToGenerate); + } + + logger.LogInformation("Customer Seeding completed successfully."); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj new file mode 100644 index 0000000..9318292 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj @@ -0,0 +1,160 @@ + + + + Exe + net10.0 + enable + enable + 5c3bc894-8654-4691-99e8-f90d3414843f + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs new file mode 100644 index 0000000..cb96bfe --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs @@ -0,0 +1,246 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks; +using LiteCharms.Features.MidrandBooks.Authors; +using LiteCharms.Features.MidrandBooks.Products; +using LiteCharms.Features.MidrandBooks.Seed.Configuration; + +namespace LiteCharms.Features.MidrandBooks.Seed; + +public sealed class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService, + IFeatureManager features, IOptions options, ILogger logger) : BackgroundService +{ + private readonly CdnSettings cdnSettings = options.Value; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (await features.IsEnabledAsync("ProductsSeederService") is not true) return; + + logger.LogInformation("Product Seeding started"); + + if (cdnSettings.BookCovers is null || cdnSettings.BookCovers.Length == 0) + { + logger.LogWarning("No book covers found in CDN settings. Seeding aborted."); + return; + } + + // Initialize Bogus Faker engine + var faker = new Faker(); + var culture = CultureInfo.InvariantCulture; + + // Ensure repeatable data sets if run multiple times by anchoring the seed + Randomizer.Seed = new Random(42); + + foreach (var bookCover in cdnSettings.BookCovers) + { + if (stoppingToken.IsCancellationRequested) break; + + // Generate beautifully mixed eclectic topics on the fly + var bookTopic = faker.PickRandom( + // --- Tech & IT --- + "C# 12 & Modern .NET Architecture", + "PostgreSQL Database Optimization", + "Docker & Kubernetes in Production", + "Domain-Driven Design Paradigms", + "Artificial Intelligence with Python", + + // --- Sci-Fi & Fantasy --- + "The Chronicles of the Quantum Nebula", + "Legends of the Lost Cybernetic Kingdom", + "Parallel Dimensions and Rogue Time Streams", + "The Last Android in Neo-Johannesburg", + + // --- Thrillers, Mystery & Crime --- + "The Midnight Code Cryptograph", + "Shadows in the Highveld", + "The Silent Witness of Midrand", + "Deception on the 14th Floor", + + // --- Business, Finance & Wealth --- + "Mastering the South African Tech Market", + "The Modern Entrepreneur's Blueprint", + "Generational Wealth and Venture Capital", + "Negotiation Tactics for High-Stakes Deals", + + // --- Self-Help & Personal Growth --- + "The Art of Relentless Focus", + "Building High-Performance Habits", + "The Mindfulness Guide for Software Engineers", + "Unlocking Creative Flow Under Pressure" + ); + + // Dynamic raw title generation formulas executed via random function picker + var titlePatterns = new Func[] + { + () => $"{faker.Company.CatchPhrase()} with {bookTopic}", + () => $"The {faker.Commerce.ProductAdjective()} Guide to {bookTopic}", + () => $"Mastering {bookTopic}: A {faker.Company.Bs()} Blueprint", + () => $"{bookTopic} for the Modern {faker.Name.JobTitle()}", + () => $"Advanced {bookTopic}: Demystifying the {faker.Company.CatchPhrase()}", + () => $"{faker.Random.Replace("###")} Blueprints for {bookTopic}" + }; + + // Pick a format template and resolve it down to raw string text + var rawTitle = faker.PickRandom(titlePatterns)(); + var bookTitle = rawTitle.Length > 255 ? rawTitle[..252] + "..." : rawTitle; + + var rawSummary = $"A comprehensive guide to mastering {bookTopic}. Learn modern implementation techniques through real-world software engineering paradigms."; + var bookSummary = rawSummary.Length > 512 ? rawSummary[..509] + "..." : rawSummary; + + // Generating a single concise paragraph ensures a rich text description falling safely well under 1024 + var rawDescription = faker.Lorem.Paragraph(3); + var bookDescription = rawDescription.Length > 1024 ? rawDescription[..1021] + "..." : rawDescription; + + var authorFirstName = faker.Name.FirstName(); + var authorLastName = faker.Name.LastName(); + var publisherCompany = faker.Company.CompanyName(); + + // Safe bounded random picking for book thumbnails + string? pickedBookThumbnail = null; + string? pickedBookThumbnail1 = null; + string? pickedBookThumbnail2 = null; + string? pickedBookThumbnail3 = null; + string? pickedBookThumbnail4 = null; + if (cdnSettings.BookThumbnails is not null && cdnSettings.BookThumbnails.Length > 0) + { + pickedBookThumbnail = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}"; + pickedBookThumbnail1 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}"; + pickedBookThumbnail2 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}"; + pickedBookThumbnail3 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}"; + pickedBookThumbnail4 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}"; + } + + // Step 1: Add Product + var productCreateResult = await productService.CreateProductAsync(new Products.Models.CreateProduct + { + Name = bookTitle, + Summary = bookSummary, + Description = bookDescription, + ImageUrl = $"{cdnSettings.BaseCdn}{bookCover}", + Type = ProductTypes.Book, + Metadata = new Models.ProductMetadata + { + CopyrightInfo = $"© {DateTime.UtcNow.Year} {publisherCompany}. All rights reserved.", + ManufactureDate = faker.Date.Past(3).ToString("yyyy-MM-dd", culture), + Manufacturer = $"{authorFirstName} {authorLastName} / {publisherCompany}", + SerialNumber = faker.Phone.PhoneNumber("978-##########") + }, + Categories = ["Coding", "Computers", "IT"], + ThumbnailUrls = pickedBookThumbnail is not null ? [pickedBookThumbnail, pickedBookThumbnail1!, pickedBookThumbnail2!, pickedBookThumbnail3!, pickedBookThumbnail4!] : null + }, stoppingToken); + + if (productCreateResult.IsFailed) + { + logger.LogError("Failed to create product: {Error}", productCreateResult.Errors[0].Message); + break; + } + + // Step 2: Enable product so it can show on the shop + var enableProductResult = await productService.UpdateProductStatusAsync(productId: productCreateResult.Value, isEnabled: true, stoppingToken); + + if (enableProductResult.IsFailed) + { + logger.LogError("Failed to enable created product: {Error}", enableProductResult.Errors[0].Message); + break; + } + + // Step 3: Create Product Price + var productPriceCreateResult = await productService.CreateProductPriceAsync(productId: productCreateResult.Value, request: new Products.Models.CreateProductPrice + { + Amount = Math.Round(faker.Random.Decimal(150m, 650m), 2), + Discount = 0.0m + }, stoppingToken); + + if (productPriceCreateResult.IsFailed) + { + logger.LogError("Failed to create product price: {Error}", productPriceCreateResult.Errors[0].Message); + break; + } + + // Safe bounded picking for Authors (Real Avatars) + string authorAvatarUrl = faker.Internet.Avatar(); // Fallback + if (cdnSettings.Authors is not null && cdnSettings.Authors.Length > 0) + { + authorAvatarUrl = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.Authors)}"; + } + + // Safe bounded picking for Author Thumbnails (Cartoon Avatars) + string? authorThumbnailUrl = null; + if (cdnSettings.AuthorThumbnails is not null && cdnSettings.AuthorThumbnails.Length > 0) + { + var selectedThumb = faker.PickRandom(cdnSettings.AuthorThumbnails); + authorThumbnailUrl = $"{cdnSettings.BaseCdn}{selectedThumb}.jpg"; + } + + // Synthesize a highly dynamic, organic opening bio statement + var professionalBackgrounds = new[] + { + $"{authorFirstName} {authorLastName} is an award-winning {faker.Name.JobDescriptor()} {faker.Name.JobTitle()} with over {faker.Random.Number(5, 25)} years of core engineering domain expertise.", + $"As a veteran systems consultant and practicing {faker.Name.JobTitle()}, {authorFirstName} has spent decades leading digital infrastructure transformations and managing complex topologies.", + $"Operating from modern innovation hubs, {authorFirstName} {authorLastName} specializes in global product strategies and serves as an authority in {faker.Name.JobDescriptor()} computing.", + $"With a rich professional background as a principal {faker.Name.JobTitle()} at {publisherCompany}, {authorFirstName} has spent a lifetime refining the system workflows highlighted here." + }; + + // Pick a randomized context hook and append a 2-paragraph contextual narrative block + var biographyPrefix = faker.PickRandom(professionalBackgrounds); + var authorBiography = $"{biographyPrefix} {faker.Lorem.Paragraph(2)}"; + + // Step 4: Create Author + var authorCreateResult = await authorService.CreateAuthorAsync(request: new Authors.Models.CreateAuthor + { + Name = authorFirstName, + LastName = authorLastName, + Company = publisherCompany, + VatNumber = faker.Random.Bool() ? faker.Phone.PhoneNumber("4#########") : "", + PublisherType = faker.PickRandom(), + Email = faker.Internet.Email(authorFirstName, authorLastName), + Website = faker.Internet.Url(), + ImageUrl = authorAvatarUrl, + SocialMedia = + [ + new Models.SocialMedia + { + Name = "LinkedIn", + ImageUrl = "https://cdn.example.com/icons/linkedin.png", + Type = SocialMediaTypes.LinkedIn, + Url = $"https://linkedin.com/in/{authorFirstName.ToLower(culture)}-{authorLastName.ToLower(culture)}" + }, + new Models.SocialMedia + { + Name = "GitHub", + ImageUrl = "https://cdn.example.com/icons/github.png", + Type = SocialMediaTypes.GitHub, + Url = $"https://github.com/tech-{authorFirstName.ToLower(culture)}" + } + ], + Biography = authorBiography, + ThumbnailImageUrl = authorThumbnailUrl + }, stoppingToken); + + if (authorCreateResult.IsFailed) + { + logger.LogError("Failed to create author: {Error}", authorCreateResult.Errors[0].Message); + break; + } + + // Step 5: Create Author-Book link (product linkage) + var authorBookCreateResult = await booksService.CreateBookAsync(authorId: authorCreateResult.Value, productId: productCreateResult.Value, stoppingToken); + + if (authorBookCreateResult.IsFailed) + { + logger.LogError("Failed to create author-book linkage: {Error}", authorBookCreateResult.Errors[0].Message); + break; + } + + var enableAuthorBookResult = await booksService.UpdateBookStatusAsync(bookId: authorBookCreateResult.Value, isEnabled: true, stoppingToken); + + if (enableAuthorBookResult.IsFailed) + { + logger.LogError("Failed to enable author-book link: {Error}", enableAuthorBookResult.Errors[0].Message); + break; + } + + logger.LogInformation("Successfully seeded book product: {Title}", bookTitle); + } + + logger.LogInformation("Product Seeding completed successfully."); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Seed/Program.cs b/LiteCharms.Features.MidrandBooks.Seed/Program.cs new file mode 100644 index 0000000..8483ab8 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/Program.cs @@ -0,0 +1,27 @@ +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Seed; +using LiteCharms.Features.MidrandBooks.Seed.Configuration; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Configuration + .AddCommandLine(args) + .AddUserSecrets(typeof(Program).Assembly) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables(); + +builder.Services.AddScopedFeatureManagement(); + +builder.Services + .AddLogging() + .AddShopServices() + .AddHostedService() + .AddHostedService() + .AddHostedService() + .AddMidrandShopDatabase(builder.Configuration); + +builder.Services.Configure(options => builder.Configuration.GetSection(nameof(CdnSettings)).Bind(options)); + +using var host = builder.Build(); + +await host.RunAsync(); \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json new file mode 100644 index 0000000..b7a0751 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json @@ -0,0 +1,266 @@ +{ + "FeatureManagement": { + "CategorySeederService": false, + "CustomerSeederService": false, + "ProductsSeederService": false + }, + "CdnSettings": { + "BaseCdn": "https://bookshop.cdn.khongisa.co.za/design/", + "BookCovers": [ + "144e0314-0bd8-4e2c-8814-2b34608c0600_1764780116467.webp", + "2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp", + "3cd172b9-416e-4b3a-b613-7ff0a3aecc4c_1764780129459.webp", + "762947b6-63ac-436e-98fb-91dd94de1d82_1764780126141.webp", + "7b42f23f-666c-4dd9-82e1-9b928e1b4db7_1764780186376.webp", + "8e281eea-8910-473d-b650-43f539995d9e_1764780152316.webp", + "91d18e2c-6ee6-44b4-84a5-8692db5dbfe4_1764780114484.webp", + "94fdf403-4d10-4cb4-a537-fc914a1f5ba8_1764780198423.webp", + "9b881786-75e1-47f7-9bc9-27188d46d1ec_1764780122458.webp", + "c417236d-a628-45eb-82c2-ec1cb0326e4f_1765006317965.webp", + "clpqjsgi71jpr1i6392e449l2.webp", + "clps1mk9f0m1z1i32grvq17tg.webp", + "clq550xxy2dab1ywb6mdv4acv.webp", + "clq5535o52dj51yw557ml49c1.webp", + "clq55455w2dcm1yvd8d8258d8.webp", + "clq558fkh2djy1ywbghwcawnh.webp", + "clq55cvqh2dqi1yyratl123wq.webp", + "clrhnk94e115k1y00bg8h9ixb.webp", + "d44a3c04-f124-4f0b-8301-3841ae2fd439_1764780121224.webp", + "e6ba52f208914285bcdf1966cfb08f6f.jpg", + "fa9cbbe6-f947-4f83-8e98-61d2661f43e0_1764841636705.webp" + ], + "Authors": [ + "authors/uifaces-human-avatar.jpg", + "authors/uifaces-human-avatar-1.jpg", + "authors/uifaces-human-avatar-2.jpg", + "authors/uifaces-human-avatar-3.jpg", + "authors/uifaces-human-avatar-4.jpg", + "authors/uifaces-human-avatar-5.jpg", + "authors/uifaces-human-avatar-6.jpg", + "authors/uifaces-human-avatar-7.jpg", + "authors/uifaces-human-avatar-8.jpg", + "authors/uifaces-human-avatar-9.jpg", + "authors/uifaces-human-avatar-10.jpg", + "authors/uifaces-human-avatar-11.jpg", + "authors/uifaces-human-avatar-12.jpg", + "authors/uifaces-human-avatar-13.jpg", + "authors/uifaces-human-avatar-14.jpg", + "authors/uifaces-human-avatar-15.jpg", + "authors/uifaces-human-avatar-16.jpg" + ], + "AuthorThumbnails": [ + "authors/thumbnails/uifaces-cartoon-avatar-1", + "authors/thumbnails/uifaces-cartoon-avatar-2", + "authors/thumbnails/uifaces-cartoon-avatar-3", + "authors/thumbnails/uifaces-cartoon-avatar-4", + "authors/thumbnails/uifaces-cartoon-avatar-5", + "authors/thumbnails/uifaces-cartoon-avatar-6", + "authors/thumbnails/uifaces-cartoon-avatar-7", + "authors/thumbnails/uifaces-cartoon-avatar-8", + "authors/thumbnails/uifaces-cartoon-avatar-9", + "authors/thumbnails/uifaces-cartoon-avatar-10" + ], + "BookThumbnails": [ + "thumbnails/book_thumbnail_001.jpg", + "thumbnails/book_thumbnail_002.jpg", + "thumbnails/book_thumbnail_003.jpg", + "thumbnails/book_thumbnail_004.jpg", + "thumbnails/book_thumbnail_005.jpg", + "thumbnails/book_thumbnail_006.jpg", + "thumbnails/book_thumbnail_007.jpg", + "thumbnails/book_thumbnail_008.jpg", + "thumbnails/book_thumbnail_009.jpg", + "thumbnails/book_thumbnail_010.jpg", + "thumbnails/book_thumbnail_011.jpg", + "thumbnails/book_thumbnail_012.jpg", + "thumbnails/book_thumbnail_013.jpg", + "thumbnails/book_thumbnail_014.jpg", + "thumbnails/book_thumbnail_015.jpg", + "thumbnails/book_thumbnail_016.jpg", + "thumbnails/book_thumbnail_017.jpg", + "thumbnails/book_thumbnail_018.jpg", + "thumbnails/book_thumbnail_019.jpg", + "thumbnails/book_thumbnail_020.jpg", + "thumbnails/book_thumbnail_021.jpg", + "thumbnails/book_thumbnail_022.jpg", + "thumbnails/book_thumbnail_023.jpg", + "thumbnails/book_thumbnail_024.jpg", + "thumbnails/book_thumbnail_025.jpg", + "thumbnails/book_thumbnail_026.jpg", + "thumbnails/book_thumbnail_027.jpg", + "thumbnails/book_thumbnail_028.jpg", + "thumbnails/book_thumbnail_029.jpg", + "thumbnails/book_thumbnail_030.jpg", + "thumbnails/book_thumbnail_031.jpg", + "thumbnails/book_thumbnail_032.jpg", + "thumbnails/book_thumbnail_033.jpg", + "thumbnails/book_thumbnail_034.jpg", + "thumbnails/book_thumbnail_035.jpg", + "thumbnails/book_thumbnail_036.jpg", + "thumbnails/book_thumbnail_037.jpg", + "thumbnails/book_thumbnail_038.jpg", + "thumbnails/book_thumbnail_039.jpg", + "thumbnails/book_thumbnail_040.jpg", + "thumbnails/book_thumbnail_041.jpg", + "thumbnails/book_thumbnail_042.jpg", + "thumbnails/book_thumbnail_043.jpg", + "thumbnails/book_thumbnail_044.jpg", + "thumbnails/book_thumbnail_045.jpg", + "thumbnails/book_thumbnail_046.jpg", + "thumbnails/book_thumbnail_047.jpg", + "thumbnails/book_thumbnail_048.jpg", + "thumbnails/book_thumbnail_049.jpg", + "thumbnails/book_thumbnail_050.jpg", + "thumbnails/book_thumbnail_051.jpg", + "thumbnails/book_thumbnail_052.jpg", + "thumbnails/book_thumbnail_053.jpg", + "thumbnails/book_thumbnail_054.jpg", + "thumbnails/book_thumbnail_055.jpg", + "thumbnails/book_thumbnail_056.jpg", + "thumbnails/book_thumbnail_057.jpg", + "thumbnails/book_thumbnail_058.jpg", + "thumbnails/book_thumbnail_059.jpg", + "thumbnails/book_thumbnail_060.jpg", + "thumbnails/book_thumbnail_061.jpg", + "thumbnails/book_thumbnail_062.jpg", + "thumbnails/book_thumbnail_063.jpg", + "thumbnails/book_thumbnail_064.jpg", + "thumbnails/book_thumbnail_065.jpg", + "thumbnails/book_thumbnail_066.jpg", + "thumbnails/book_thumbnail_067.jpg", + "thumbnails/book_thumbnail_068.jpg", + "thumbnails/book_thumbnail_069.jpg", + "thumbnails/book_thumbnail_070.jpg", + "thumbnails/book_thumbnail_071.jpg", + "thumbnails/book_thumbnail_072.jpg", + "thumbnails/book_thumbnail_073.jpg", + "thumbnails/book_thumbnail_074.jpg", + "thumbnails/book_thumbnail_075.jpg", + "thumbnails/book_thumbnail_076.jpg", + "thumbnails/book_thumbnail_077.jpg", + "thumbnails/book_thumbnail_078.jpg", + "thumbnails/book_thumbnail_079.jpg", + "thumbnails/book_thumbnail_080.jpg", + "thumbnails/book_thumbnail_081.jpg", + "thumbnails/book_thumbnail_082.jpg", + "thumbnails/book_thumbnail_083.jpg", + "thumbnails/book_thumbnail_084.jpg", + "thumbnails/book_thumbnail_085.jpg", + "thumbnails/book_thumbnail_086.jpg", + "thumbnails/book_thumbnail_087.jpg", + "thumbnails/book_thumbnail_088.jpg", + "thumbnails/book_thumbnail_089.jpg", + "thumbnails/book_thumbnail_090.jpg", + "thumbnails/book_thumbnail_091.jpg", + "thumbnails/book_thumbnail_092.jpg", + "thumbnails/book_thumbnail_093.jpg", + "thumbnails/book_thumbnail_094.jpg", + "thumbnails/book_thumbnail_095.jpg", + "thumbnails/book_thumbnail_096.jpg", + "thumbnails/book_thumbnail_097.jpg", + "thumbnails/book_thumbnail_098.jpg", + "thumbnails/book_thumbnail_099.jpg", + "thumbnails/book_thumbnail_100.jpg", + "thumbnails/book_thumbnail_101.jpg", + "thumbnails/book_thumbnail_102.jpg", + "thumbnails/book_thumbnail_103.jpg", + "thumbnails/book_thumbnail_104.jpg", + "thumbnails/book_thumbnail_105.jpg", + "thumbnails/book_thumbnail_106.jpg", + "thumbnails/book_thumbnail_107.jpg", + "thumbnails/book_thumbnail_108.jpg", + "thumbnails/book_thumbnail_109.jpg", + "thumbnails/book_thumbnail_110.jpg", + "thumbnails/book_thumbnail_111.jpg", + "thumbnails/book_thumbnail_112.jpg", + "thumbnails/book_thumbnail_113.jpg", + "thumbnails/book_thumbnail_114.jpg", + "thumbnails/book_thumbnail_115.jpg", + "thumbnails/book_thumbnail_116.jpg", + "thumbnails/book_thumbnail_117.jpg", + "thumbnails/book_thumbnail_118.jpg", + "thumbnails/book_thumbnail_119.jpg", + "thumbnails/book_thumbnail_120.jpg", + "thumbnails/book_thumbnail_121.jpg", + "thumbnails/book_thumbnail_122.jpg", + "thumbnails/book_thumbnail_123.jpg", + "thumbnails/book_thumbnail_124.jpg", + "thumbnails/book_thumbnail_125.jpg", + "thumbnails/book_thumbnail_126.jpg", + "thumbnails/book_thumbnail_127.jpg", + "thumbnails/book_thumbnail_128.jpg", + "thumbnails/book_thumbnail_129.jpg", + "thumbnails/book_thumbnail_130.jpg", + "thumbnails/book_thumbnail_131.jpg", + "thumbnails/book_thumbnail_132.jpg", + "thumbnails/book_thumbnail_133.jpg", + "thumbnails/book_thumbnail_134.jpg", + "thumbnails/book_thumbnail_135.jpg", + "thumbnails/book_thumbnail_136.jpg", + "thumbnails/book_thumbnail_137.jpg", + "thumbnails/book_thumbnail_138.jpg", + "thumbnails/book_thumbnail_139.jpg", + "thumbnails/book_thumbnail_140.jpg", + "thumbnails/book_thumbnail_141.jpg", + "thumbnails/book_thumbnail_142.jpg", + "thumbnails/book_thumbnail_143.jpg", + "thumbnails/book_thumbnail_144.jpg", + "thumbnails/book_thumbnail_145.jpg", + "thumbnails/book_thumbnail_146.jpg", + "thumbnails/book_thumbnail_147.jpg", + "thumbnails/book_thumbnail_148.jpg", + "thumbnails/book_thumbnail_149.jpg", + "thumbnails/book_thumbnail_150.jpg", + "thumbnails/book_thumbnail_151.jpg", + "thumbnails/book_thumbnail_152.jpg", + "thumbnails/book_thumbnail_153.jpg", + "thumbnails/book_thumbnail_154.jpg", + "thumbnails/book_thumbnail_155.jpg", + "thumbnails/book_thumbnail_156.jpg", + "thumbnails/book_thumbnail_157.jpg", + "thumbnails/book_thumbnail_158.jpg", + "thumbnails/book_thumbnail_159.jpg", + "thumbnails/book_thumbnail_160.jpg", + "thumbnails/book_thumbnail_161.jpg", + "thumbnails/book_thumbnail_162.jpg", + "thumbnails/book_thumbnail_163.jpg", + "thumbnails/book_thumbnail_164.jpg", + "thumbnails/book_thumbnail_165.jpg", + "thumbnails/book_thumbnail_166.jpg", + "thumbnails/book_thumbnail_167.jpg", + "thumbnails/book_thumbnail_168.jpg", + "thumbnails/book_thumbnail_169.jpg", + "thumbnails/book_thumbnail_170.jpg", + "thumbnails/book_thumbnail_171.jpg", + "thumbnails/book_thumbnail_172.jpg", + "thumbnails/book_thumbnail_173.jpg", + "thumbnails/book_thumbnail_174.jpg", + "thumbnails/book_thumbnail_175.jpg", + "thumbnails/book_thumbnail_176.jpg", + "thumbnails/book_thumbnail_177.jpg", + "thumbnails/book_thumbnail_178.jpg", + "thumbnails/book_thumbnail_179.jpg", + "thumbnails/book_thumbnail_180.jpg", + "thumbnails/book_thumbnail_181.jpg", + "thumbnails/book_thumbnail_182.jpg", + "thumbnails/book_thumbnail_183.jpg", + "thumbnails/book_thumbnail_184.jpg", + "thumbnails/book_thumbnail_185.jpg", + "thumbnails/book_thumbnail_186.jpg", + "thumbnails/book_thumbnail_187.jpg", + "thumbnails/book_thumbnail_188.jpg", + "thumbnails/book_thumbnail_189.jpg", + "thumbnails/book_thumbnail_190.jpg", + "thumbnails/book_thumbnail_191.jpg", + "thumbnails/book_thumbnail_192.jpg", + "thumbnails/book_thumbnail_193.jpg", + "thumbnails/book_thumbnail_194.jpg", + "thumbnails/book_thumbnail_195.jpg", + "thumbnails/book_thumbnail_196.jpg", + "thumbnails/book_thumbnail_197.jpg", + "thumbnails/book_thumbnail_198.jpg", + "thumbnails/book_thumbnail_199.jpg", + "thumbnails/book_thumbnail_200.jpg" + ] + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs new file mode 100644 index 0000000..8116065 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/AuthorServiceFeatureTests.cs @@ -0,0 +1,81 @@ +using LiteCharms.Features.MidrandBooks.Authors; +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.Models; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class AuthorServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly AuthorService authorService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateAuthorAsync_ShouldReturn_ResultWithAuthorId() + { + var request = new CreateAuthor + { + Name = "John", + LastName = "Doe", + Company = "Solo Publishers", + Email = "solo@publishers.co.za", + PublisherType = PublisherTypes.Independent, + ImageUrl = "" + }; + + var result = await authorService.CreateAuthorAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task UpdateAuthorAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateAuthor + { + Name = "Jane", + LastName = "Doe", + Company = "Solo Publishers", + Email = "solo@publishers.co.za", + PublisherType = PublisherTypes.Independent, + ImageUrl = "" + }; + + var result = await authorService.UpdateAuthorAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetAuthors_ShouldReturn_ResultWithAuthorList() + { + var range = new DateRange + { + From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 1000 + }; + + var result = await authorService.GetAuthorsAsync(range, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetAuthorAsync_ShouldReturn_ResultWithAuthor() + { + var result = await authorService.GetAuthorAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task UpdateAuthorStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await authorService.UpdateAuthorStatusAsync(1, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs new file mode 100644 index 0000000..57e4474 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/BooksServiceFeatureTests.cs @@ -0,0 +1,52 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class BooksServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly BooksService bookService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateBookAsync_ShouldReturn_ResultWithBookId() + { + var result = await bookService.CreateBookAsync(1, 2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetBookAsync_ShouldReturn_ResultWithBook() + { + var result = await bookService.GetBookAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetBooksByAuthorAsync_ShouldReturn_ResultWithAuthorBooks() + { + var result = await bookService.GetBooksByAuthorAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetPublishedBooksAsync_ShouldReturn_ResultWithBublishedBooks() + { + var result = await bookService.GetPublishedBooksAsync(0, 1000, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task UpdateBookStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await bookService.UpdateBookStatusAsync(1, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs new file mode 100644 index 0000000..f84b3f2 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs @@ -0,0 +1,70 @@ +using LiteCharms.Features.MidrandBooks.Categories; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class CategoryServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly CategoryService categoryService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task UpdateCategoryStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await categoryService.UpdateCategoryStatusAsync(3, false, false, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetCategoryAsync_ShouldReturn_ResultWithCategory() + { + var result = await categoryService.GetCategoryAsync(3, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetCategoriesAsync_ShouldReturn_All_ResultWithCategoryList() + { + var result = await categoryService.GetCategoriesAsync(isMain: null,fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCategoriesAsync_ShouldReturn_MainCategory_ResultWithCategoryList() + { + var result = await categoryService.GetCategoriesAsync(true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCategoriesAsync_ShouldReturn_SubMainCategory_ResultWithCategoryList() + { + var result = await categoryService.GetCategoriesAsync(false, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task CreateCategoriesAsync_ShouldReturn_ResultWithSuccess() + { + var result = await categoryService.CreateCategoriesAsync(fixture.CancellationToken, "Test", "Test 1", "Test 2"); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreateCategoryAsync_ShouldReturn_ResultWithCategoryId() + { + var result = await categoryService.CreateCategoryAsync("Test", true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs new file mode 100644 index 0000000..f102a78 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/CustomerServiceFeatureTests.cs @@ -0,0 +1,201 @@ +using LiteCharms.Features.MidrandBooks.Customers; +using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class CustomerServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly CustomerService customerService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateCustomerAsync_ShouldReturn_ResultWithCustomerId() + { + var request = new CreateCustomer + { + Company = "Book Lovers", + Email = "hank@booklovers.com", + Phone = "555 1245 8577", + Website = "https://www.booklovers.com" + }; + + var result = await customerService.CreateCustomerAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task CreateCustomerContactAsync_ShouldReturn_ResultWithCustomerContactId() + { + var request = new CreateCustomerContact + { + Name = "Sipho", + LastName = "Madlanga", + Phone = "0710857365", + Email = "sipho@madlanga.africa", + Type = ContactTypes.Business + }; + + var result = await customerService.CreateCustomerContactAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task CreateCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddressId() + { + var request = new CreateCustomerAddress + { + Name = "Business", + BuildingType = AddressBuildingTypes.MixedUse, + Type = AddressType.Shipping, + Street = "123 Building 4, XYZ Suburb, Some Region", + City = "Johannesburg", + State = "Gauteng", + Country = "South Africa", + IsPrimary = true, + Enabled = true, + PostalCode = "12345" + }; + + var result = await customerService.CreateCustomerAddressAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task UpdateCustomerAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateCustomer + { + Company = "Book Lovers", + Email = "hank@booklovers.com", + Phone = "555 1245 8578", + Website = "https://www.booklovers.com" + }; + + var result = await customerService.UpdateCustomerAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerContactAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateCustomerContact + { + Name = "Sipho", + LastName = "Madlanga", + Phone = "0710857366", + Email = "sipho@madlanga.africa", + Type = ContactTypes.Business + }; + + var result = await customerService.UpdateCustomerContactAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerAddressAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateCustomerAddress + { + Name = "Business", + BuildingType = AddressBuildingTypes.MixedUse, + Type = AddressType.Shipping, + Street = "123 Building 4, XYZ Suburb, Some Region", + City = "Johannesburg", + State = "Gauteng", + Country = "South Africa", + IsPrimary = true, + Enabled = true, + PostalCode = "12346" + }; + + var result = await customerService.UpdateCustomerAddressAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await customerService.UpdateCustomerStatusAsync(1, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerContactStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await customerService.UpdateCustomerContactStatusAsync(1, true, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateCustomerAddressStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await customerService.UpdateCustomerAddressStatusAsync(1, true, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetCustomersAsync_ShouldReturn_ResultWithCustomerList() + { + var result = await customerService.GetCustomersAsync(fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerContactsAsync_ShouldReturn_ResultWithCustomerContactList() + { + var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerAddressesAsync_ShouldReturn_ResultWithCustomerAddressList() + { + var result = await customerService.GetCustomerAddressesAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerAsync_ShouldReturn_ResultWithCustomer() + { + var result = await customerService.GetCustomerAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerContactAsync_ShouldReturn_ResultWithCustomerContact() + { + var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddress() + { + var result = await customerService.GetCustomerAddressAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj new file mode 100644 index 0000000..b8084c2 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj @@ -0,0 +1,49 @@ + + + + net10.0 + enable + enable + false + b205af96-ceef-44e1-851c-458c9fd1c437 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs new file mode 100644 index 0000000..1e01faa --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/OrderServiceFeatureTests.cs @@ -0,0 +1,196 @@ +using LiteCharms.Features.MidrandBooks.Orders; +using LiteCharms.Features.MidrandBooks.Orders.Models; +using LiteCharms.Features.Models; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class OrderServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly OrderService orderService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateOrderAsync_ShouldReturn_ResultWithOrderId() + { + var request = new CreateOrder(250, "At the intercomm, dial 1 then option 2"); + + var result = await orderService.CreateOrderAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task AddItemToOrderAsync_ShouldReturn_ResultWithOrderItemId() + { + var request = new CreateOrderItem(1, 1, 2); + + var result = await orderService.AddItemToOrderAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task AddItemsToOrderAsync_ShouldReturn_ResultWithSuccess() + { + var requests = new List + { + new(1, 1, 1), + new(1, 1, 3) + }; + + var result = await orderService.AddItemsToOrderAsync(1, [.. requests], fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task RemoveItemFromOrderAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.RemoveItemFromOrderAsync(1, 5, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task ClearOrderItemsAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.ClearOrderItemsAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CancelOrderAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.CancelOrderAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetOrderAsync_ShouldReturn_ResultWithOrder() + { + var result = await orderService.GetOrderAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetOrdersByCustomerAsync_ShouldReturn_ResultWithOrderList() + { + var result = await orderService.GetOrdersByCustomerAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetOrdersAsync_ShouldReturn_ResultWithOrderList() + { + var range = new DateRange + { + From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 1000 + }; + + var result = await orderService.GetOrdersAsync(range, 0, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task UpdateOrderStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.UpdateOrderStatusAsync(1, OrderStatus.Pending, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task AddShippingToOrderAsync_ShouldReturn_ResultWithSuccess() + { + var request = new CreateShipping(1, 2); + + var result = await orderService.AddShippingToOrderAsync(1, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task UpdateShippingStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.UpdateShippingStatusAsync(1, ShippingStatuses.Shipped, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetShippingByOrderIdAsync_ShouldReturn_ResultWithShipping() + { + var result = await orderService.GetShippingByOrderIdAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task RemoveShippingFromOrderAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.RemoveShippingFromOrderAsync(1, 1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateShippingTrackingNumberAsync_ShouldReturn_ResultWithSuccess() + { + var result = await orderService.UpdateShippingTrackingNumberAsync(1, 2, "NA0009969397"); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreateShippingProviderAsync_ShouldReturn_ResultWithShippingProviderId() + { + var request = new CreateShippingProvider(ShippingProviderTypes.FastWay, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel"); + + var result = await orderService.CreateShippingProviderAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task GetShippingProvidersAsync_ShouldReturn_ResultWithShippingProviderList() + { + var result = await orderService.GetShippingProvidersAsync(true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetShippingProviderAsync_ShouldReturn_ResultWithShippingProvider() + { + var result = await orderService.GetShippingProviderAsync(2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task UpdateShippingProviderAsync_ShouldReturn_ResultWithSuccess() + { + var request = new UpdateShippingProvider(2,true, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel"); + + var result = await orderService.UpdateShippingProviderAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs new file mode 100644 index 0000000..afecd71 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/PageServiceFeatureTests.cs @@ -0,0 +1,63 @@ +using LiteCharms.Features.MidrandBooks.Pages; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class PageServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly PageService pageService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreatePageAsync_ShouldReturn_ResultWithPageId() + { + + } + + [IntegrationFact] + public async Task GetPagesAsync_ByBookId_ShouldReturn_ResultWithPageList() + { + + } + + [IntegrationFact] + public async Task GetPageAsync_ShouldReturn_ResultWithPage() + { + + } + + [IntegrationFact] + public async Task GetPageByNumberAsync_ById_And_BookPageNumber_ShouldReturn_ResultWithPage() + { + + } + + [IntegrationFact] + public async Task UpdatePageAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task DeletePageAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task DeleteByPageTypeAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task DeleteAllAsync_ShouldReturn_ResultWithSuccess() + { + + } + + [IntegrationFact] + public async Task UpdatePageStatusAsync_ShouldReturn_ResultWithSuccess() + { + + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs new file mode 100644 index 0000000..7923a6b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/PayfastServiceFeatureTests.cs @@ -0,0 +1,113 @@ +using LiteCharms.Features.MidrandBooks.Payments; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public sealed class PayfastServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly PayfastService payfastService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task WriteLedgerEntryAsync_ShouldReturn_ResultWithGatewayLedgerId() + { + var request = new CreateGatewayLedgerEntry + { + OrderId = 1, + PaymentId = 1, + MerchantPaymentId = "M_REF_TEST_99", + PayfastPaymentId = "PF_SYS_ID_10023", + CustomerEmail = "buyer@litecharms.co.za", + AmountGross = 350.00m, + AmountFee = 12.50m, + AmountNet = 337.50m, + PaymentStatus = "COMPLETE" + }; + + var result = await payfastService.WriteLedgerEntryAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task ValidateReferrerIpAsync_WithValidPayfastHostIp_ShouldReturnTrue() + { + var addresses = await Dns.GetHostAddressesAsync("sandbox.payfast.co.za", fixture.CancellationToken); + + string liveTargetIp = addresses.First().ToString(); + + var result = await payfastService.ValidateReferrerIpAsync(liveTargetIp, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value); + } + + [IntegrationFact] + public async Task ValidateReferrerIpAsync_WithUntrustedIp_ShouldReturnFalse() + { + string rogueIp = "8.8.8.8"; + + var result = await payfastService.ValidateReferrerIpAsync(rogueIp, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.False(result.Value); + } + + [IntegrationFact] + public void ValidatePaymentAmount_WhenWithinAllowableDelta_ShouldReturnTrue() + { + decimal systemExpectedTotal = 199.99m; + string gatewayClearedGross = "200.00"; // Variance is exactly R0.01 + + var result = payfastService.ValidatePaymentAmount(systemExpectedTotal, gatewayClearedGross); + + Assert.True(result.IsSuccess); + Assert.True(result.Value); + } + + [IntegrationFact] + public void ValidatePaymentAmount_WhenVarianceBreachesDeltaBounds_ShouldReturnFalse() + { + decimal systemExpectedTotal = 199.99m; + string gatewayClearedGross = "150.00"; + + var result = payfastService.ValidatePaymentAmount(systemExpectedTotal, gatewayClearedGross); + + Assert.True(result.IsSuccess); + Assert.False(result.Value); + } + + [IntegrationFact] + public async Task ValidateServerConfirmationAsync_WithUnrecognizedPayload_ShouldReturnFalseFromCentralGateway() + { + // Arrange - Execute against actual Payfast servers using raw mock parameters. + // The server handshake will return 200 OK with string payload 'INVALID' + string arbitraryParameters = "merchant_id=10000000&payment_status=COMPLETE"; + + var result = await payfastService.ValidateServerConfirmationAsync(arbitraryParameters, isSandbox: true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.False(result.Value); // Handshake data rejected as fraudulent/unrecognized + } + + [IntegrationFact] + public void GenerateSignature_WithStandardTelemetryData_ShouldSucceedAndHashString() + { + var telemetryPayload = new Dictionary + { + { "merchant_id", "10049307" }, + { "merchant_key", "ju6navn0jcbf0" }, + { "amount_gross", "250.00" }, + { "item_name", "Midrand School Textbook Variant A" } + }; + + string passphrase = "oauth_test_signature_pass"; + + var result = PayfastService.GenerateSignature(telemetryPayload, passphrase); + + Assert.True(result.IsSuccess); + Assert.False(string.IsNullOrWhiteSpace(result.Value)); + Assert.Equal(32, result.Value.Length); // MD5 outputs hex representations totaling 32 characters + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs new file mode 100644 index 0000000..c1514b1 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/PaymentServiceFeatureTests.cs @@ -0,0 +1,98 @@ +using LiteCharms.Features.MidrandBooks.Payments; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public sealed class PaymentServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly PaymentService paymentService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CreateRefundAsync_ShouldReturn_ResultWithRefundId() + { + var request = new CreateRefund + { + Amount = 50, + OrderId = 2, + Type = RefundTypes.Partial, + Reason = "Returned damaged book", + Status = RefundStatus.Completed, + }; + + var result = await paymentService.CreateRefundAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task WriteLedgerEntryAsync_ShouldReturn_ResultWithSuccess() + { + var request = new CreateLedgerEntry + { + CustomerId = 1, + OrderId = 1, + PaymentGatewayId = 1, + PaymentGatewayReference = "TEST REFERENCE", + PaymentId = 1, + Status = LedgerStatuses.Received, + }; + + var result = await paymentService.WriteLedgerEntryAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetPaymentGatewayAsync_ShouldReturn_ResultWithPaymentGateway() + { + var result = await paymentService.GetPaymentGatewayAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task CreatePaymentGatewayAsync_ShouldReturn_ResultWithGatewayId() + { + var request = new CreatePaymentGateway + { + IsSandbox = true, + MerchantId = "10049307", + MerchantKey = "ju6navn0jcbf0", + Name = "Payfast", + Website = "https://sandbox.payfast.co.za/eng/process", + }; + + var result = await paymentService.CreatePaymentGatewayAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task CompletePaymentAsync_ShouldReturn_ResultWithSuccess() + { + var result = await paymentService.CompletePaymentAsync(1, PaymentStatuses.Paid, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdatePaymentAsync_ShouldReturn_ResultWithSuccess() + { + var result = await paymentService.UpdatePaymentAsync(1, 200, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreatePaymentAsync_ShouldReturn_ResultWithPaymentId() + { + var result = await paymentService.CreatePaymentAsync(100, 1, "HASHEDID", fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs new file mode 100644 index 0000000..860f059 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs @@ -0,0 +1,217 @@ +using LiteCharms.Features.MidrandBooks.Products; +using LiteCharms.Features.MidrandBooks.Products.Models; +using LiteCharms.Features.Models; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture +{ + private readonly ProductService productService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task CheckProductStockAvailabilityAsync_ShouldReturn_ResultWithProductInventory() + { + var result = await productService.CheckProductStockAvailabilityAsync(1, 1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task ReserveProductInventoryAsync_ShouldReturn_ResultWithSuccess() + { + var request = new ReserveStock + { + ProductId = 1, + ProductPriceId = 1, + Reservation = 100, + }; + + var result = await productService.ReserveProductInventoryAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task AllocateProductInventoryAsync_ShouldReturn_ResultWithSuccess() + { + var request = new AllocateStock + { + ProductId = 1, + ProductPriceId = 1, + Allocation = 500, + }; + + var result = await productService.AllocateProductInventoryAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } + + [IntegrationFact] + public async Task AddProductCategoryAsync_ShouldReturn_ResultWithId() + { + var result = await productService.AddProductCategoryAsync(1, 2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetProductCategoriesAsync_ShouldReturn_ResultWithCategoryList() + { + var result = await productService.GetProductCategoriesAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task DeleteProductCategoryAsync_ShouldReturn_ResultWithSuccess() + { + var result = await productService.DeleteProductCategoryAsync(1, 1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task DeleteAllProductCategoriesAsync_ShouldReturn_ResultWithSuccess() + { + var result = await productService.DeleteAllProductCategoriesAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetProductPriceAsync_ShouldReturn_ResultOneProductPrice() + { + var result = await productService.GetProductPriceAsync(2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task GetProductPricesAsync_ShouldReturn_ResultProductPriceList() + { + var result = await productService.GetProductPricesAsync(2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task SearchProductsAsync_ShouldReturn_ResultMatchingProducts() + { + var filter = new ProductFilter + { + Name = "system", + Manufacturer = "techwave", + SerialNumber = "2024", + MinPrice = 10, + MaxPrice = 30 + }; + + var result = await productService.SearchProductsAsync(filter, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task GetProductAsync_ShouldReturn_ResultOneProduct() + { + var result = await productService.GetProductAsync(2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task GetProductsAsync_ShouldReturn_ResultProducts() + { + var range = new DateRange + { + From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 1000 + }; + + var result = await productService.GetProductsAsync(0, range, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task UpdateProductStatusAsync_ShouldResurn_ResultTrue() + { + var result = await productService.UpdateProductStatusAsync(2, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateProductPriceStatusAsync_ShouldReturn_ResultTrue() + { + var result = await productService.UpdateProductPriceStatusAsync(2, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreateProductPriceAsync_Should_Return_NewProductPriceId() + { + var request = new CreateProductPrice + { + Amount = 29.99m, + Discount = 0.00m + }; + + var result = await productService.CreateProductPriceAsync(2, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess, "Product price creation should be successful."); + Assert.True(result.Value > 0, "New ProductPriceId should be greater than 0."); + + output.WriteLine($"Created ProductPriceId: {result.Value}"); + } + + [IntegrationFact] + public async Task CreateProductAsync_Result_Returns_ProductId() + { + var request = new CreateProduct + { + Name = "Systems Rewired", + Description = "[Design], , AND /CHAORS/ IN ***SYNC***", + Summary = "A comprehensive guide to systems thinking and design.", + ImageUrl = "https://bookshop.cdn.khongisa.co.za/design/2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp", + Type = ProductTypes.Book, + Categories = ["Systems Thinking", "Design", "Programming"], + Metadata = new ProductMetadata + { + CopyrightInfo = "© 2024 John Doe. All rights reserved.", + ManufactureDate = "2024-06-01", + Manufacturer = "TechWave Publishing", + SerialNumber = "SR-2024-0001" + } + }; + + var result = await productService.CreateProductAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess, "Product creation should be successful."); + Assert.True(result.Value > 0, "ProductId should be greater than 0."); + + output.WriteLine($"Created ProductId: {result.Value}"); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Abstractions/IMidrandBooks.cs b/LiteCharms.Features.MidrandBooks/Abstractions/IMidrandBooks.cs new file mode 100644 index 0000000..3994e63 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Abstractions/IMidrandBooks.cs @@ -0,0 +1,3 @@ +namespace LiteCharms.Features.MidrandBooks.Abstractions; + +public interface IMidrandBooks; diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs new file mode 100644 index 0000000..4051f8f --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -0,0 +1,167 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.AuthorBooks; + +public sealed class BooksService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Books + .Where(b => b.Id == bookId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(b => b.Enabled, isEnabled) + .SetProperty(b => b.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Book with ID {bookId} not found")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateBookAsync(long authorId, long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken)) + return Result.Fail("Author not found."); + + if (!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken)) + return Result.Fail("Product not found."); + + var book = context.Books.Add(new Entities.AuthorBook + { + CreatedAt = DateTime.UtcNow, + AuthorId = authorId, + ProductId = productId + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(book.Entity.Id) + : Result.Fail("Failed to create book."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetBookByProductIdAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var book = await context.Books + .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product) + .ThenInclude(b => b!.Prices) + .Include(b => b.Pages) + .FirstOrDefaultAsync(b => b.ProductId == productId, cancellationToken); + + return book is null + ? Result.Fail(new Error($"Book with product ID {productId} not found")) + : Result.Ok(book.ToModel()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetBookAsync(long bookId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var book = await context.Books + .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product) + .ThenInclude(b => b!.Prices) + .Include(b => b.Pages) + .FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); + + return book is null + ? Result.Fail(new Error($"Book with ID {bookId} not found")) + : Result.Ok(book.ToModel()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetBooksByAuthorAsync(long authorId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken)) + return Result.Fail(new Error($"Author with ID {authorId} not found")); + + var books = await context.Books + .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product) + .ThenInclude(b => b!.Prices) + .OrderByDescending(b => b.CreatedAt) + .Where(b => b.AuthorId == authorId) + .ToListAsync(cancellationToken); + + return books?.Count > 0 + ? Result.Ok(books.Select(b => b.ToModel()).ToArray()) + : Result.Fail(new Error($"No books found for author with ID {authorId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPublishedBooksAsync(int offset, int limit, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var books = await context.Books + .AsNoTracking() + .Include(b => b.Author) + .Include(b => b.Product) + .ThenInclude(b => b!.Prices) + .Include(b => b.Pages) + .Where(b => b.Enabled && b.Product!.Enabled && b.Author!.Enabled) + .OrderByDescending(b => b.Ranking) + .ThenByDescending(b => b.Ranking) + .ThenByDescending(b => b.CreatedAt) + .ThenByDescending(b => b.UpdatedAt) + .Skip(offset).Take(limit) + .AsSplitQuery() + .ToArrayAsync(cancellationToken); + + return books?.Length > 0 + ? Result.Ok(books.Select(b => b.ToModel()).ToArray()) + : Result.Fail(new Error("No published books found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs new file mode 100644 index 0000000..2b6b906 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBook.cs @@ -0,0 +1,14 @@ +using LiteCharms.Features.MidrandBooks.Authors.Entities; +using LiteCharms.Features.MidrandBooks.Pages.Entities; +using LiteCharms.Features.MidrandBooks.Products.Entities; + +namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +public class AuthorBook : Models.AuthorBook +{ + public virtual Author? Author { get; set; } + + public new virtual Product? Product { get; set; } + + public virtual ICollection Pages { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs new file mode 100644 index 0000000..1bc1259 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Entities/AuthorBookConfiguration.cs @@ -0,0 +1,28 @@ +namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +public sealed class AuthorBookConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Books"); + + builder.HasKey(f => f.AuthorId); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.AuthorId).IsRequired(); + builder.Property(f => f.ProductId).IsRequired(); + builder.Property(f => f.Rating).IsRequired(false); + builder.Property(f => f.Ranking).IsRequired(false); + builder.Property(f => f.Enabled).HasDefaultValue(true); + + builder.HasOne(f => f.Author) + .WithMany(a => a.Books) + .HasForeignKey(f => f.AuthorId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Product) + .WithMany() + .HasForeignKey(f => f.ProductId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs new file mode 100644 index 0000000..6ac8dce --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/Models/AuthorBook.cs @@ -0,0 +1,24 @@ +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models; + +public class AuthorBook +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long AuthorId { get; set; } + + public long ProductId { get; set; } + + public int Rating { get; set; } + + public int Ranking { get; set; } + + public Product? Product { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs new file mode 100644 index 0000000..0b30a85 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/AuthorService.cs @@ -0,0 +1,170 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Authors; + +public sealed class AuthorService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask> GetAuthorByProductIdAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var author = await context.Books + .AsNoTracking() + .Include(i => i.Author) + .Where(b => b.ProductId == productId) + .FirstOrDefaultAsync(cancellationToken); + + if (author is null) + return Result.Fail(new Error($"No author association discovered for Product ID {productId}")); + + return Result.Ok(author.Author!.ToModel()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Authors + .Where(a => a.Id == authorId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(a => a.Enabled, isEnabled) + .SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Author with ID {authorId} not found")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAuthorAsync(long authorId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); + + return author is not null + ? Result.Ok(author.ToModel()) + : Result.Fail(new Error($"Author with ID {authorId} not found")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAuthorsAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var authors = await context.Authors.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .ThenByDescending(o => o.UpdatedAt) + .Where(a => a.CreatedAt >= fromDate && a.CreatedAt <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return authors?.Length > 0 + ? Result.Ok(authors.Select(a => a.ToModel()).ToArray()) + : Result.Fail(new Error("No authors found in the specified date range.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); + + if (author is null) + return Result.Fail(new Error($"Author with ID {authorId} not found")); + + author.UpdatedAt = DateTime.UtcNow; + author.PublisherType = request.PublisherType; + author.Company = request.Company; + author.VatNumber = request.VatNumber; + author.Name = request.Name; + author.LastName = request.LastName; + author.Biography = request.Biography; + author.Email = request.Email; + author.Website = request.Website; + author.ImageUrl = request.ImageUrl; + author.ThumbnailImageUrl = request.ThumbnailImageUrl; + author.SocialMedia = request.SocialMedia; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update author with ID {authorId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateAuthorAsync(CreateAuthor request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken)) + return Result.Fail(new Error($"An author with the name {request.Name} {request.LastName} already exists")); + + if (await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken)) + return Result.Fail(new Error($"An author with the email {request.Email} already exists")); + + var newAuthor = context.Authors.Add(new Entities.Author + { + Company = request.Company, + VatNumber = request.VatNumber, + PublisherType = request.PublisherType, + Name = request.Name, + LastName = request.LastName, + Biography = request.Biography, + Email = request.Email, + Website = request.Website, + ImageUrl = request.ImageUrl, + ThumbnailImageUrl = request.ThumbnailImageUrl, + SocialMedia = request.SocialMedia + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newAuthor.Entity.Id) + : Result.Fail(new Error($"Failed to create author {request.Name} {request.LastName}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs new file mode 100644 index 0000000..28ea8da --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/Author.cs @@ -0,0 +1,9 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +namespace LiteCharms.Features.MidrandBooks.Authors.Entities; + +[EntityTypeConfiguration] +public sealed class Author : Models.Author +{ + public ICollection Books { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs new file mode 100644 index 0000000..8a7de14 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Features.MidrandBooks.Authors.Entities; + +public sealed class AuthorConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Authors"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.PublisherType).IsRequired(); + builder.Property(f => f.VatNumber).IsRequired(false).HasMaxLength(255); + builder.Property(f => f.Name).IsRequired().HasMaxLength(255); + builder.Property(f => f.LastName).IsRequired().HasMaxLength(255); + builder.Property(f => f.Biography).IsRequired(false).HasMaxLength(2048); + builder.Property(f => f.Email).IsRequired().HasMaxLength(512); + builder.Property(f => f.Website).IsRequired(false).HasMaxLength(1024); + builder.Property(f => f.ImageUrl).IsRequired().HasMaxLength(2048); + builder.Property(f => f.ThumbnailImageUrl).IsRequired(false).HasMaxLength(2048); + builder.Property(f => f.Enabled).HasDefaultValue(true); + + builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); }); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs b/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs new file mode 100644 index 0000000..c6080f1 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Models/Author.cs @@ -0,0 +1,36 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Authors.Models; + +public class Author +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public PublisherTypes PublisherType { get; set; } + + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public string? Name { get; set; } + + public string? LastName { get; set; } + + public string? Biography { get; set; } + + public string? Email { get; set; } + + public string? Website { get; set; } + + public string? ImageUrl { get; set; } + + public string? ThumbnailImageUrl { get; set; } + + public ICollection? SocialMedia { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs new file mode 100644 index 0000000..2b6a055 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Authors/Models/Records.cs @@ -0,0 +1,30 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Authors.Models; + +public sealed record UpdateAuthor : CreateAuthor; + +public record CreateAuthor +{ + public required PublisherTypes PublisherType { get; set; } + + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public required string Name { get; set; } + + public required string LastName { get; set; } + + public string? Biography { get; set; } + + public required string Email { get; set; } + + public string? Website { get; set; } + + public required string ImageUrl { get; set; } + + public string? ThumbnailImageUrl { get; set; } + + public SocialMedia[]? SocialMedia { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs new file mode 100644 index 0000000..411f163 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs @@ -0,0 +1,132 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.MidrandBooks.Categories.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Categories; + +public sealed class CategoryService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask UpdateCategoryStatusAsync(long categoryId, bool enabled, bool isMain, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Categories + .Where(c => c.Id == categoryId && c.Enabled) + .ExecuteUpdateAsync(setters => setters + .SetProperty(c => c.Enabled, enabled) + .SetProperty(c => c.IsMain, isMain), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update category")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCategoryAsync(long categoryId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var category = await context.Categories.AsNoTracking().FirstOrDefaultAsync(c => c.Id == categoryId, cancellationToken); + + return category is not null + ? Result.Ok(category.ToModel()) + : Result.Fail("Failed to create new category"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCategoriesAsync(bool? isMain = null, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var query = context.Categories.AsNoTracking() + .OrderByDescending(o => o.IsMain) + .ThenByDescending(o => o.Id) + .ThenBy(o => o.Name) + .AsQueryable(); + + query = isMain is null + ? query.Where(c => c.Enabled).AsQueryable() + : query.Where(c => c.Enabled && c.IsMain == isMain.Value); + + var categories = await query.ToListAsync(cancellationToken); + + return categories?.Count > 0 + ? Result.Ok(categories.Select(c => c.ToModel()).ToArray()) + : Result.Fail("No categories found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask CreateCategoriesAsync(CancellationToken cancellationToken = default, params string[] categories) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + foreach (var category in categories) + { + if (await context.Categories.AnyAsync(c => EF.Functions.ILike(c.Name!, category!), cancellationToken)) + continue; + + context.Categories.Add(new Entities.Category + { + Name = category.Humanize(LetterCasing.Title), + IsMain = false, + Enabled = true, + }); + } + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to add any category in the list"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateCategoryAsync(string category, bool isMain, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Categories.AnyAsync(c => EF.Functions.ILike(c.Name!, category!), cancellationToken)) + return Result.Fail($"Category '{category}' already exists"); + + var newCategory = context.Categories.Add(new Entities.Category + { + Name = StringHumanizeExtensions.Humanize(category, LetterCasing.Title), + IsMain = isMain, + Enabled = true, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newCategory.Entity.Id) + : Result.Fail("Failed to create new category"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Categories/Entities/Category.cs b/LiteCharms.Features.MidrandBooks/Categories/Entities/Category.cs new file mode 100644 index 0000000..5aac931 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/Entities/Category.cs @@ -0,0 +1,4 @@ +namespace LiteCharms.Features.MidrandBooks.Categories.Entities; + +[EntityTypeConfiguration] +public sealed class Category : Models.Category; diff --git a/LiteCharms.Features.MidrandBooks/Categories/Entities/CategoryConfiguration.cs b/LiteCharms.Features.MidrandBooks/Categories/Entities/CategoryConfiguration.cs new file mode 100644 index 0000000..bc5a7bc --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/Entities/CategoryConfiguration.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.MidrandBooks.Categories.Entities; + +public sealed class CategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Categories"); + + builder.HasKey(c => c.Id); + builder.Property(c => c.Name).IsRequired().HasMaxLength(15); + builder.Property(c => c.IsMain).HasDefaultValue(false); + builder.Property(c => c.Enabled).HasDefaultValue(true); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Categories/Models/Category.cs b/LiteCharms.Features.MidrandBooks/Categories/Models/Category.cs new file mode 100644 index 0000000..b793204 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/Models/Category.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.MidrandBooks.Categories.Models; + +public class Category +{ + public long Id { get; set; } + + public string? Name { get; set; } + + public bool IsMain { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs new file mode 100644 index 0000000..0bb50a5 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/CustomerService.cs @@ -0,0 +1,420 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Customers; + +public sealed class CustomerService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask> CreateCustomerAsync(CreateCustomer request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Customers.AnyAsync(c => EF.Functions.ILike(c.Email!, $"%{request.Email}%"), cancellationToken)) + return Result.Fail(new Error($"Customer with email '{request.Email}' already exists.")); + + var customer = context.Customers.Add(new Entities.Customer + { + Company = request.Company, + VatNumber = request.VatNumber, + Email = request.Email, + Website = request.Website, + Phone = request.Phone, + SocialMedia = request.SocialMedia, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(customer.Entity.Id) + : Result.Fail(new Error("Failed to create customer.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateCustomerContactAsync(long customerId, CreateCustomerContact request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + if (await context.Contacts.AnyAsync(cc => cc.CustomerId == customerId && EF.Functions.ILike(cc.Email!, $"%{request.Email}%"), cancellationToken)) + return Result.Fail(new Error($"Contact with email '{request.Email}' already exists for this customer.")); + + var contact = context.Contacts.Add(new Entities.Contact + { + CustomerId = customerId, + Name = request.Name, + Email = request.Email, + Phone = request.Phone, + LastName = request.LastName, + Type = request.Type, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(contact.Entity.Id) + : Result.Fail(new Error("Failed to create customer contact.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateCustomerAddressAsync(long customerId, CreateCustomerAddress request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + var address = context.Addresses.Add(new Entities.Address + { + CustomerId = customerId, + Street = request.Street, + City = request.City, + State = request.State, + PostalCode = request.PostalCode, + Country = request.Country, + Type = request.Type, + Enabled = true, + BuildingType = request.BuildingType, + IsPrimary = request.IsPrimary, + Name = request.Name + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(address.Entity.Id) + : Result.Fail(new Error("Failed to create customer address.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerAsync(long customerId, UpdateCustomer request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); + + if (customer is null) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + customer.UpdatedAt = DateTime.UtcNow; + customer.Company = request.Company; + customer.VatNumber = request.VatNumber; + customer.Email = request.Email; + customer.Website = request.Website; + customer.Phone = request.Phone; + customer.SocialMedia = request.SocialMedia; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to update customer.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerContactAsync(long contactId, UpdateCustomerContact request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Contacts + .Where(cc => cc.Id == contactId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(cc => cc.Name, request.Name) + .SetProperty(cc => cc.LastName, request.LastName) + .SetProperty(cc => cc.Email, request.Email) + .SetProperty(cc => cc.Phone, request.Phone) + .SetProperty(cc => cc.Type, request.Type) + .SetProperty(cc => cc.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerAddressAsync(long addressId, UpdateCustomerAddress request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Addresses + .Where(a => a.Id == addressId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(a => a.Street, request.Street) + .SetProperty(a => a.City, request.City) + .SetProperty(a => a.State, request.State) + .SetProperty(a => a.PostalCode, request.PostalCode) + .SetProperty(a => a.Country, request.Country) + .SetProperty(a => a.Type, request.Type) + .SetProperty(a => a.BuildingType, request.BuildingType) + .SetProperty(a => a.IsPrimary, request.IsPrimary) + .SetProperty(a => a.Name, request.Name) + .SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerStatusAsync(long customerId, bool enabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Customers + .Where(c => c.Id == customerId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(c => c.Enabled, enabled) + .SetProperty(c => c.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerContactStatusAsync(long contactId, bool enabled, bool isPrimary, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Contacts + .Where(cc => cc.Id == contactId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(cc => cc.Enabled, enabled) + .SetProperty(cc => cc.IsPrimary, isPrimary) + .SetProperty(cc => cc.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerAddressStatusAsync(long addressId, bool enabled, bool isPrimary, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Addresses + .Where(a => a.Id == addressId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(a => a.Enabled, enabled) + .SetProperty(a => a.IsPrimary, isPrimary) + .SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Address with ID '{addressId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomersAsync(CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customers = await context.Customers + .AsNoTracking() + .Include(c => c.Contacts) + .Include(c => c.Addresses) + .OrderByDescending(c => c.CreatedAt) + .ThenByDescending(c => c.UpdatedAt) + .AsSplitQuery() + .ToListAsync(cancellationToken); + + return customers?.Count > 0 + ? Result.Ok(customers.Select(c => c.ToModel()).ToArray()) + : Result.Fail(new Error("No customers found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerContactsAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + var contacts = await context.Contacts + .AsNoTracking() + .Where(cc => cc.CustomerId == customerId) + .OrderByDescending(cc => cc.CreatedAt) + .ThenByDescending(cc => cc.UpdatedAt) + .ToListAsync(cancellationToken); + + return contacts?.Count > 0 + ? Result.Ok(contacts.Select(cc => cc.ToModel()).ToArray()) + : Result.Fail(new Error("No contacts found for the specified customer.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAddressesAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + + var addresses = await context.Addresses + .AsNoTracking() + .Where(a => a.CustomerId == customerId) + .OrderByDescending(a => a.CreatedAt) + .ThenByDescending(a => a.UpdatedAt) + .ToListAsync(cancellationToken); + + return addresses?.Count > 0 + ? Result.Ok(addresses.Select(a => a.ToModel()).ToArray()) + : Result.Fail(new Error($"No addresses found for customer with ID '{customerId}'.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAsync(string email, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers + .AsNoTracking() + .Include(c => c.Contacts) + .Include(c => c.Addresses) + .FirstOrDefaultAsync(c => c.Email == email, cancellationToken); + + return customer is not null + ? Result.Ok(customer.ToModel()) + : Result.Fail(new Error($"Customer with email '{email}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers + .AsNoTracking() + .Include(c => c.Contacts) + .Include(c => c.Addresses) + .FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); + + return customer is not null + ? Result.Ok(customer.ToModel()) + : Result.Fail(new Error($"Customer with ID '{customerId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerContactAsync(long contactId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var contact = await context.Contacts + .AsNoTracking() + .FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken); + + return contact is not null + ? Result.Ok(contact.ToModel()) + : Result.Fail(new Error($"Contact with ID '{contactId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAddressAsync(long addressId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var address = await context.Addresses + .AsNoTracking() + .FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); + + return address is not null + ? Result.Ok(address.ToModel()) + : Result.Fail
(new Error($"Address with ID '{addressId}' does not exist.")); + } + catch (Exception ex) + { + return Result.Fail
(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/Address.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/Address.cs new file mode 100644 index 0000000..d54f7cd --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/Address.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +[EntityTypeConfiguration] +public class Address : Models.Address +{ + public virtual Customer? Customer { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs new file mode 100644 index 0000000..4934bac --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/AddressConfiguration.cs @@ -0,0 +1,29 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +public sealed class AddressConfiguration : IEntityTypeConfiguration
+{ + public void Configure(EntityTypeBuilder
builder) + { + builder.ToTable("Addresses"); + + builder.HasKey(a => a.Id); + builder.Property(a => a.CustomerId).IsRequired(); + builder.Property(a => a.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(a => a.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(a => a.Name).IsRequired(); + builder.Property(a => a.Type).IsRequired(); + builder.Property(a => a.BuildingType).IsRequired(); + builder.Property(a => a.Street).IsRequired(); + builder.Property(a => a.City).IsRequired(); + builder.Property(a => a.State).IsRequired(); + builder.Property(a => a.PostalCode).IsRequired(); + builder.Property(a => a.Country).IsRequired(); + builder.Property(a => a.IsPrimary).HasDefaultValue(false); + builder.Property(a => a.Enabled).HasDefaultValue(true); + + builder.HasOne(a => a.Customer) + .WithMany(c => c.Addresses) + .HasForeignKey(a => a.CustomerId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/Contact.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/Contact.cs new file mode 100644 index 0000000..3d5509e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/Contact.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +[EntityTypeConfiguration] +public class Contact : Models.Contact +{ + public virtual Customer? Customer { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs new file mode 100644 index 0000000..69c931c --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/ContactConfiguration.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +public sealed class ContactConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Contacts"); + + builder.HasKey(c => c.Id); + builder.Property(c => c.CustomerId).IsRequired(); + builder.Property(c => c.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(c => c.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(c => c.Name).IsRequired(); + builder.Property(c => c.LastName).IsRequired(); + builder.Property(c => c.Type).IsRequired(); + builder.Property(c => c.Phone).IsRequired(); + builder.Property(c => c.Email).IsRequired(); + builder.Property(c => c.IsPrimary).HasDefaultValue(false); + builder.Property(c => c.Enabled).HasDefaultValue(true); + + builder.HasOne(c => c.Customer) + .WithMany(c => c.Contacts) + .HasForeignKey(c => c.CustomerId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/Customer.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/Customer.cs new file mode 100644 index 0000000..4443225 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/Customer.cs @@ -0,0 +1,9 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +[EntityTypeConfiguration] +public class Customer : Models.Customer +{ + public virtual ICollection Contacts { get; set; } = []; + + public virtual ICollection
Addresses { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs new file mode 100644 index 0000000..facc079 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs @@ -0,0 +1,21 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Entities; + +public sealed class CustomerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Customers"); + + builder.HasKey(c => c.Id); + builder.Property(c => c.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(c => c.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(c => c.Company).IsRequired(false); + builder.Property(c => c.VatNumber).IsRequired(false); + builder.Property(c => c.Email).IsRequired(); + builder.Property(c => c.Phone).IsRequired(false); + builder.Property(c => c.Website).IsRequired(false); + builder.Property(c => c.Enabled).HasDefaultValue(true); + + builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); }); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Address.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Address.cs new file mode 100644 index 0000000..35c7816 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Address.cs @@ -0,0 +1,32 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public class Address +{ + public long Id { get; set; } + + public long CustomerId { get; set; } + + public string? Name { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public AddressType Type { get; set; } + + public AddressBuildingTypes BuildingType { get; set; } + + public string? Street { get; set; } + + public string? City { get; set; } + + public string? State { get; set; } + + public string? PostalCode { get; set; } + + public string? Country { get; set; } + + public bool IsPrimary { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Contact.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Contact.cs new file mode 100644 index 0000000..7eed90e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Contact.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public class Contact +{ + public long Id { get; set; } + + public long CustomerId { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public ContactTypes Type { get; set; } + + public string? Name { get; set; } + + public string? LastName { get; set; } + + public string? Email { get; set; } + + public string? Phone { get; set; } + + public bool IsPrimary { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs new file mode 100644 index 0000000..9cf8f7e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Customer.cs @@ -0,0 +1,26 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public class Customer +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public string? Email { get; set; } + + public string? Website { get; set; } + + public string? Phone { get; set; } + + public ICollection? SocialMedia { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs new file mode 100644 index 0000000..0383187 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Customers/Models/Records.cs @@ -0,0 +1,60 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Customers.Models; + +public record CreateCustomer +{ + public string? Company { get; set; } + + public string? VatNumber { get; set; } + + public string? Email { get; set; } + + public string? Website { get; set; } + + public string? Phone { get; set; } + + public ICollection? SocialMedia { get; set; } +} + +public sealed record UpdateCustomer : CreateCustomer; + +public record CreateCustomerContact +{ + public ContactTypes Type { get; set; } + + public string? Name { get; set; } + + public string? LastName { get; set; } + + public string? Email { get; set; } + + public string? Phone { get; set; } +} + +public sealed record UpdateCustomerContact : CreateCustomerContact; + +public record CreateCustomerAddress +{ + public string? Name { get; set; } + + public AddressType Type { get; set; } + + public AddressBuildingTypes BuildingType { get; set; } + + public string? Street { get; set; } + + public string? City { get; set; } + + public string? State { get; set; } + + public string? PostalCode { get; set; } + + public string? Country { get; set; } + + public bool IsPrimary { get; set; } + + public bool Enabled { get; set; } +} + +public sealed record UpdateCustomerAddress : CreateCustomerAddress; \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs b/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs new file mode 100644 index 0000000..bf35ee8 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Extensions/HealthChecks.cs @@ -0,0 +1,30 @@ +using LiteCharms.Features.MidrandBooks.HealthChecks; +using static LiteCharms.Features.Extensions.Postgres; +using static LiteCharms.Features.MidrandBooks.Extensions.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Extensions; + +public static class HealthChecks +{ + public static IServiceCollection AddMidrandShopQuartzHealthCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck(SchedulerDbConfigName); + + return services; + } + + public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck(MidrandBooksDbConfigName); + + return services; + } + + public static IServiceCollection AddHealthChecksSupport(this IServiceCollection services, IConfiguration configuration) + { + services.AddHealthChecks() + .AddCheck("Self", () => HealthCheckResult.Healthy()); + + return services; + } +} diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs new file mode 100644 index 0000000..467d096 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -0,0 +1,258 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Categories.Models; +using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.MidrandBooks.Orders.Models; +using LiteCharms.Features.MidrandBooks.Pages.Models; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.Extensions; + +public static class Mappers +{ + public static PaymentGatewayLedger ToModel(this Payments.Entities.PaymentGatewayLedger entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CustomerEmail = entity.CustomerEmail, + OrderId = entity.OrderId, + PaymentId = entity.PaymentId, + MerchantPaymentId = entity.MerchantPaymentId, + PayfastPaymentId = entity.PayfastPaymentId, + PaymentStatus = entity.PaymentStatus, + AmountGross = entity.AmountGross, + AmountFee = entity.AmountFee, + AmountNet = entity.AmountNet + }; + + public static Refund ToModel(this Payments.Entities.Refund entity) => new() + { + CreatedAt = entity.CreatedAt, + Amount = entity.Amount, + Id = entity.Id, + OrderId = entity.OrderId, + Reason = entity.Reason, + Status = entity.Status, + Type = entity.Type, + UpdatedAt = entity.UpdatedAt, + }; + + public static PaymentLedger ToModel(this Payments.Entities.PaymentLedger entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + CustomerId = entity.CustomerId, + OrderId = entity.OrderId, + PaymentId = entity.PaymentId, + Status = entity.Status, + MerchantPaymentId = entity.MerchantPaymentId, + }; + + public static PaymentGateway ToModel(this Payments.Entities.PaymentGateway entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Enabled = entity.Enabled, + IsSandbox = entity.IsSandbox, + MerchantId = entity.MerchantId, + MerchantKey = entity.MerchantKey, + Name = entity.Name, + Website = entity.Website, + }; + + public static Payment ToModel(this Payments.Entities.Payment entity) => new() + { + Id = entity.Id, + Amount = entity.Amount, + CreatedAt = entity.CreatedAt, + OrderId = entity.OrderId, + Reference = entity.Reference, + Status = entity.Status, + UpdatedAt = entity.UpdatedAt, + }; + + public static ProductInventory ToModel(this Products.Entities.ProductInventory entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + ProductId = entity.ProductId, + ProductPriceId = entity.ProductPriceId, + Status = entity.Status, + TotalAllocated = entity.TotalAllocated, + TotalReserved = entity.TotalReserved, + }; + + public static Category ToModel(this Categories.Entities.Category entity) => new() + { + Id = entity.Id, + Name = entity.Name, + IsMain = entity.IsMain, + Enabled = entity.Enabled, + }; + + public static ShippingProvider ToModel(this Orders.Entities.ShippingProvider entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Name = entity.Name, + Type = entity.Type, + Price = entity.Price, + Enabled = entity.Enabled, + TrackingUrl = entity.TrackingUrl, + }; + + public static Shipping ToModel(this Orders.Entities.Shipping entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + OrderId = entity.OrderId, + AddressId = entity.AddressId, + Status = entity.Status, + TrackingNumber = entity.TrackingNumber + }; + + public static OrderItem ToModel(this Orders.Entities.OrderItem entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + OrderId = entity.OrderId, + Quantity = entity.Quantity, + AuthorBookId = entity.AuthorBookId, + ProductPriceId = entity.ProductPriceId + }; + + public static Order ToModel(this Orders.Entities.Order entity) => new() + { + Id = entity.Id, + CustomerId = entity.CustomerId, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Status = entity.Status, + Total = entity.Total, + InvoiceUrl = entity.InvoiceUrl, + Notes = entity.Notes + }; + + public static Customer ToModel(this Customers.Entities.Customer entiry) => new() + { + Id = entiry.Id, + Company = entiry.Company, + CreatedAt = entiry.CreatedAt, + Email = entiry.Email, + Enabled = entiry.Enabled, + Phone = entiry.Phone, + SocialMedia = entiry.SocialMedia, + UpdatedAt = entiry.UpdatedAt, + VatNumber = entiry.VatNumber, + Website = entiry.Website + }; + + public static Address ToModel(this Customers.Entities.Address entity) => new() + { + Id = entity.Id, + BuildingType = entity.BuildingType, + CreatedAt = entity.CreatedAt, + CustomerId = entity.CustomerId, + Enabled = entity.Enabled, + IsPrimary = entity.IsPrimary, + Name = entity.Name, + PostalCode = entity.PostalCode, + Type = entity.Type, + UpdatedAt = entity.UpdatedAt, + Street = entity.Street, + City = entity.City, + State = entity.State, + Country = entity.Country + }; + + public static Contact ToModel(this Customers.Entities.Contact entity) => new() + { + Id = entity.Id, + Type = entity.Type, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + CustomerId = entity.CustomerId, + Email = entity.Email, + Enabled = entity.Enabled, + LastName = entity.LastName, + Name = entity.Name, + Phone = entity.Phone + }; + + public static BookPage ToModel(this Pages.Entities.BookPage entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + AuthorBookId = entity.AuthorBookId, + Content = entity.Content, + ContentType = entity.ContentType, + Number = entity.Number, + Enabled = entity.Enabled, + Notes = entity.Notes, + References = entity.References, + Type = entity.Type + }; + + public static AuthorBook ToModel(this AuthorBooks.Entities.AuthorBook entity) => new() + { + Id = entity.Id, + ProductId = entity.ProductId, + CreatedAt = entity.CreatedAt, + AuthorId = entity.AuthorId, + Ranking = entity.Ranking, + Rating = entity.Rating, + Enabled = entity.Enabled, + Product = entity.Product?.ToModel(), + }; + + public static ProductPrice ToModel(this Products.Entities.ProductPrice entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + ProductId = entity.ProductId, + Amount = entity.Amount, + Discount = entity.Discount, + Enabled = entity.Enabled + }; + + public static Product ToModel(this Products.Entities.Product entity) => new Product + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Name = entity.Name, + Summary = entity.Summary, + Description = entity.Description, + Type = entity.Type, + ImageUrl = entity.ImageUrl, + ThumbnailUrls = entity.ThumbnailUrls, + Metadata = entity.Metadata, + Enabled = entity.Enabled, + Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null, + }; + + public static Author ToModel(this Authors.Entities.Author entity) => new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Name = entity.Name, + LastName = entity.LastName, + Biography = entity.Biography, + Email = entity.Email, + Website = entity.Website, + ImageUrl = entity.ImageUrl, + ThumbnailImageUrl = entity.ThumbnailImageUrl, + SocialMedia = entity.SocialMedia, + Enabled = entity.Enabled, + Company = entity.Company, + PublisherType = entity.PublisherType, + VatNumber = entity.VatNumber + }; +} diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs new file mode 100644 index 0000000..54307dc --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs @@ -0,0 +1,26 @@ +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Extensions; + +public static class Postgres +{ + public const string MidrandBooksDbConfigName = "PostgresMidrandBooks"; + + public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName); + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); + + dataSourceBuilder.ConfigureTypeLoading(options => { options.EnableTypeLoading(false); }); + + var dataSource = dataSourceBuilder.Build(); + + services.AddSingleton(dataSource); + + services.AddPooledDbContextFactory(options => + options.UseNpgsql(dataSource)); + + return services; + } +} diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs new file mode 100644 index 0000000..892c282 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -0,0 +1,28 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Browser; +using LiteCharms.Features.MidrandBooks.Abstractions; + +namespace LiteCharms.Features.MidrandBooks.Extensions; + +public static class Shop +{ + public static IServiceCollection AddShopServices(this IServiceCollection services, bool includeLocalStorage = false) + { + var serviceType = typeof(IService); + + var sharedImplementations = typeof(IFeatures).Assembly.GetTypes() + .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); + + foreach (var sharedImplementation in sharedImplementations) services.AddScoped(sharedImplementation); + + var coreImplementations = typeof(IMidrandBooks).Assembly.GetTypes() + .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); + + foreach (var coreImplementation in coreImplementations) services.AddScoped(coreImplementation); + + if (includeLocalStorage) + services.AddScoped(); + + return services; + } +} diff --git a/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs b/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs new file mode 100644 index 0000000..0de8562 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/HealthChecks/MidrandShopQuartzHealthCheck.cs @@ -0,0 +1,28 @@ +using static LiteCharms.Features.Extensions.Quartz; + +namespace LiteCharms.Features.MidrandBooks.HealthChecks; + +public sealed class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck +{ + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var scheduler = await schedulerFactory.GetScheduler(MidrandShopSchedulerName, cancellationToken); + + if(scheduler == null) + return HealthCheckResult.Unhealthy($"Scheduler with name '{MidrandShopSchedulerName}' not found."); + + if (!scheduler.IsStarted) + return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler is not running"); + + await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken); + + return HealthCheckResult.Healthy($"{MidrandShopSchedulerName} Quartz scheduler is ready"); + } + catch (SchedulerException) + { + return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler cannot connect to the store"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs new file mode 100644 index 0000000..dba8e72 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/HealthChecks/PostgresMidrandShopHealthCheck.cs @@ -0,0 +1,28 @@ +using static LiteCharms.Features.MidrandBooks.Extensions.Postgres; + +namespace LiteCharms.Features.MidrandBooks.HealthChecks; + +public sealed class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck +{ + private readonly string connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName)!; + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + await using var dataSource = NpgsqlDataSource.Create(connectionString); + await using var connection = await dataSource.OpenConnectionAsync(cancellationToken); + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT 1"; + + await command.ExecuteScalarAsync(cancellationToken); + + return HealthCheckResult.Healthy($"{MidrandBooksDbConfigName} is responsive."); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy($"{MidrandBooksDbConfigName} is unreachable.", ex); + } + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj new file mode 100644 index 0000000..022093b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -0,0 +1,173 @@ + + + + net10.0 + enable + enable + True + ..\LiteCharms.snk + 5be62f49-3ed0-4468-884e-1b04e048b45a + + + + + LiteCharms.Features.MidrandBooks + 1.0.20 + Khwezi Mngoma + Lite Charms (PTY) Ltd + MidrandBooks feature components for Lite Charms applications. + https://gitea.khongisa.co.za/litecharms/components + https://gitea.khongisa.co.za/litecharms/components.git + git + LICENSE + utility;dotnet + icon.png + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs new file mode 100644 index 0000000..657af6f --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/Order.cs @@ -0,0 +1,13 @@ +using LiteCharms.Features.MidrandBooks.Payments.Entities; + +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +[EntityTypeConfiguration] +public class Order : Models.Order +{ + public virtual Shipping? Shipping { get; set; } + + public virtual ICollection OrderItems { get; set; } = []; + + public virtual ICollection Refunds { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs new file mode 100644 index 0000000..629b030 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs @@ -0,0 +1,17 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public sealed class OrderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Orders"); + + builder.HasKey(o => o.Id); + builder.Property(o => o.CustomerId).IsRequired(); + builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(o => o.Status).IsRequired(); + builder.Property(o => o.Total).IsRequired().HasPrecision(18, 2); + builder.Property(o => o.Notes).HasMaxLength(1000); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItem.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItem.cs new file mode 100644 index 0000000..d255a04 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItem.cs @@ -0,0 +1,14 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; +using LiteCharms.Features.MidrandBooks.Products.Entities; + +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +[EntityTypeConfiguration] +public class OrderItem : Models.OrderItem +{ + public virtual Order? Order { get; set; } + + public virtual AuthorBook? AuthorBook { get; set; } + + public virtual ProductPrice? ProductPrice { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs new file mode 100644 index 0000000..5a82a1a --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderItemConfiguration.cs @@ -0,0 +1,31 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public sealed class OrderItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("OrderItems"); + + builder.HasKey(oi => oi.Id); + builder.Property(oi => oi.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(oi => oi.OrderId).IsRequired(); + builder.Property(oi => oi.AuthorBookId).IsRequired(); + builder.Property(oi => oi.ProductPriceId).IsRequired(); + builder.Property(oi => oi.Quantity).IsRequired(); + + builder.HasOne(oi => oi.Order) + .WithMany(o => o.OrderItems) + .HasForeignKey(oi => oi.OrderId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(oi => oi.AuthorBook) + .WithMany() + .HasForeignKey(oi => oi.AuthorBookId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(oi => oi.ProductPrice) + .WithMany() + .HasForeignKey(oi => oi.ProductPriceId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/Shipping.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/Shipping.cs new file mode 100644 index 0000000..d8f9e07 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/Shipping.cs @@ -0,0 +1,13 @@ +using LiteCharms.Features.MidrandBooks.Customers.Entities; + +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +[EntityTypeConfiguration] +public class Shipping : Models.Shipping +{ + public virtual Order? Order { get; set; } + + public virtual Address? Address { get; set; } + + public virtual ShippingProvider? ShippingProvider { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs new file mode 100644 index 0000000..5938ff2 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingConfiguration.cs @@ -0,0 +1,33 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public sealed class ShippingConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Shippings"); + + builder.HasKey(s => s.Id); + builder.Property(s => s.OrderId).IsRequired(); + builder.Property(s => s.AddressId).IsRequired(); + builder.Property(s => s.ShippingProviderId).IsRequired(); + builder.Property(s => s.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(s => s.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(s => s.Status).IsRequired(); + builder.Property(s => s.TrackingNumber).HasMaxLength(255); + + builder.HasOne(s => s.Order) + .WithOne(o => o.Shipping) + .HasForeignKey(s => s.OrderId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(s => s.Address) + .WithMany() + .HasForeignKey(s => s.AddressId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.ShippingProvider) + .WithMany(f => f.Shippings) + .HasForeignKey(f => f.ShippingProviderId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProvider.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProvider.cs new file mode 100644 index 0000000..1fc3127 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProvider.cs @@ -0,0 +1,6 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public class ShippingProvider : Models.ShippingProvider +{ + public virtual ICollection Shippings { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs new file mode 100644 index 0000000..83b4755 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/ShippingProviderConfiguration.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Entities; + +public sealed class ShippingProviderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ShippingProviders"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.Type).IsRequired(); + builder.Property(f => f.Name).IsRequired().HasMaxLength(100); + builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Enabled).HasDefaultValue(true); + builder.Property(f => f.TrackingUrl).HasMaxLength(200); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Order.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Order.cs new file mode 100644 index 0000000..a86c03e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Order.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Models; + +public class Order +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long CustomerId { get; set; } + + public OrderStatus Status { get; set; } + + public decimal Total { get; set; } + + public string? Notes { get; set; } + + public string? InvoiceUrl { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/OrderItem.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/OrderItem.cs new file mode 100644 index 0000000..424ad4f --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/OrderItem.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Models; + +public class OrderItem +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public long OrderId { get; set; } + + public long AuthorBookId { get; set; } + + public long ProductPriceId { get; set; } + + public int Quantity { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs new file mode 100644 index 0000000..6ffccb4 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Records.cs @@ -0,0 +1,11 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Models; + +public sealed record CreateOrder(decimal TotalPrice, string? Notes); + +public sealed record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity); + +public sealed record CreateShipping(long AddressId, long ShippingProviderId, string? TrackingNumber = null); + +public sealed record CreateShippingProvider(ShippingProviderTypes Type, string Name, decimal Price, string TrackingUrl); + +public sealed record UpdateShippingProvider(long ProviderId, bool Enabled, string Name, decimal Price, string TrackingUrl); diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs new file mode 100644 index 0000000..51491fe --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/Shipping.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Models; + +public class Shipping +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long OrderId { get; set; } + + public long AddressId { get; set; } + + public long ShippingProviderId { get; set; } + + public string? TrackingNumber { get; set; } + + public ShippingStatuses Status { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs b/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs new file mode 100644 index 0000000..a26d934 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/Models/ShippingProvider.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Orders.Models; + +public class ShippingProvider +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public ShippingProviderTypes Type { get; set; } + + public string? Name { get; set; } + + public decimal? Price { get; set; } + + public string? TrackingUrl { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs new file mode 100644 index 0000000..e94cded --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Orders/OrderService.cs @@ -0,0 +1,483 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Orders.Models; +using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Orders; + +public sealed class OrderService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask> CreateOrderAsync(long customerId, CreateOrder request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail("Customer not found."); + + var order = context.Orders.Add(new Entities.Order + { + CustomerId = customerId, + Status = OrderStatus.Pending, + Total = request.TotalPrice + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(order.Entity.Id) + : Result.Fail("Failed to create order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> AddItemToOrderAsync(long orderId, CreateOrderItem request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) + return Result.Fail("Order not found."); + + if(!await context.Books.AnyAsync(ab => ab.Id == request.AuthorBookId, cancellationToken)) + return Result.Fail("Author book not found."); + + if (!await context.Prices.AnyAsync(pp => pp.Id == request.ProductPriceId, cancellationToken)) + return Result.Fail("Product price not found."); + + var existingItem = await context.OrderItems.FirstOrDefaultAsync(i => i.ProductPriceId == request.ProductPriceId && i.OrderId == orderId, cancellationToken); + + if(existingItem is not null) + { + existingItem.Quantity += request.Quantity; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(existingItem.Id) + : Result.Fail("Update existing order item."); + } + + var orderItem = context.OrderItems.Add(new Entities.OrderItem + { + OrderId = orderId, + AuthorBookId = request.AuthorBookId, + ProductPriceId = request.ProductPriceId, + Quantity = request.Quantity + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(orderItem.Entity.Id) + : Result.Fail("Failed to add item to order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask AddItemsToOrderAsync(long orderId, CreateOrderItem[] items, CancellationToken cancellationToken = default) + { + try + { + if(items.Length == 0) + return Result.Fail("No items to add."); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) + return Result.Fail("Order not found."); + + foreach (var item in items) + { + if (!await context.Books.AnyAsync(ab => ab.Id == item.AuthorBookId, cancellationToken)) + return Result.Fail($"Author book with ID {item.AuthorBookId} not found."); + + if (!await context.Prices.AnyAsync(pp => pp.Id == item.ProductPriceId, cancellationToken)) + return Result.Fail($"Product price with ID {item.ProductPriceId} not found."); + + var existingItem = await context.OrderItems.FirstOrDefaultAsync(i => i.ProductPriceId == item.ProductPriceId && i.OrderId == orderId, cancellationToken); + + if (existingItem is not null) + existingItem.Quantity += item.Quantity; + else + context.OrderItems.Add(new Entities.OrderItem + { + OrderId = orderId, + AuthorBookId = item.AuthorBookId, + ProductPriceId = item.ProductPriceId, + Quantity = item.Quantity + }); + + await context.SaveChangesAsync(cancellationToken); + } + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask RemoveItemFromOrderAsync(long orderId, long orderItemId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.OrderItems + .Where(oi => oi.Id == orderItemId && oi.OrderId == orderId) + .ExecuteDeleteAsync(cancellationToken); + + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("Order item not found or failed to remove."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask ClearOrderItemsAsync(long orderId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var deletedItems = await context.OrderItems.Where(oi => oi.OrderId == orderId) + .ExecuteDeleteAsync(cancellationToken); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to clear order items."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask CancelOrderAsync(long orderId, CancellationToken cancellationToken = default) => + await UpdateOrderStatusAsync(orderId, OrderStatus.Cancelled, cancellationToken); + + public async ValueTask> GetPendingOrderAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.AsNoTracking() + .Where(o => o.Status == OrderStatus.Pending && o.CustomerId == customerId) + .OrderByDescending(o => o.Id) + .FirstOrDefaultAsync(cancellationToken); + + return order is not null + ? Result.Ok(order.ToModel()) + : Result.Fail("Order not found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrderAsync(long orderId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.AsNoTracking().FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); + + return order is not null + ? Result.Ok(order.ToModel()) + : Result.Fail("Order not found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrdersByCustomerAsync(long customerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail("Customer not found."); + + var orders = await context.Orders + .AsNoTracking() + .Where(o => o.CustomerId == customerId) + .ToListAsync(cancellationToken); + + return Result.Ok(orders.Select(o => o.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrdersAsync(DateRange range, int index, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var orders = await context.Orders + .AsNoTracking() + .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) + .Skip(index).Take(range.MaxRecords) + .ToListAsync(cancellationToken); + + return Result.Ok(orders.Select(o => o.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateOrderStatusAsync(long orderId, OrderStatus newStatus, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Orders + .Where(o => o.Id == orderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(o => o.Status, newStatus) + .SetProperty(o => o.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail("Order not found or status update failed."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) + return Result.Fail("Order not found."); + + if(!await context.Addresses.AnyAsync(a => a.Id == request.AddressId, cancellationToken)) + return Result.Fail("Address not found."); + + if(!await context.ShippingProviders.AnyAsync(sp => sp.Id == request.ShippingProviderId && sp.Enabled, cancellationToken)) + return Result.Fail("Shipping provider not found or disabled."); + + if(await context.Shippings.AnyAsync(s => s.OrderId == orderId, cancellationToken)) + return Result.Fail("Shipping already exists for this order."); + + var shipping = context.Shippings.Add(new Entities.Shipping + { + OrderId = orderId, + AddressId = request.AddressId, + ShippingProviderId = request.ShippingProviderId, + Status = ShippingStatuses.Pending, + TrackingNumber = request.TrackingNumber + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to add shipping to order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateShippingStatusAsync(long orderId, ShippingStatuses newStatus, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Shippings + .Where(s => s.OrderId == orderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(s => s.Status, newStatus) + .SetProperty(s => s.UpdatedAt, DateTime.UtcNow), + cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail("Shipping not found for this order or status update failed."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task> GetShippingByOrderIdAsync(long orderId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var shipping = await context.Shippings + .AsNoTracking() + .FirstOrDefaultAsync(s => s.OrderId == orderId, cancellationToken); + + return shipping is not null + ? Result.Ok(shipping.ToModel()) + : Result.Fail("Shipping not found for this order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task RemoveShippingFromOrderAsync(long orderId, long shippingId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.Shippings + .Where(s => s.Id == shippingId && s.OrderId == orderId) + .ExecuteDeleteAsync(cancellationToken); + + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("Shipping record not found for this order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateShippingTrackingNumberAsync(long orderId, long shippingId, string trackingNumber, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Shippings + .Where(s => s.Id == shippingId && s.OrderId == orderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(s => s.TrackingNumber, trackingNumber) + .SetProperty(s => s.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail("Shipping record not found for this order."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(await context.ShippingProviders.AnyAsync(sp => sp.Type == request.Type, cancellationToken)) + return Result.Fail("Shipping provider with the same type already exists."); + + var shippingProvider = context.ShippingProviders.Add(new Entities.ShippingProvider + { + CreatedAt = DateTime.UtcNow, + Name = request.Name, + Type = request.Type, + Price = request.Price, + TrackingUrl = request.TrackingUrl + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(shippingProvider.Entity.Id) + : Result.Fail("Failed to create shipping provider."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShippingProvidersAsync(bool isEnabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var providers = await context.ShippingProviders.AsNoTracking().Where(sp => sp.Enabled == isEnabled) + .ToListAsync(cancellationToken); + + return Result.Ok(providers.Select(sp => sp.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShippingProviderAsync(long providerId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var provider = await context.ShippingProviders.AsNoTracking() + .FirstOrDefaultAsync(sp => sp.Id == providerId, cancellationToken); + + return provider is not null + ? Result.Ok(provider.ToModel()) + : Result.Fail("Shipping provider not found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateShippingProviderAsync(UpdateShippingProvider request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.ShippingProviders + .Where(sp => sp.Id == request.ProviderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(sp => sp.Name, request.Name) + .SetProperty(sp => sp.Price, request.Price) + .SetProperty(sp => sp.TrackingUrl, request.TrackingUrl) + .SetProperty(sp => sp.Enabled, request.Enabled) + .SetProperty(sp => sp.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail("Shipping provider not found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPage.cs new file mode 100644 index 0000000..bba4436 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPage.cs @@ -0,0 +1,9 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; + +namespace LiteCharms.Features.MidrandBooks.Pages.Entities; + +[EntityTypeConfiguration] +public class BookPage : Models.BookPage +{ + public virtual AuthorBook Book { get; set; } = new(); +} diff --git a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs new file mode 100644 index 0000000..eb33f55 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs @@ -0,0 +1,27 @@ +namespace LiteCharms.Features.MidrandBooks.Pages.Entities; + +public sealed class BookPageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("BookPages"); + + builder.HasKey(bp => bp.Id); + builder.Property(bp => bp.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(bp => bp.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(bp => bp.Number).IsRequired().HasDefaultValue(0); + builder.Property(bp => bp.AuthorBookId).IsRequired(); + builder.Property(bp => bp.Content).IsRequired(); + builder.Property(bp => bp.Type).IsRequired(); + builder.Property(bp => bp.ContentType).IsRequired(); + builder.Property(bp => bp.Enabled).HasDefaultValue(true); + builder.Property(bp => bp.Notes).IsRequired(false); + + builder.OwnsMany(f => f.References, b => { b.ToJson(); }); + + builder.HasOne(f =>f.Book) + .WithMany(b => b.Pages) + .HasForeignKey(f => f.AuthorBookId) + .OnDelete(DeleteBehavior.NoAction); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs new file mode 100644 index 0000000..c9d082d --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/BookPage.cs @@ -0,0 +1,28 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Pages.Models; + +public class BookPage +{ + public long Id { get; set; } + + public long AuthorBookId { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public BookPageTypes Type { get; set; } + + public BookContentTypes ContentType { get; set; } + + public int Number { get; set; } + + public byte[]? Content { get; set; } + + public string[]? Notes { get; set; } + + public ICollection? References { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Pages/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Pages/Models/Records.cs new file mode 100644 index 0000000..5af145a --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/Models/Records.cs @@ -0,0 +1,20 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Pages.Models; + +public class CreateBookPage +{ + public BookPageTypes Type { get; set; } + + public BookContentTypes ContentType { get; set; } + + public int Number { get; set; } + + public byte[]? Content { get; set; } + + public string[]? Notes { get; set; } + + public ICollection? References { get; set; } +} + +public sealed class UpdateBookPage : CreateBookPage; \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks/Pages/PageService.cs b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs new file mode 100644 index 0000000..22db7b7 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Pages/PageService.cs @@ -0,0 +1,210 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Pages.Models; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Pages; + +public sealed class PageService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask DeleteAllAsync(long authorBookId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.Pages + .Where(p => p.AuthorBookId == authorBookId) + .ExecuteDeleteAsync(cancellationToken); + + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("No pages found for the specified book"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeleteByPageTypeAsync(long authorBookId, int pageNumber, BookPageTypes pageType, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.Pages + .Where(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType) + .ExecuteDeleteAsync(cancellationToken); + + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("Page not found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdatePageStatusAsync(long bookPageId, bool enabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Pages + .Where(p => p.Id == bookPageId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Enabled, enabled) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail("Page not found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeletePageAsync(long bookPageId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.Pages + .Where(p => p.Id == bookPageId) + .ExecuteDeleteAsync(cancellationToken); + + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("Page not found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdatePageAsync(long bookPageId, UpdateBookPage request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Pages + .Where(p => p.Id == bookPageId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Type, request.Type) + .SetProperty(p => p.ContentType, request.ContentType) + .SetProperty(p => p.Number, request.Number) + .SetProperty(p => p.Content, request.Content) + .SetProperty(p => p.Notes, request.Notes) + .SetProperty(p => p.References, request.References) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail("Page not found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreatePageAsync(long authorBookId, CreateBookPage request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken)) + return Result.Fail("Book not found"); + + if (await context.Pages.AnyAsync(p => p.AuthorBookId == authorBookId && p.Number == request.Number && p.Type == request.Type, cancellationToken)) + return Result.Fail("A page with the same number already exists for this book"); + + var page = context.Pages.Add(new Entities.BookPage + { + AuthorBookId = authorBookId, + Type = request.Type, + ContentType = request.ContentType, + Number = request.Number, + Content = request.Content, + Notes = request.Notes, + References = request.References, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(page.Entity.Id) + : Result.Fail("Failed to create page"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPagesAsync(long authorBookId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken)) + return Result.Fail("Book not found"); + + var pages = await context.Pages.AsNoTracking() + .Where(p => p.AuthorBookId == authorBookId).ToArrayAsync(cancellationToken); + + return pages?.Length > 0 + ? Result.Ok(pages.Select(p => p.ToModel()).ToArray()) + : Result.Fail("No pages found for the specified book"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPageByNumberAsync(long pageId, int number, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == pageId && p.Number == number, cancellationToken); + + return page is not null + ? page.ToModel() + : Result.Fail("Page not found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPageAsync(long pageId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == pageId, cancellationToken); + + return page is not null + ? page.ToModel() + : Result.Fail("Page not found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/CartService.cs b/LiteCharms.Features.MidrandBooks/Payments/CartService.cs new file mode 100644 index 0000000..b78479c --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/CartService.cs @@ -0,0 +1,153 @@ +using LiteCharms.Features.Browser; +using LiteCharms.Features.Hasher; +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.Payments; + +public sealed class CartService(LocalStorageService localStorage) +{ + private readonly string CartStorageKey = HashService.ToMd5Hash(nameof(Cart)).Value; + + public Cart ShoppingCart { get; private set; } = new(); + + public event Action? OnCartChanged; + + public static Func GetCartItemQuantity = (shoppingCart, productPriceId) => + shoppingCart.Items.FirstOrDefault(p => p.Price!.Id == productPriceId)?.Quantity ?? 1; + + public Cart GetCart() => ShoppingCart; + + public void NotifyStateChanged() => OnCartChanged?.Invoke(); + + public async Task LoadCartFromStorageAsync() + { + var loadResult = await localStorage.GetAsync(CartStorageKey); + + if (loadResult.IsFailed) await localStorage.SaveAsync(CartStorageKey, ShoppingCart); + + if (loadResult.IsSuccess) ShoppingCart = loadResult.Value; + + NotifyStateChanged(); + } + + public async Task SaveCartToStorageAsync() => await localStorage.SaveAsync(CartStorageKey, ShoppingCart); + + public void AddItem(ProductPrice productPrice, Product product, Author author) + { + var itemExists = false; + + for (var i = 0; i < ShoppingCart.Items.Count; i++) + { + if (ShoppingCart.Items[i].Price!.Id == productPrice.Id) + { + ShoppingCart.Items[i].Quantity++; + ShoppingCart.Items[i].Amount += productPrice.Amount; + + itemExists = true; + + break; + } + } + + if (!itemExists) + ShoppingCart.Items.Add(new CartItem + { + Product = product, + Author = author, + Price = productPrice, + Amount = productPrice.Amount, + Quantity = 1, + }); + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void UpdateQuantity(long productPriceId, int delta) + { + for (var i = 0; i < ShoppingCart.Items.Count; i++) + { + if (ShoppingCart.Items[i].Price!.Id == productPriceId) + { + var oldQuantity = ShoppingCart.Items[i].Quantity; + var pricePerUnit = ShoppingCart.Items[i].Price!.Amount; + + ShoppingCart.Items[i].Quantity += delta; + ShoppingCart.Items[i].Amount = pricePerUnit * ShoppingCart.Items[i].Quantity; + break; + } + } + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void RemoveOneItem(long productPriceId) + { + for (var i = 0; i < ShoppingCart.Items.Count; i++) + { + if (ShoppingCart.Items[i].Price!.Id == productPriceId) + { + if (ShoppingCart.Items[i].Quantity <= 1) + { + ShoppingCart.Items.Remove(ShoppingCart.Items[i]); + + break; + } + else + { + ShoppingCart.Items[i].Quantity--; + ShoppingCart.Items[i].Amount -= ShoppingCart.Items[i].Price!.Amount; + } + + break; + } + } + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void RemoveAllSameItem(long productPriceId) + { + if (ShoppingCart.Items.Count == 0) return; + + var item = ShoppingCart.Items.FirstOrDefault(i => i.Price?.Id == productPriceId); + + if (item is not null) ShoppingCart.Items.Remove(item); + + CalculateTotalPrice(); + NotifyStateChanged(); + } + + public void Clear() + { + if(ShoppingCart.CustomerId is not null || ShoppingCart.OrderId is not null) + { + ShoppingCart.TotalAmount = 0; + ShoppingCart.TotalVat = 0; + ShoppingCart.Items.Clear(); + + return; + } + + ShoppingCart = new Cart(); + + NotifyStateChanged(); + } + + public decimal CalculateTotalPrice() + { + if (ShoppingCart.Items.Count == 0) return 0; + + var gross = ShoppingCart.Items.Sum(i => i.Amount); + + if (!ShoppingCart.IsVatInclusive) ShoppingCart.TotalVat = gross * ShoppingCart.VatRate; + + ShoppingCart.TotalAmount = gross + ShoppingCart.TotalVat; + + return ShoppingCart.TotalAmount; + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/Payment.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/Payment.cs new file mode 100644 index 0000000..69e5fcb --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/Payment.cs @@ -0,0 +1,9 @@ +using LiteCharms.Features.MidrandBooks.Orders.Entities; + +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class Payment : Models.Payment +{ + public virtual Order? Order { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentConfiguration.cs new file mode 100644 index 0000000..d1f32ef --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentConfiguration.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class PaymentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Payments"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt); + builder.Property(f => f.Status).IsRequired(); + builder.Property(f => f.Reference).IsRequired(); + builder.Property(f => f.OrderId).IsRequired(); + builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); + + builder.HasOne(f => f.Order) + .WithMany() + .HasForeignKey(f => f.OrderId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGateway.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGateway.cs new file mode 100644 index 0000000..cf47bf9 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGateway.cs @@ -0,0 +1,4 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class PaymentGateway : Models.PaymentGateway; diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs new file mode 100644 index 0000000..43873ed --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayConfiguration.cs @@ -0,0 +1,19 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class PaymentGatewayConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Gateways"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt); + builder.Property(f => f.Website).IsRequired(false); + builder.Property(f => f.IsSandbox); + builder.Property(f => f.MerchantKey).IsRequired(); + builder.Property(f => f.MerchantId).IsRequired(); + builder.Property(f => f.Enabled); + builder.Property(f => f.Name).IsRequired(); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedger.cs new file mode 100644 index 0000000..6ed1fe6 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedger.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.MidrandBooks.Orders.Entities; + +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class PaymentGatewayLedger : Models.PaymentGatewayLedger +{ + public virtual Order? Order { get; set; } + + public virtual Payment? Payment { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedgerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedgerConfiguration.cs new file mode 100644 index 0000000..f95b256 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentGatewayLedgerConfiguration.cs @@ -0,0 +1,30 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class PaymentGatewayLedgerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("GatewayLedger"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.OrderId).IsRequired(); + builder.Property(f => f.PaymentId).IsRequired(); + builder.Property(f => f.PayfastPaymentId).IsRequired(); + builder.Property(f => f.MerchantPaymentId).IsRequired(); + builder.Property(f => f.AmountGross).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.AmountFee).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.AmountNet).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.CustomerEmail).IsRequired(false); + + builder.HasOne(f => f.Order) + .WithMany() + .HasForeignKey(f => f.OrderId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Payment) + .WithMany() + .HasForeignKey(f => f.PaymentId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs new file mode 100644 index 0000000..acec0ea --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedger.cs @@ -0,0 +1,14 @@ +using LiteCharms.Features.MidrandBooks.Customers.Entities; +using LiteCharms.Features.MidrandBooks.Orders.Entities; + +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class PaymentLedger : Models.PaymentLedger +{ + public virtual Payment? Payment { get; set; } + + public virtual Order? Order { get; set; } + + public virtual Customer? Customer { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs new file mode 100644 index 0000000..c0add81 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/PaymentLedgerConfiguration.cs @@ -0,0 +1,34 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class PaymentLedgerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Ledger"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.Status).IsRequired(); + builder.Property(f => f.MerchantPaymentId).IsRequired(false); + builder.Property(f => f.OrderId).IsRequired(); + builder.Property(f => f.CustomerId).IsRequired(); + builder.Property(f => f.PaymentId).IsRequired(); + + builder.HasOne(f => f.Payment) + .WithMany() + .IsRequired() + .HasForeignKey(f => f.PaymentId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Order) + .WithMany() + .IsRequired() + .HasForeignKey(f => f.OrderId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Customer) + .WithMany() + .IsRequired() + .HasForeignKey(f => f.CustomerId); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/Refund.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/Refund.cs new file mode 100644 index 0000000..585ee1a --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/Refund.cs @@ -0,0 +1,9 @@ +using LiteCharms.Features.MidrandBooks.Orders.Entities; + +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +[EntityTypeConfiguration] +public class Refund : Models.Refund +{ + public virtual Order? Order { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Entities/RefundConfiguration.cs b/LiteCharms.Features.MidrandBooks/Payments/Entities/RefundConfiguration.cs new file mode 100644 index 0000000..5227c6e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Entities/RefundConfiguration.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Entities; + +public sealed class RefundConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Refunds"); + + builder.HasKey(r => r.Id); + builder.Property(r => r.OrderId).IsRequired(); + builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(o => o.Status).IsRequired(); + builder.Property(r => r.Amount).IsRequired().HasPrecision(18, 2); + builder.Property(r => r.Reason).HasMaxLength(1000); + + builder.HasOne(r => r.Order) + .WithMany(o => o.Refunds) + .HasForeignKey(r => r.OrderId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs new file mode 100644 index 0000000..5e4fe07 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/Handlers/PayfastPaymentConfirmationReceivedEventHandler.cs @@ -0,0 +1,104 @@ +using LiteCharms.Features.Hasher; +using LiteCharms.Features.Mediator; +using LiteCharms.Features.MidrandBooks.Orders; +using LiteCharms.Features.MidrandBooks.Payments.Models; + +namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers; + +public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, ILogger logger) : + INotificationHandler +{ + public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken) + { + using var activity = MediatorTelemetry.Source.StartActivity($"Quartz: {typeof(PayfastPaymentConfirmationReceivedEvent).Name}"); + activity?.SetTag("event.correlation_id", notification.CorrelationId); + + await using var scope = services.CreateAsyncScope(); + var hashService = scope.ServiceProvider.GetRequiredService(); + var orderService = scope.ServiceProvider.GetRequiredService(); + var paymentService = scope.ServiceProvider.GetRequiredService(); + var payfastService = scope.ServiceProvider.GetRequiredService(); + + var payload = notification.Payload ?? throw new Exception("Payload metadata context is null."); + + var hashResult = hashService.DecodeLongIdHash(payload.MerchantPaymentId!); + if (hashResult.IsFailed) throw new Exception("Failed to decode application tracking hash key identifier."); + + var orderResult = await orderService.GetOrderAsync(hashResult.Value, cancellationToken); + if (orderResult.IsFailed) throw new Exception("Target system order entity context cannot be traced."); + + var paymentResult = await paymentService.GetOrderPaymentAsync(orderResult.Value.Id, cancellationToken); + if (paymentResult.IsFailed) throw new Exception("Target payment ledger entity cannot be resolved."); + + var isAlreadyProcessed = await paymentService.HasLedgerEntryAsync(orderResult.Value.Id, paymentResult.Value.Id, cancellationToken); + if (isAlreadyProcessed.Value) + { + logger.LogWarning("Webhook reference token '{Ref}' already verified. Skipping processing routines.", payload.MerchantPaymentId); + + return; + } + + var isAmountValid = payfastService.ValidatePaymentAmount(orderResult.Value.Total, payload.AmountGross); + if (!isAmountValid.Value) + throw new Exception("Security validation exception: Transaction cost variance bounds breached (Price Tampering Detected)."); + + decimal.TryParse(payload.AmountGross, CultureInfo.InvariantCulture, out var gross); + decimal.TryParse(payload.AmountFee, CultureInfo.InvariantCulture, out var fee); + decimal.TryParse(payload.AmountNet, CultureInfo.InvariantCulture, out var net); + string status = payload.PaymentStatus ?? "UNKNOWN"; + + await payfastService.WriteLedgerEntryAsync(new CreateGatewayLedgerEntry + { + OrderId = orderResult.Value.Id, + PaymentId = paymentResult.Value.Id, + MerchantPaymentId = payload.MerchantPaymentId!, + PayfastPaymentId = payload.PaymentId, + CustomerEmail = payload.EmailAddress, + AmountFee = fee, + AmountGross = gross, + AmountNet = net, + PaymentStatus = status, + }, cancellationToken); + + if (status.Equals("COMPLETE", StringComparison.OrdinalIgnoreCase)) + { + var ledgerWriteResult = await paymentService.WriteLedgerEntryAsync(new CreateLedgerEntry + { + OrderId = orderResult.Value.Id, + PaymentId = paymentResult.Value.Id, + PaymentGatewayReference = payload.MerchantPaymentId!, + Status = LedgerStatuses.Completed, + CustomerId = orderResult.Value.CustomerId, + }, cancellationToken); + + if (ledgerWriteResult.IsFailed) throw new Exception("Failed to write ledger entry for payment confirmation."); + + var completePaymentResult = await paymentService.CompletePaymentAsync(paymentResult.Value.Id, PaymentStatuses.Paid, cancellationToken); + if (completePaymentResult.IsFailed) throw new Exception("Failed to update payment status to 'Paid'."); + + var updateOrderResult = await orderService.UpdateOrderStatusAsync(orderResult.Value.Id, OrderStatus.Completed, cancellationToken); + if (updateOrderResult.IsFailed) throw new Exception("Failed to update order status to 'Completed'."); + + logger.LogInformation("Order payment verified secure and cleared successfully."); + } + else + { + LedgerStatuses ledgerStatus = status.Equals("CANCELLED", StringComparison.OrdinalIgnoreCase) + ? LedgerStatuses.Cancelled + : LedgerStatuses.Failed; + + await paymentService.WriteLedgerEntryAsync(new CreateLedgerEntry + { + OrderId = orderResult.Value.Id, + PaymentId = paymentResult.Value.Id, + PaymentGatewayReference = payload.MerchantPaymentId!, + Status = ledgerStatus, + CustomerId = orderResult.Value.CustomerId, + }, cancellationToken); + + logger.LogInformation("Webhook pipeline logged non-success entry to ledger with status: {Status}", status); + } + + activity?.SetStatus(ActivityStatusCode.Ok); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs b/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs new file mode 100644 index 0000000..edd0f71 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Events/PayfastPaymentConfirmationReceivedEvent.cs @@ -0,0 +1,30 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.MidrandBooks.Payments.Models; + +namespace LiteCharms.Features.MidrandBooks.Payments.Events; + +public sealed class PayfastPaymentConfirmationReceivedEvent : EventBase, IEvent +{ + public string Name { get; set; } = nameof(PayfastPaymentConfirmationReceivedEvent); + + public PayfastWebhookPayload? Payload { get; set; } + + public string? RemoteIpAddress { get; set; } + + public bool PerformBackgroundChecks { get; set; } + + public bool AllowLoopback { get; set; } + + public PayfastPaymentConfirmationReceivedEvent() { } + + private PayfastPaymentConfirmationReceivedEvent(PayfastWebhookPayload? payload, string paymentId, bool performBackgroundChecks = true, bool allowLoopback = false) + { + Payload = payload; + CorrelationId = paymentId; + PerformBackgroundChecks = performBackgroundChecks; + AllowLoopback = allowLoopback; + } + + public static PayfastPaymentConfirmationReceivedEvent Create(PayfastWebhookPayload? payload, string paymentId, bool performBackgroundChecks = true, bool allowLoopback = false) => + new(payload, paymentId, performBackgroundChecks, allowLoopback); +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/Cart.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Cart.cs new file mode 100644 index 0000000..3af0292 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Cart.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public sealed class Cart +{ + public long? CustomerId { get; set; } + + public long? OrderId { get; set; } + + public decimal TotalAmount { get; set; } + + public decimal TotalVat { get; set; } + + public decimal VatRate { get; set; } = 0.15m; + + public bool IsVatInclusive { get; set; } = true; + + public IList Items { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/CartItem.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/CartItem.cs new file mode 100644 index 0000000..a656420 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/CartItem.cs @@ -0,0 +1,17 @@ +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public sealed class CartItem +{ + public Author? Author { get; set; } + + public Product? Product { get; set; } + + public ProductPrice? Price { get; set; } + + public int Quantity { get; set; } + + public decimal Amount { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PayfastWebhookPayload.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PayfastWebhookPayload.cs new file mode 100644 index 0000000..b0d8da7 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PayfastWebhookPayload.cs @@ -0,0 +1,59 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public sealed class PayfastWebhookPayload +{ + public string? MerchantId { get; set; } + + public string? MerchantKey { get; set; } + + public string? Signature { get; set; } + + public string? MerchantPaymentId { get; set; } + + public string? PaymentId { get; set; } + + public string? PaymentStatus { get; set; } + + public string? ItemName { get; set; } + + public string? ItemDescription { get; set; } + + public string? AmountGross { get; set; } + + public string? AmountFee { get; set; } + + public string? AmountNet { get; set; } + + public string? NameFirst { get; set; } + + public string? NameLast { get; set; } + + public string? EmailAddress { get; set; } + + public string? CustomStr1 { get; set; } + + public string? CustomInt1 { get; set; } + + public string? Token { get; set; } + + public IDictionary ToParamDictionary() => new Dictionary + (StringComparer.Ordinal) + { + { "merchant_id", MerchantId }, + { "merchant_key", MerchantKey }, + { "m_payment_id", MerchantPaymentId }, + { "pf_payment_id", PaymentId }, + { "payment_status", PaymentStatus }, + { "item_name", ItemName }, + { "item_description", ItemDescription }, + { "amount_gross", AmountGross }, + { "amount_fee", AmountFee }, + { "amount_net", AmountNet }, + { "custom_str1", CustomStr1 }, + { "custom_int1", CustomInt1 }, + { "name_first", NameFirst }, + { "name_last", NameLast }, + { "email_address", EmailAddress }, + { "token", Token } + }; +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/Payment.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Payment.cs new file mode 100644 index 0000000..61c22eb --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Payment.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class Payment +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public decimal Amount { get; set; } + + public long OrderId { get; set; } + + public string? Reference { get; set; } + + public PaymentStatuses Status { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs new file mode 100644 index 0000000..4c2701c --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGateway.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class PaymentGateway +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Name { get; set; } + + public string? Website { get; set; } + + public string? MerchantId { get; set; } + + public string? MerchantKey { get; set; } + + public bool IsSandbox { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGatewayLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGatewayLedger.cs new file mode 100644 index 0000000..2e27057 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentGatewayLedger.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class PaymentGatewayLedger +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public string? CustomerEmail { get; set; } + + public long OrderId { get; set; } + + public long PaymentId { get; set; } + + public string? MerchantPaymentId { get; set; } + + public string? PayfastPaymentId { get; set; } + + public string? PaymentStatus { get; set; } + + public decimal AmountGross { get; set; } + + public decimal AmountFee { get; set; } + + public decimal AmountNet { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs new file mode 100644 index 0000000..d650698 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/PaymentLedger.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class PaymentLedger +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public LedgerStatuses Status { get; set; } + + public long OrderId { get; set; } + + public long PaymentId { get; set; } + + public long CustomerId { get; set; } + + public string? MerchantPaymentId { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs new file mode 100644 index 0000000..f403b20 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Records.cs @@ -0,0 +1,74 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public sealed record CreateGatewayLedgerEntry +{ + public string? CustomerEmail { get; set; } + + public required long OrderId { get; set; } + + public required long PaymentId { get; set; } + + public string? MerchantPaymentId { get; set; } + + public string? PayfastPaymentId { get; set; } + + public string? PaymentStatus { get; set; } + + public decimal AmountGross { get; set; } + + public decimal AmountFee { get; set; } + + public decimal AmountNet { get; set; } +} + +public sealed record UpdateRefund +{ + public long OrderId { get; set; } + + public RefundStatus Status { get; set; } + + public string? Reason { get; set; } + + public decimal Amount { get; set; } +}; + +public sealed record CreateRefund +{ + public long OrderId { get; set; } + + public RefundTypes Type { get; set; } + + public RefundStatus Status { get; set; } + + public string? Reason { get; set; } + + public decimal Amount { get; set; } +} + +public sealed record CreateLedgerEntry +{ + public required LedgerStatuses Status { get; set; } + + public required long OrderId { get; set; } + + public required long PaymentId { get; set; } + + public required long CustomerId { get; set; } + + public string? PaymentGatewayReference { get; set; } + + public long? PaymentGatewayId { get; set; } +} + +public sealed record CreatePaymentGateway +{ + public required string? Name { get; set; } + + public string? Website { get; set; } + + public required string? MerchantId { get; set; } + + public required string? MerchantKey { get; set; } + + public bool IsSandbox { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/Models/Refund.cs b/LiteCharms.Features.MidrandBooks/Payments/Models/Refund.cs new file mode 100644 index 0000000..5e81d3a --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/Models/Refund.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.MidrandBooks.Payments.Models; + +public class Refund +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long OrderId { get; set; } + + public RefundTypes Type { get; set; } + + public RefundStatus Status { get; set; } + + public string? Reason { get; set; } + + public decimal Amount { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs new file mode 100644 index 0000000..06bdbca --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/PayfastService.cs @@ -0,0 +1,245 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Api.Configuration; +using LiteCharms.Features.Hasher; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Payments; + +public sealed partial class PayfastService(IDbContextFactory contextFactory, + IOptions payfastOptions, ILogger logger, IHttpClientFactory httpClientFactory) : IService +{ + [GeneratedRegex(@"%[0-9A-Fa-f]{2}", RegexOptions.None, matchTimeoutMilliseconds: 1000)] + public static partial Regex PercentEncodingRegex { get; } + + public async ValueTask> WriteLedgerEntryAsync(CreateGatewayLedgerEntry request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if(!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) + return Result.Fail("Referenced order ID does not exist in database."); + + if(!await context.Payments.AnyAsync(p => p.Id == request.PaymentId, cancellationToken)) + return Result.Fail("Referenced payment ID does not exist in database."); + + var entry = context.GatewayLedger.Add(new Entities.PaymentGatewayLedger + { + CustomerEmail = request.CustomerEmail, + OrderId = request.OrderId, + PaymentId = request.PaymentId, + MerchantPaymentId = request.MerchantPaymentId, + PayfastPaymentId = request.PayfastPaymentId, + PaymentStatus = request.PaymentStatus, + AmountGross = request.AmountGross, + AmountFee = request.AmountFee, + AmountNet = request.AmountNet, + CreatedAt = DateTime.UtcNow, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(entry.Entity.Id) + : Result.Fail("Failed to save Payfast ledger entry to database."); + } + catch (Exception ex) + { + return Result.Fail(new Error("Failed to write Payfast ledger entry to database.").CausedBy(ex)); + } + } + + public static bool VerifyIncomingSignatureFromForm(IFormCollection formCollection, string passphrase) + { + var sortedFields = new Dictionary(StringComparer.Ordinal); + + foreach (var field in formCollection) + { + sortedFields.Add(field.Key, field.Value.ToString()); + } + + if (!sortedFields.TryGetValue("signature", out var incomingSignature)) return false; + + var stringBuilder = new StringBuilder(); + + foreach (var key in sortedFields.Keys) + { + if (key.Equals("signature", StringComparison.OrdinalIgnoreCase)) continue; + + string encodedVal = HttpUtility.UrlEncode(sortedFields[key].Trim()); + string cleanVal = PercentEncodingRegex.Replace(encodedVal, m => m.Value.ToUpperInvariant()); + + stringBuilder.Append($"{key}={cleanVal}&"); + } + + string encodedPassphrase = HttpUtility.UrlEncode(passphrase.Trim()); + string safePassphrase = PercentEncodingRegex.Replace(encodedPassphrase, m => m.Value.ToUpperInvariant()); + + stringBuilder.Append($"passphrase={safePassphrase}"); + + string generatedSignature = HashService.ToMd5Hash(stringBuilder.ToString()).Value; + + return incomingSignature.Equals(generatedSignature, StringComparison.OrdinalIgnoreCase); + } + + public async ValueTask> ValidateReferrerIpAsync(string remoteIpAddress, bool allowLoopback = false, CancellationToken cancellationToken = default) + { + if(payfastOptions.Value?.ValidHosts?.Length == 0) + return Result.Fail("Valid payfast hosts not configured."); + + if (string.IsNullOrWhiteSpace(remoteIpAddress)) + return Result.Fail("Remote IP address is null or whitespace."); + + try + { + var validIps = new HashSet(); + + foreach (var host in payfastOptions.Value!.ValidHosts!) + { + try + { + var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken); + + foreach (var addr in addresses) validIps.Add(addr); + } + catch (SocketException ex) + { + logger.LogWarning(ex, "DNS warning: Failed to resolve Payfast node '{Host}'. It may be decommissioned or unreachable.", host); + } + } + + if (IPAddress.TryParse(remoteIpAddress, out var incomingIp)) + { + if (allowLoopback && IPAddress.IsLoopback(incomingIp)) + { + logger.LogInformation("Local development loopback IP '{RemoteIp}' allowed bypassing DNS verification.", remoteIpAddress); + return Result.Ok(true); + } + + bool isValid = validIps.Contains(incomingIp); + + if (!isValid) + logger.LogWarning("SECURITY ALERT: Webhook IP '{RemoteIp}' originated from an unlisted host schema.", remoteIpAddress); + + return Result.Ok(isValid); + } + + return Result.Fail("Invalid remote IP address format."); + } + catch (Exception ex) + { + return Result.Fail(new Error("DNS Verification error while scanning Payfast IP nodes.").CausedBy(ex)); + } + } + + public Result ValidatePaymentAmount(decimal expectedTotal, string? amountGrossString) + { + if (!decimal.TryParse(amountGrossString, CultureInfo.InvariantCulture, out decimal grossAmount)) + return Result.Fail("Failed to parse payment amount."); + + decimal delta = Math.Abs(expectedTotal - grossAmount); + + bool isAmountValid = delta <= 0.01m; + + if (!isAmountValid) + logger.LogError("FINANCIAL DRIFT EXCEPTION: Expected order total R{Expected} but gateway cleared R{Cleared}.", expectedTotal, grossAmount); + + return Result.Ok(isAmountValid); + } + + public async ValueTask> ValidateServerConfirmationAsync(string rawQueryParamString, bool isSandbox, CancellationToken ct) + { + try + { + string host = isSandbox ? "sandbox.payfast.co.za" : "www.payfast.co.za"; + string targetUrl = $"https://{host}/eng/query/validate"; + + using var content = new StringContent(rawQueryParamString, Encoding.UTF8, "application/x-www-form-urlencoded"); + + var httpClient = httpClientFactory.CreateClient(); + + var response = await httpClient.PostAsync(targetUrl, content, ct); + + if (!response.IsSuccessStatusCode) return Result.Fail("Failed to validate server confirmation."); + + string responseText = await response.Content.ReadAsStringAsync(ct); + + bool isValidated = string.Equals(responseText.Trim(), "VALID", StringComparison.OrdinalIgnoreCase); + + if (!isValidated) + logger.LogWarning("SECURITY WARNING: Payfast back-channel returned validation response: '{Response}'", responseText); + + return Result.Ok(isValidated); + } + catch (Exception ex) + { + return Result.Fail(new Error("Failed to complete back-channel cURL verification handshakes with Payfast remote endpoints.").CausedBy(ex)); + } + } + + public static Result GenerateSignature(IDictionary data, string? passPhrase = null) + { + var pfOutput = new StringBuilder(); + + var mandatorySequence = GetPayfastMandatoryFieldSequence(); + + foreach (string key in mandatorySequence) + { + if (data.TryGetValue(key, out string? rawValue) && !string.IsNullOrEmpty(rawValue)) + { + string encodedVal = HttpUtility.UrlEncode(rawValue.Trim()); + string val = PercentEncodingRegex.Replace(encodedVal, m => m.Value.ToUpperInvariant()); + + pfOutput.Append($"{key}={val}&"); + } + } + + var getString = pfOutput.Length > 0 + ? pfOutput.ToString()[..^1] + : string.Empty; + + if (!string.IsNullOrWhiteSpace(passPhrase)) + { + string encodedPassphrase = HttpUtility.UrlEncode(passPhrase.Trim()); + string safePassphrase = PercentEncodingRegex.Replace(encodedPassphrase, m => m.Value.ToUpperInvariant()); + + getString += $"&passphrase={safePassphrase}"; + } + + return HashService.ToMd5Hash(getString); + } + + private static string[] GetPayfastMandatoryFieldSequence() => + [ + "merchant_id", + "merchant_key", + "return_url", + "cancel_url", + "notify_url", + "name_first", + "name_last", + "email_address", + "cell_number", + "m_payment_id", + "amount", + "item_name", + "item_description", + "custom_int1", + "custom_int2", + "custom_int3", + "custom_int4", + "custom_int5", + "custom_str1", + "custom_str2", + "custom_str3", + "custom_str4", + "custom_str5", + "email_confirmation", + "confirmation_address", + "payment_method", + "subscription_type", + "billing_date", + "recurring_amount", + "frequency", + "cycles" + ]; +} diff --git a/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs new file mode 100644 index 0000000..b1a3531 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Payments/PaymentService.cs @@ -0,0 +1,301 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Payments.Models; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Payments; + +public sealed class PaymentService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask> GetOrderPaymentAsync(long orderId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var payment = await context.Payments.AsNoTracking() + .Where(p => p.OrderId == orderId) + .OrderByDescending(p => p.Id) + .FirstOrDefaultAsync(cancellationToken); + + return payment is not null + ? Result.Ok(payment.ToModel()) + : Result.Fail("Could not find payment for the order"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetRefundAsync(long refundId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var refund = await context.Refunds.AsNoTracking() + .FirstOrDefaultAsync(r => r.Id == refundId, cancellationToken); + + return refund is not null + ? Result.Ok(refund.ToModel()) + : Result.Fail("Could not find refund"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateRefundAsync(long refundId, UpdateRefund request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) + return Result.Fail("Order not found"); + + var updatedRows = await context.Refunds + .Where(r => r.Id == refundId && r.OrderId == request.OrderId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(r => r.Status, request.Status) + .SetProperty(r => r.Reason, request.Reason) + .SetProperty(r => r.UpdatedAt, DateTime.UtcNow) + .SetProperty(r => r.Amount, request.Amount), cancellationToken); + + return updatedRows > 0 + ? Result.Ok() + : Result.Fail("Failed to update refund"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateRefundAsync(CreateRefund request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == request.OrderId + && o.Status == OrderStatus.Completed, cancellationToken); + + if (order is null) return Result.Fail("Order not found"); + + if (request.Amount > order.Total) + return Result.Fail("Refund amount cannot be greater than order total"); + + var totalRefundsPaid = await context.Refunds + .Where(r => r.OrderId == request.OrderId) + .SumAsync(r => r.Amount, cancellationToken); + + if (request.Amount > (order.Total - totalRefundsPaid)) + return Result.Fail("Refund amount exceeds amount available for refund"); + + var refund = context.Refunds.Add(new Entities.Refund + { + Amount = request.Amount, + CreatedAt = DateTime.UtcNow, + OrderId = request.OrderId, + Reason = request.Reason, + Status = request.Status, + Type = request.Type, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(refund.Entity.Id) + : Result.Fail("Failed to create refund"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> HasLedgerEntryAsync(long orderId, long paymentId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var exists = await context.Ledger.AnyAsync(l => + l.OrderId == orderId && l.PaymentId == paymentId && l.Status == LedgerStatuses.Completed, cancellationToken); + + return Result.Ok(exists); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask WriteLedgerEntryAsync(CreateLedgerEntry request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) + return Result.Fail("Order not found"); + + if (!await context.Customers.AnyAsync(o => o.Id == request.CustomerId, cancellationToken)) + return Result.Fail("Customer not found"); + + if (!await context.Orders.AnyAsync(oc => oc.Id == request.OrderId && oc.CustomerId == request.CustomerId, cancellationToken)) + return Result.Fail("Customer does not match the order"); + + if (!await context.Payments.AnyAsync(o => o.Id == request.PaymentId && o.OrderId == request.OrderId, cancellationToken)) + return Result.Fail("Payment not found"); + + if (request.PaymentGatewayId is not null) + if (!await context.Gateways.AnyAsync(o => o.Id == request.PaymentGatewayId, cancellationToken)) + return Result.Fail("Gateway not found"); + + context.Ledger.Add(new Entities.PaymentLedger + { + CreatedAt = DateTime.UtcNow, + CustomerId = request.CustomerId, + OrderId = request.OrderId, + PaymentId = request.PaymentId, + MerchantPaymentId = request.PaymentGatewayReference, + Status = request.Status, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to create ledger entry"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPaymentGatewayAsync(long paymentGatewayId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var gateway = await context.Gateways.AsNoTracking().FirstOrDefaultAsync(g => g.Id == paymentGatewayId, cancellationToken); + + return gateway is not null + ? Result.Ok(gateway.ToModel()) + : Result.Fail("Could not find gateway"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreatePaymentGatewayAsync(CreatePaymentGateway request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Gateways.AnyAsync(g => g.MerchantId == request.MerchantId && g.MerchantKey == request.MerchantKey, cancellationToken)) + return Result.Fail("A gateway with the same credentials already exists"); + + var gateway = context.Gateways.Add(new Entities.PaymentGateway + { + CreatedAt = DateTime.UtcNow, + Enabled = true, + IsSandbox = request.IsSandbox, + MerchantId = request.MerchantId, + MerchantKey = request.MerchantKey, + Name = request.Name, + Website = request.Website, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(gateway.Entity.Id) + : Result.Fail("Failed to create payment gateway"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask CompletePaymentAsync(long paymentId, PaymentStatuses status, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (status == PaymentStatuses.NotPaid) + return Result.Fail("Cannot finalise a payment using NotPaid status"); + + var updatedRecords = await context.Payments + .Where(p => p.Id == paymentId && p.Status != PaymentStatuses.Paid && p.Status != status) + .ExecuteUpdateAsync(setters => setters + .SetProperty(u => u.Status, status) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return updatedRecords > 0 + ? Result.Ok() + : Result.Fail("Failed to update payment"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdatePaymentAsync(long paymentId, decimal amount, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var updatedRecords = await context.Payments + .Where(p => p.Id == paymentId && p.Status == PaymentStatuses.NotPaid) + .ExecuteUpdateAsync(setters => setters + .SetProperty(u => u.Amount, amount) + .SetProperty(u => u.Status, PaymentStatuses.NotPaid) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return updatedRecords > 0 + ? Result.Ok() + : Result.Fail("Failed to update payment"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreatePaymentAsync(decimal amount, long orderId, string reference, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Payments.AnyAsync(p => p.OrderId == orderId && p.Amount == amount && p.Status != PaymentStatuses.Paid, cancellationToken)) + return Result.Fail("An order with the same amount already exists in the system"); + + var payment = context.Payments.Add(new Entities.Payment + { + CreatedAt = DateTime.UtcNow, + Amount = amount, + OrderId = orderId, + Reference = reference, + Status = PaymentStatuses.NotPaid, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(payment.Entity.Id) + : Result.Fail("Failed to make payment"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs new file mode 100644 index 0000000..822b1c0 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -0,0 +1,53 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; +using LiteCharms.Features.MidrandBooks.Authors.Entities; +using LiteCharms.Features.MidrandBooks.Categories.Entities; +using LiteCharms.Features.MidrandBooks.Customers.Entities; +using LiteCharms.Features.MidrandBooks.Orders.Entities; +using LiteCharms.Features.MidrandBooks.Pages.Entities; +using LiteCharms.Features.MidrandBooks.Payments.Entities; +using LiteCharms.Features.MidrandBooks.Products.Entities; + +namespace LiteCharms.Features.MidrandBooks.Postgres; + +public sealed class MidrandBooksDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Authors => Set(); + + public DbSet Products => Set(); + + public DbSet Prices => Set(); + + public DbSet Books => Set(); + + public DbSet Pages => Set(); + + public DbSet Contacts => Set(); + + public DbSet
Addresses => Set
(); + + public DbSet Customers => Set(); + + public DbSet Orders => Set(); + + public DbSet OrderItems => Set(); + + public DbSet Refunds => Set(); + + public DbSet Shippings => Set(); + + public DbSet ShippingProviders => Set(); + + public DbSet Categories => Set(); + + public DbSet ProductCategories => Set(); + + public DbSet Inventories => Set(); + + public DbSet Payments => Set(); + + public DbSet Gateways => Set(); + + public DbSet Ledger => Set(); + + public DbSet GatewayLedger => Set(); +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs new file mode 100644 index 0000000..fe25033 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContextFactory.cs @@ -0,0 +1,20 @@ +using static LiteCharms.Features.MidrandBooks.Extensions.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Postgres; + +public sealed class MidrandBooksDbContextFactory : IDesignTimeDbContextFactory +{ + public MidrandBooksDbContext CreateDbContext(string[] args) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddUserSecrets(typeof(MidrandBooksDbContext).Assembly) + .AddEnvironmentVariables() + .Build(); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName)); + + return new MidrandBooksDbContext(optionsBuilder.Options); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.Designer.cs new file mode 100644 index 0000000..42297c0 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.Designer.cs @@ -0,0 +1,938 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260529070104_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection("Categories") + .HasColumnType("text[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.cs new file mode 100644 index 0000000..d960313 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260529070104_Init.cs @@ -0,0 +1,471 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Authors", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + PublisherType = table.Column(type: "integer", nullable: false), + Company = table.Column(type: "text", nullable: true), + VatNumber = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + LastName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Biography = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Email = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Website = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + ThumbnailImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + SocialMedia = table.Column(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Authors", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Company = table.Column(type: "text", nullable: true), + VatNumber = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: false), + Website = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + SocialMedia = table.Column(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + CustomerId = table.Column(type: "bigint", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Total = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Notes = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + InvoiceUrl = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + ImageUrl = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), + ThumbnailUrls = table.Column(type: "text[]", nullable: true), + Categories = table.Column(type: "text[]", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: false), + Metadata = table.Column(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ShippingProviders", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Type = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "text", nullable: true), + Price = table.Column(type: "numeric", nullable: true), + TrackingUrl = table.Column(type: "text", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShippingProviders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Addresses", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CustomerId = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + BuildingType = table.Column(type: "integer", nullable: false), + Street = table.Column(type: "text", nullable: false), + City = table.Column(type: "text", nullable: false), + State = table.Column(type: "text", nullable: false), + PostalCode = table.Column(type: "text", nullable: false), + Country = table.Column(type: "text", nullable: false), + IsPrimary = table.Column(type: "boolean", nullable: false, defaultValue: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Addresses", x => x.Id); + table.ForeignKey( + name: "FK_Addresses_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Contacts", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CustomerId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + IsPrimary = table.Column(type: "boolean", nullable: false, defaultValue: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Contacts", x => x.Id); + table.ForeignKey( + name: "FK_Contacts_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Refunds", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + OrderId = table.Column(type: "bigint", nullable: false), + Type = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Reason = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Refunds", x => x.Id); + table.ForeignKey( + name: "FK_Refunds_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Books", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + AuthorId = table.Column(type: "bigint", nullable: false), + ProductId = table.Column(type: "bigint", nullable: false), + Rating = table.Column(type: "integer", nullable: false), + Ranking = table.Column(type: "integer", nullable: false), + Enabled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Books", x => x.Id); + table.ForeignKey( + name: "FK_Books_Authors_AuthorId", + column: x => x.AuthorId, + principalTable: "Authors", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Books_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Prices", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + ProductId = table.Column(type: "bigint", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Discount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Prices", x => x.Id); + table.ForeignKey( + name: "FK_Prices_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Shippings", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + OrderId = table.Column(type: "bigint", nullable: false), + AddressId = table.Column(type: "bigint", nullable: false), + ShippingProviderId = table.Column(type: "bigint", nullable: false), + TrackingNumber = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + Status = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Shippings", x => x.Id); + table.ForeignKey( + name: "FK_Shippings_Addresses_AddressId", + column: x => x.AddressId, + principalTable: "Addresses", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Shippings_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Shippings_ShippingProviders_ShippingProviderId", + column: x => x.ShippingProviderId, + principalTable: "ShippingProviders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "BookPages", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AuthorBookId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), + Type = table.Column(type: "integer", nullable: false), + ContentType = table.Column(type: "integer", nullable: false), + Number = table.Column(type: "integer", nullable: false, defaultValue: 0), + Content = table.Column(type: "bytea", nullable: false), + Notes = table.Column(type: "text[]", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + References = table.Column(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BookPages", x => x.Id); + table.ForeignKey( + name: "FK_BookPages_Books_AuthorBookId", + column: x => x.AuthorBookId, + principalTable: "Books", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "OrderItems", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + OrderId = table.Column(type: "bigint", nullable: false), + AuthorBookId = table.Column(type: "bigint", nullable: false), + ProductPriceId = table.Column(type: "bigint", nullable: false), + Quantity = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderItems", x => x.Id); + table.ForeignKey( + name: "FK_OrderItems_Books_AuthorBookId", + column: x => x.AuthorBookId, + principalTable: "Books", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_OrderItems_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrderItems_Prices_ProductPriceId", + column: x => x.ProductPriceId, + principalTable: "Prices", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Addresses_CustomerId", + table: "Addresses", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_BookPages_AuthorBookId", + table: "BookPages", + column: "AuthorBookId"); + + migrationBuilder.CreateIndex( + name: "IX_Books_AuthorId", + table: "Books", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_Books_ProductId", + table: "Books", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Contacts_CustomerId", + table: "Contacts", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_AuthorBookId", + table: "OrderItems", + column: "AuthorBookId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_OrderId", + table: "OrderItems", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_ProductPriceId", + table: "OrderItems", + column: "ProductPriceId"); + + migrationBuilder.CreateIndex( + name: "IX_Prices_ProductId", + table: "Prices", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Refunds_OrderId", + table: "Refunds", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Shippings_AddressId", + table: "Shippings", + column: "AddressId"); + + migrationBuilder.CreateIndex( + name: "IX_Shippings_OrderId", + table: "Shippings", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Shippings_ShippingProviderId", + table: "Shippings", + column: "ShippingProviderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BookPages"); + + migrationBuilder.DropTable( + name: "Contacts"); + + migrationBuilder.DropTable( + name: "OrderItems"); + + migrationBuilder.DropTable( + name: "Refunds"); + + migrationBuilder.DropTable( + name: "Shippings"); + + migrationBuilder.DropTable( + name: "Books"); + + migrationBuilder.DropTable( + name: "Prices"); + + migrationBuilder.DropTable( + name: "Addresses"); + + migrationBuilder.DropTable( + name: "Orders"); + + migrationBuilder.DropTable( + name: "ShippingProviders"); + + migrationBuilder.DropTable( + name: "Authors"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "Customers"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.Designer.cs new file mode 100644 index 0000000..e007a80 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.Designer.cs @@ -0,0 +1,966 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260530104851_AddedCategories")] + partial class AddedCategories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection("Categories") + .HasColumnType("text[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.cs new file mode 100644 index 0000000..96d30de --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(15)", maxLength: 15, nullable: false), + IsMain = table.Column(type: "boolean", nullable: false, defaultValue: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Categories"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.Designer.cs new file mode 100644 index 0000000..af132c2 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.Designer.cs @@ -0,0 +1,1007 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260530125323_AddedProductCategories")] + partial class AddedProductCategories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.cs new file mode 100644 index 0000000..4f35ac7 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.cs @@ -0,0 +1,68 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedProductCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Categories", + table: "Products"); + + migrationBuilder.CreateTable( + name: "ProductCategories", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProductId = table.Column(type: "bigint", nullable: false), + CategoryId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductCategories", x => x.Id); + table.ForeignKey( + name: "FK_ProductCategories_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProductCategories_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ProductCategories_CategoryId", + table: "ProductCategories", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductCategories_ProductId", + table: "ProductCategories", + column: "ProductId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ProductCategories"); + + migrationBuilder.AddColumn( + name: "Categories", + table: "Products", + type: "text[]", + nullable: true); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.Designer.cs new file mode 100644 index 0000000..9bdf38e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.Designer.cs @@ -0,0 +1,1235 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260531094401_AddedPaymentObjects")] + partial class AddedPaymentObjects + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Passphrase") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayReference") + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentGatewayId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", "Gateway") + .WithMany() + .HasForeignKey("PaymentGatewayId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Gateway"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.cs new file mode 100644 index 0000000..b485cd1 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260531094401_AddedPaymentObjects.cs @@ -0,0 +1,185 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedPaymentObjects : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Gateways", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Name = table.Column(type: "text", nullable: false), + Website = table.Column(type: "text", nullable: true), + MerchantId = table.Column(type: "text", nullable: false), + MerchantKey = table.Column(type: "text", nullable: false), + Passphrase = table.Column(type: "text", nullable: false), + IsSandbox = table.Column(type: "boolean", nullable: false), + Enabled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Gateways", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Inventories", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Status = table.Column(type: "integer", nullable: false), + ProductId = table.Column(type: "bigint", nullable: false), + ProductPriceId = table.Column(type: "bigint", nullable: false), + TotalAllocated = table.Column(type: "integer", nullable: false), + TotalReserved = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Inventories", x => x.Id); + table.ForeignKey( + name: "FK_Inventories_Prices_ProductPriceId", + column: x => x.ProductPriceId, + principalTable: "Prices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Inventories_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Payments", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + OrderId = table.Column(type: "bigint", nullable: false), + Reference = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Payments", x => x.Id); + table.ForeignKey( + name: "FK_Payments_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Ledger", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Status = table.Column(type: "integer", nullable: false), + OrderId = table.Column(type: "bigint", nullable: false), + PaymentId = table.Column(type: "bigint", nullable: false), + CustomerId = table.Column(type: "bigint", nullable: false), + PaymentGatewayReference = table.Column(type: "text", nullable: true), + PaymentGatewayId = table.Column(type: "bigint", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Ledger", x => x.Id); + table.ForeignKey( + name: "FK_Ledger_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Ledger_Gateways_PaymentGatewayId", + column: x => x.PaymentGatewayId, + principalTable: "Gateways", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Ledger_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Ledger_Payments_PaymentId", + column: x => x.PaymentId, + principalTable: "Payments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Inventories_ProductId", + table: "Inventories", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Inventories_ProductPriceId", + table: "Inventories", + column: "ProductPriceId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_CustomerId", + table: "Ledger", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_OrderId", + table: "Ledger", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_PaymentGatewayId", + table: "Ledger", + column: "PaymentGatewayId"); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_PaymentId", + table: "Ledger", + column: "PaymentId"); + + migrationBuilder.CreateIndex( + name: "IX_Payments_OrderId", + table: "Payments", + column: "OrderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Inventories"); + + migrationBuilder.DropTable( + name: "Ledger"); + + migrationBuilder.DropTable( + name: "Gateways"); + + migrationBuilder.DropTable( + name: "Payments"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.Designer.cs new file mode 100644 index 0000000..bcf52cd --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.Designer.cs @@ -0,0 +1,1231 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260601071804_RemovedPassphraseFromPaymentGateway")] + partial class RemovedPassphraseFromPaymentGateway + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayId") + .HasColumnType("bigint"); + + b.Property("PaymentGatewayReference") + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentGatewayId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", "Gateway") + .WithMany() + .HasForeignKey("PaymentGatewayId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Gateway"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.cs similarity index 51% rename from LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.cs rename to LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.cs index 71b82d1..9401647 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260601071804_RemovedPassphraseFromPaymentGateway.cs @@ -2,28 +2,28 @@ #nullable disable -namespace LiteCharms.Infrastructure.Database.Migrations +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations { /// - public partial class AddedStatusToLead : Migration + public partial class RemovedPassphraseFromPaymentGateway : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AddColumn( - name: "Status", - table: "Lead", - type: "integer", - nullable: false, - defaultValue: 0); + migrationBuilder.DropColumn( + name: "Passphrase", + table: "Gateways"); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropColumn( - name: "Status", - table: "Lead"); + migrationBuilder.AddColumn( + name: "Passphrase", + table: "Gateways", + type: "text", + nullable: false, + defaultValue: ""); } } } diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.Designer.cs new file mode 100644 index 0000000..6266a05 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.Designer.cs @@ -0,0 +1,1291 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260602202421_AddedPaymentGatewayLedger")] + partial class AddedPaymentGatewayLedger + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountGross") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountNet") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerEmail") + .HasColumnType("text"); + + b.Property("MerchantPaymentId") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PayfastPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("GatewayLedger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("MerchantPaymentId") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.cs new file mode 100644 index 0000000..b4cc909 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602202421_AddedPaymentGatewayLedger.cs @@ -0,0 +1,108 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedPaymentGatewayLedger : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Ledger_Gateways_PaymentGatewayId", + table: "Ledger"); + + migrationBuilder.DropIndex( + name: "IX_Ledger_PaymentGatewayId", + table: "Ledger"); + + migrationBuilder.DropColumn( + name: "PaymentGatewayId", + table: "Ledger"); + + migrationBuilder.RenameColumn( + name: "PaymentGatewayReference", + table: "Ledger", + newName: "MerchantPaymentId"); + + migrationBuilder.CreateTable( + name: "GatewayLedger", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + CustomerEmail = table.Column(type: "text", nullable: true), + OrderId = table.Column(type: "bigint", nullable: false), + PaymentId = table.Column(type: "bigint", nullable: false), + MerchantPaymentId = table.Column(type: "text", nullable: true), + PayfastPaymentId = table.Column(type: "text", nullable: false), + PaymentStatus = table.Column(type: "text", nullable: true), + AmountGross = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + AmountFee = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + AmountNet = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GatewayLedger", x => x.Id); + table.ForeignKey( + name: "FK_GatewayLedger_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GatewayLedger_Payments_PaymentId", + column: x => x.PaymentId, + principalTable: "Payments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_GatewayLedger_OrderId", + table: "GatewayLedger", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_GatewayLedger_PaymentId", + table: "GatewayLedger", + column: "PaymentId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GatewayLedger"); + + migrationBuilder.RenameColumn( + name: "MerchantPaymentId", + table: "Ledger", + newName: "PaymentGatewayReference"); + + migrationBuilder.AddColumn( + name: "PaymentGatewayId", + table: "Ledger", + type: "bigint", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Ledger_PaymentGatewayId", + table: "Ledger", + column: "PaymentGatewayId"); + + migrationBuilder.AddForeignKey( + name: "FK_Ledger_Gateways_PaymentGatewayId", + table: "Ledger", + column: "PaymentGatewayId", + principalTable: "Gateways", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.Designer.cs new file mode 100644 index 0000000..6ccd5e5 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.Designer.cs @@ -0,0 +1,1292 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger")] + partial class AddedPayfastPaymentIdToPaymentGatewayLedger + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountGross") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountNet") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerEmail") + .HasColumnType("text"); + + b.Property("MerchantPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PayfastPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("GatewayLedger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("MerchantPaymentId") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.cs new file mode 100644 index 0000000..e0853da --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260602205838_AddedPayfastPaymentIdToPaymentGatewayLedger.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedPayfastPaymentIdToPaymentGatewayLedger : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "MerchantPaymentId", + table: "GatewayLedger", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "MerchantPaymentId", + table: "GatewayLedger", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.Designer.cs new file mode 100644 index 0000000..b0689aa --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.Designer.cs @@ -0,0 +1,1290 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260612210020_OnlyEmailIsMandatoryOnCustomer")] + partial class OnlyEmailIsMandatoryOnCustomer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountGross") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountNet") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerEmail") + .HasColumnType("text"); + + b.Property("MerchantPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PayfastPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("GatewayLedger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("MerchantPaymentId") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.cs new file mode 100644 index 0000000..a49fdcf --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260612210020_OnlyEmailIsMandatoryOnCustomer.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class OnlyEmailIsMandatoryOnCustomer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Website", + table: "Customers", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Phone", + table: "Customers", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Website", + table: "Customers", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Phone", + table: "Customers", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs new file mode 100644 index 0000000..44c0efd --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -0,0 +1,1287 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + partial class MidrandBooksDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reference") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsSandbox") + .HasColumnType("boolean"); + + b.Property("MerchantId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MerchantKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Gateways", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountGross") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("AmountNet") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerEmail") + .HasColumnType("text"); + + b.Property("MerchantPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PayfastPaymentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("GatewayLedger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("MerchantPaymentId") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId"); + + b.HasIndex("PaymentId"); + + b.ToTable("Ledger", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalAllocated") + .HasColumnType("integer"); + + b.Property("TotalReserved") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Inventories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGatewayLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("Payment"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Price"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs new file mode 100644 index 0000000..265f787 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs @@ -0,0 +1,9 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +[EntityTypeConfiguration] +public class Product : Models.Product +{ + public virtual ICollection Categories { get; set; } = []; + + public virtual ICollection Prices { get; set; } = []; +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategory.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategory.cs new file mode 100644 index 0000000..6b84876 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategory.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.MidrandBooks.Categories.Entities; + +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +[EntityTypeConfiguration] +public class ProductCategory : Models.ProductCategory +{ + public virtual Product? Product { get; set; } + + public virtual Category? Category { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategoryConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategoryConfiguration.cs new file mode 100644 index 0000000..768926d --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategoryConfiguration.cs @@ -0,0 +1,23 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public sealed class ProductCategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ProductCategories"); + + builder.HasKey(p => p.Id); + builder.Property(p => p.ProductId).IsRequired(); + builder.Property(p => p.CategoryId).IsRequired(); + + builder.HasOne(p => p.Product) + .WithMany(p => p.Categories) + .HasForeignKey(p => p.ProductId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(c => c.Category) + .WithMany() + .HasForeignKey(c => c.CategoryId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs new file mode 100644 index 0000000..daf7f19 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs @@ -0,0 +1,24 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public sealed class ProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Ignore(p => p.Price); + + builder.ToTable("Products"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.Type).IsRequired(); + builder.Property(f => f.Name).IsRequired().HasMaxLength(255); + builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); + builder.Property(f => f.Description).HasMaxLength(1024); + builder.Property(f => f.ImageUrl).HasMaxLength(1024); + builder.Property(f => f.Enabled).HasDefaultValue(false); + builder.Property(f => f.ThumbnailUrls).IsRequired(false); + + builder.OwnsOne(f => f.Metadata, b => { b.ToJson(); }); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventory.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventory.cs new file mode 100644 index 0000000..7b43c55 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventory.cs @@ -0,0 +1,9 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +[EntityTypeConfiguration] +public class ProductInventory : Models.ProductInventory +{ + public virtual Product? Product { get; set; } + + public virtual ProductPrice? Price { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventoryConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventoryConfiguration.cs new file mode 100644 index 0000000..4e68570 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductInventoryConfiguration.cs @@ -0,0 +1,27 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public sealed class ProductInventoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Inventories"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.Status).IsRequired(); + builder.Property(f => f.TotalAllocated).IsRequired(); + builder.Property(f => f.TotalReserved).IsRequired(); + builder.Property(f => f.ProductId).IsRequired(); + builder.Property(f => f.ProductPriceId).IsRequired(); + + builder.HasOne(f => f.Product) + .WithMany() + .HasForeignKey(f => f.ProductId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Price) + .WithMany() + .HasForeignKey(f => f.ProductPriceId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Entities/ProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs similarity index 69% rename from LiteCharms.Entities/ProductPrice.cs rename to LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs index db492ed..ba6a593 100644 --- a/LiteCharms.Entities/ProductPrice.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs @@ -1,6 +1,4 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; +namespace LiteCharms.Features.MidrandBooks.Products.Entities; [EntityTypeConfiguration] public class ProductPrice : Models.ProductPrice diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs new file mode 100644 index 0000000..635ec8c --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPriceConfiguration.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public sealed class ProductPriceConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Prices"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()"); + builder.Property(f => f.ProductId).IsRequired(); + builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Discount).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.Enabled).HasDefaultValue(false); + + builder.HasOne(f => f.Product) + .WithMany(p => p.Prices) + .HasForeignKey(f => f.ProductId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs new file mode 100644 index 0000000..bf425e4 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs @@ -0,0 +1,30 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class Product +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public ProductTypes Type { get; set; } + + public string? Name { get; set; } + + public string? Summary { get; set; } + + public string? Description { get; set; } + + public string? ImageUrl { get; set; } + + public string[]? ThumbnailUrls { get; set; } + + public ProductMetadata? Metadata { get; set; } + + public ProductPrice? Price { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/ProductCategory.cs b/LiteCharms.Features.MidrandBooks/Products/Models/ProductCategory.cs new file mode 100644 index 0000000..38e70de --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/ProductCategory.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class ProductCategory +{ + public long Id { get; set; } + + public long ProductId { get; set; } + + public long CategoryId { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/ProductInventory.cs b/LiteCharms.Features.MidrandBooks/Products/Models/ProductInventory.cs new file mode 100644 index 0000000..3212a0e --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/ProductInventory.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class ProductInventory +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public InventoryStatuses Status { get; set; } + + public long ProductId { get; set; } + + public long ProductPriceId { get; set; } + + public int TotalAllocated { get; set; } + + public int TotalReserved { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/ProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Models/ProductPrice.cs new file mode 100644 index 0000000..a297068 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/ProductPrice.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class ProductPrice +{ + public long Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public long ProductId { get; set; } + + public decimal Amount { get; set; } + + public decimal Discount { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs new file mode 100644 index 0000000..0a5a588 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs @@ -0,0 +1,47 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public sealed record ReserveStock +{ + public required long ProductId { get; set; } + + public required long ProductPriceId { get; set; } + + public int Reservation { get; set; } +} + +public sealed record AllocateStock +{ + public required long ProductId { get; set; } + + public required long ProductPriceId { get; set; } + + public int Allocation { get; set; } +} + +public sealed record CreateProduct +{ + public required ProductTypes Type { get; set; } + + public required string Name { get; set; } + + public required string Summary { get; set; } + + public required string Description { get; set; } + + public required string ImageUrl { get; set; } + + public string[]? ThumbnailUrls { get; set; } + + public string[]? Categories { get; set; } + + public ProductMetadata? Metadata { get; set; } +} + +public sealed class CreateProductPrice +{ + public decimal Amount { get; set; } + + public decimal Discount { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs new file mode 100644 index 0000000..771632b --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -0,0 +1,467 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.MidrandBooks.Categories.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; +using LiteCharms.Features.MidrandBooks.Products.Models; +using LiteCharms.Features.Models; +using Org.BouncyCastle.Asn1.Ocsp; + +namespace LiteCharms.Features.MidrandBooks.Products; + +public sealed class ProductService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask> CheckProductStockAvailabilityAsync(long productId, long productPriceId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var inventory = await context.Inventories + .AsNoTracking() + .Where(i => i.ProductPriceId == productPriceId && i.ProductId == productId) + .OrderByDescending(o => o.Id) + .FirstOrDefaultAsync(cancellationToken); + + return inventory is not null + ? Result.Ok(inventory.ToModel()) + : Result.Fail("Product sold out"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> ReserveProductInventoryAsync(ReserveStock request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var oldInventory = await context.Inventories + .AsNoTracking() + .Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId) + .OrderByDescending(o => o.Id) + .FirstOrDefaultAsync(cancellationToken); + + var newAllocation = 0; + var newReservation = 0; + + if (oldInventory is not null) + { + newAllocation = oldInventory.TotalAllocated; + newReservation = oldInventory.TotalReserved + request.Reservation; + } + else + { + newAllocation = 0; + newReservation = request.Reservation; + } + + if (newAllocation - newReservation < 0) + return Result.Fail("Allocation failure: The requested book quantity exceeds current physical inventory availability."); + + var inventory = context.Inventories.Add(new Entities.ProductInventory + { + CreatedAt = DateTime.UtcNow, + ProductId = request.ProductId, + ProductPriceId = request.ProductPriceId, + Status = InventoryStatuses.Reserved, + TotalAllocated = newAllocation, + TotalReserved = newReservation, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(inventory.Entity.Id) + : Result.Fail("Failed to create inventory entry"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> AllocateProductInventoryAsync(AllocateStock request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var oldInventory = await context.Inventories + .AsNoTracking() + .Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId) + .OrderByDescending(o => o.Id) + .FirstOrDefaultAsync(cancellationToken); + + var newAllocation = 0; + var newReservation = 0; + + if (oldInventory is not null) + { + newAllocation = oldInventory.TotalAllocated + request.Allocation; + newReservation = oldInventory.TotalReserved; + } + else + { + newAllocation = request.Allocation; + newReservation = 0; + } + + var inventory = context.Inventories.Add(new Entities.ProductInventory + { + CreatedAt = DateTime.UtcNow, + ProductId = request.ProductId, + ProductPriceId = request.ProductPriceId, + Status = InventoryStatuses.Adjustment, + TotalAllocated = newAllocation, + TotalReserved = newReservation, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(inventory.Entity.Id) + : Result.Fail("Failed to create inventory entry"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask AddProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Products.AnyAsync(p => p.Id == productId && p.Enabled, cancellationToken)) + return Result.Fail("Product does not exist"); + + if (!await context.Categories.AnyAsync(c => c.Id == categoryId && c.Enabled, cancellationToken)) + return Result.Fail("Category does not exist"); + + if (await context.ProductCategories.AnyAsync(c => c.ProductId == productId && c.CategoryId == categoryId, cancellationToken)) + return Result.Fail("Category already assigned to product"); + + context.ProductCategories.Add(new Entities.ProductCategory + { + ProductId = productId, + CategoryId = categoryId, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Could not add category to product"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductCategoriesAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var categories = await context.ProductCategories.AsNoTracking() + .Where(p => p.ProductId == productId) + .OrderByDescending(o => o.Id) + .Select(p => p.Category) + .ToArrayAsync(cancellationToken); + + return categories?.Length > 0 + ? Result.Ok(categories.Select(c => c!.ToModel()).ToArray()) + : Result.Fail("Failed to get product categories"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeleteProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.ProductCategories + .Where(p => p.ProductId == productId && p.CategoryId == categoryId) + .ExecuteDeleteAsync(cancellationToken); + + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("No product categories were deleted"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeleteAllProductCategoriesAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.ProductCategories + .Where(p => p.ProductId == productId) + .ExecuteDeleteAsync(cancellationToken); + + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("No product categories were deleted"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Prices + .Where(p => p.Id == productPriceId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Enabled, isEnabled) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Product price with ID {productPriceId} not found")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateProductStatusAsync(long productId, bool isEnabled, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Products + .Where(p => p.Id == productId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(p => p.Enabled, isEnabled) + .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Product with ID {productId} not found")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> SearchProductsAsync(ProductFilter filter, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var query = context.Products.AsQueryable(); + + var cultureInfo = CultureInfo.InvariantCulture; + + if (!string.IsNullOrWhiteSpace(filter.Name)) + query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Name}%")); + + if (!string.IsNullOrWhiteSpace(filter.Title)) + query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Title}%")); + + if (!string.IsNullOrWhiteSpace(filter.Manufacturer)) + query = query.Where(p => EF.Functions.ILike(p.Metadata!.Manufacturer!, $"%{filter.Manufacturer}%")); + + if (!string.IsNullOrWhiteSpace(filter.SerialNumber)) + query = query.Where(p => EF.Functions.ILike(p.Metadata!.SerialNumber!, $"%{filter.SerialNumber}%")); + + if (filter.MinPrice > 0) + query = query.Where(p => p.Prices!.Any(pr => pr.Amount >= filter.MinPrice && pr.Amount <= filter.MaxPrice)); + + var products = await query.AsNoTracking().Where(p => p.Enabled).ToListAsync(cancellationToken); + + return products?.Count > 0 + ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) + : Result.Fail("No products found."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateProductPriceAsync(long productId, CreateProductPrice request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken)) + return Result.Fail($"Product with ID {productId} does not exist."); + + var existingPrices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken); + + if (existingPrices.Count > 0) + foreach (var existingPrice in existingPrices) + { + existingPrice.Enabled = false; + existingPrice.UpdatedAt = DateTime.UtcNow; + context.Prices.Update(existingPrice); + } + + var price = context.Prices.Add(new Entities.ProductPrice + { + ProductId = productId, + Amount = request.Amount, + Discount = request.Discount, + Enabled = true, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(price.Entity.Id) + : Result.Fail("Failed to create product price."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateProductAsync(CreateProduct request, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken)) + return Result.Fail("A product with the same name already exists."); + + if (request.Metadata is not null) + if (await context.Products.AnyAsync(p => p.Metadata!.SerialNumber == request.Metadata.SerialNumber, cancellationToken)) + return Result.Fail("A product with the same metadata already exists."); + + var product = context.Products.Add(new Entities.Product + { + UpdatedAt = DateTime.UtcNow, + Type = request.Type, + Name = request.Name, + Summary = request.Summary, + Description = request.Description, + ImageUrl = request.ImageUrl, + ThumbnailUrls = request.ThumbnailUrls, + Metadata = request.Metadata, + Enabled = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(product.Entity.Id) + : Result.Fail("Failed to create product."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Prices + .AsNoTracking() + .OrderByDescending(p => p.CreatedAt) + .ThenBy(p => p.UpdatedAt) + .FirstOrDefaultAsync(p => p.ProductId == productId && p.Enabled, cancellationToken); + + return product is not null + ? Result.Ok(product.ToModel()) + : Result.Fail(new Error($"No price found for product with ID {productId}")); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductPricesAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var prices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken); + + return prices?.Count > 0 + ? prices.Select(p => p.ToModel()).ToArray() + : Result.Fail(new Error($"No prices found for product with ID {productId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products + .AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + + return product is null + ? Result.Fail(new Error($"Product with ID {productId} not found.")) + : Result.Ok(product.ToModel()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductsAsync(int offset, DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var products = await context.Products + .AsNoTracking() + .Include(p => p.Prices) + .OrderByDescending(p => p.CreatedAt) + .ThenByDescending(p => p.UpdatedAt) + .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) + .Skip(offset) + .Take(range.MaxRecords) + .AsSplitQuery() + .ToArrayAsync(cancellationToken); + + return products?.Length > 0 + ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) + : Result.Fail(new Error("Failed to retrieve products.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.TechShop.Tests/Fixture.cs b/LiteCharms.Features.TechShop.Tests/Fixture.cs new file mode 100644 index 0000000..eadca78 --- /dev/null +++ b/LiteCharms.Features.TechShop.Tests/Fixture.cs @@ -0,0 +1,38 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.TechShop.Extensions; + +namespace LiteCharms.Features.TechShop.Tests; + +public class Fixture : IDisposable +{ + public IConfiguration Configuration { get; set; } + + public IServiceProvider Services { get; set; } + + public IMediator Mediator { get; set; } + + public Fixture() + { + Configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddUserSecrets() + .AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .Build(); + + Services = new ServiceCollection() + .AddMediator() + .AddLogging() + .AddTechShopServices() + .AddEmailServiceBus() + .AddGarageS3(Configuration) + .AddTechShopDatabase(Configuration) + .AddEmailServices(Configuration) + .AddSingleton(Configuration) + .BuildServiceProvider(); + + Mediator = Services.GetRequiredService(); + } + + public void Dispose() { } +} diff --git a/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj b/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj new file mode 100644 index 0000000..71063cb --- /dev/null +++ b/LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj @@ -0,0 +1,51 @@ + + + + net10.0 + enable + enable + false + fa06c1a6-2ba8-4c47-b19b-11e0f78e7b92 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + Always + + + + \ No newline at end of file diff --git a/LiteCharms.Features.TechShop.Tests/NotificationsFeatureTests.cs b/LiteCharms.Features.TechShop.Tests/NotificationsFeatureTests.cs new file mode 100644 index 0000000..6a447fa --- /dev/null +++ b/LiteCharms.Features.TechShop.Tests/NotificationsFeatureTests.cs @@ -0,0 +1,65 @@ +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Notifications; +using LiteCharms.Features.TechShop.Notifications.Events; +using static LiteCharms.Features.Extensions.Email; + +namespace LiteCharms.Features.TechShop.Tests; + +public class NotificationsFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture +{ + private readonly NotificationService notificationService = fixture.Services.GetRequiredService(); + + [Fact] + public async Task CreateNotificationCommand_ShouldSucceed() + { + Notifications.Models.CreateNotification request = new() + { + CorrelationId = Guid.CreateVersion7().ToString(), + CorrelationIdType = CorrelationIdTypes.None, + Direction = NotificationDirection.Outgoing, + Platform = NotificationPlatforms.Email, + Priority = Priorities.Medium, + Sender = "xUnit Test", + SenderAddress = "khwezi@mngoma.africa", + Recipient = $"{ShopEmailFromName} [Test]", + RecipientAddress = ShopEmailFromAddress, + Subject = "Test Message", + Message = "This is an automation test", + IsHtml = false, + IsInternal = true, + }; + + var createResult = await notificationService.CreateNotificationAsync(request); + + Assert.True(createResult.IsSuccess); + + foreach (var error in createResult.Errors) output.WriteLine(error.Message); + } + + [Fact] + public async Task GetNotifications_ShouldReturn_AllNotifications() + { + DateRange range = new() + { + From = DateOnly.FromDateTime(new DateTime(2026, 04, 01, 0, 0, 0, DateTimeKind.Utc)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 10 + }; + + var getResult = await notificationService.GetNotificationsAsync(range); + + Assert.True(getResult.IsSuccess); + + foreach (var error in getResult.Errors) output.WriteLine(error.Message); + } + + [Fact] + public async Task ProcessEmailNotificationsEvent_ShouldSucceed() + { + var notification = ProcessEmailNotificationsEvent.Create(); + + await fixture.Mediator.Publish(notification); + + Assert.True(true); + } +} diff --git a/LiteCharms.Features.TechShop.Tests/ProductsFeatureTests.cs b/LiteCharms.Features.TechShop.Tests/ProductsFeatureTests.cs new file mode 100644 index 0000000..2386847 --- /dev/null +++ b/LiteCharms.Features.TechShop.Tests/ProductsFeatureTests.cs @@ -0,0 +1,19 @@ +using LiteCharms.Features.TechShop.Products; + +namespace LiteCharms.Features.TechShop.Tests; + +public class ProductsFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture +{ + [Fact] + public async Task GetProductsAsync_ReturnsProducts() + { + var productService = fixture.Services.GetRequiredService(); + + var result = await productService.GetProductsAsync(); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + + output.WriteLine($"Retrieved {result.Value.Length} products."); + } +} diff --git a/LiteCharms.Features.TechShop.Tests/appsettings.json b/LiteCharms.Features.TechShop.Tests/appsettings.json new file mode 100644 index 0000000..1066af9 --- /dev/null +++ b/LiteCharms.Features.TechShop.Tests/appsettings.json @@ -0,0 +1,34 @@ +{ + "BookshopS3Settings": { + "ServiceUrl": "http://192.168.1.177:30900", + "Region": "garage", + "BucketName": "bookshop", + "CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za" + }, + "BookshopQuotesS3Settings": { + "ServiceUrl": "http://192.168.1.177:30900", + "Region": "garage", + "BucketName": "bookshop.quotes", + "CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za" + }, + "Email": { + "Credentials": { + "Username": "shop@litecharms.co.za" + }, + "Port": 465, + "Host": "mail.litecharms.co.za", + "UseSsl": true + }, + "Monitoring": { + "ApiKey": "", + "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", + "ServiceName": "LiteCharms.LeadGenerator" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/LiteCharms.Features.TechShop/CartPackages/Entities/Package.cs b/LiteCharms.Features.TechShop/CartPackages/Entities/Package.cs new file mode 100644 index 0000000..147b6f0 --- /dev/null +++ b/LiteCharms.Features.TechShop/CartPackages/Entities/Package.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.TechShop.CartPackages.Entities; + +[EntityTypeConfiguration] +public class Package : Models.Package +{ + public virtual ICollection? PackageItems { get; set; } +} diff --git a/LiteCharms.Features.TechShop/CartPackages/Entities/PackageConfirguration.cs b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageConfirguration.cs new file mode 100644 index 0000000..3f1b9dd --- /dev/null +++ b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageConfirguration.cs @@ -0,0 +1,18 @@ +namespace LiteCharms.Features.TechShop.CartPackages.Entities; + +public class PackageConfirguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Package)); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); + builder.Property(f => f.Name).IsRequired(); + builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); + builder.Property(f => f.Description).IsRequired().HasMaxLength(2048); + builder.Property(f => f.ImageUrl).IsRequired(false).HasMaxLength(2048); + builder.Property(f => f.Active); + } +} diff --git a/LiteCharms.Features.TechShop/CartPackages/Entities/PackageItem.cs b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageItem.cs new file mode 100644 index 0000000..8a8715f --- /dev/null +++ b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageItem.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.TechShop.Products.Entities; + +namespace LiteCharms.Features.TechShop.CartPackages.Entities; + +[EntityTypeConfiguration] +public class PackageItem : Models.PackageItem +{ + public virtual Package? Package { get; set; } + + public virtual ProductPrice? ProductPrice { get; set; } +} diff --git a/LiteCharms.Features.TechShop/CartPackages/Entities/PackageItemConfiguration.cs b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageItemConfiguration.cs new file mode 100644 index 0000000..c77222a --- /dev/null +++ b/LiteCharms.Features.TechShop/CartPackages/Entities/PackageItemConfiguration.cs @@ -0,0 +1,27 @@ +namespace LiteCharms.Features.TechShop.CartPackages.Entities; + +public class PackageItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(PackageItem)); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.PackageId).IsRequired(); + builder.Property(f => f.ProductPriceId).IsRequired(); + builder.Property(f => f.Active); + + builder.HasOne(f => f.Package) + .WithMany(f => f.PackageItems) + .HasForeignKey(pi => pi.PackageId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.ProductPrice) + .WithMany() + .HasForeignKey(pi => pi.ProductPriceId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.TechShop/CartPackages/Models/Package.cs b/LiteCharms.Features.TechShop/CartPackages/Models/Package.cs new file mode 100644 index 0000000..10f8224 --- /dev/null +++ b/LiteCharms.Features.TechShop/CartPackages/Models/Package.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.TechShop.CartPackages.Models; + +public class Package +{ + public Guid Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Name { get; set; } + + public string? Summary { get; set; } + + public string? Description { get; set; } + + public string? ImageUrl { get; set; } + + public bool Active { get; set; } +} diff --git a/LiteCharms.Features.TechShop/CartPackages/Models/PackageItem.cs b/LiteCharms.Features.TechShop/CartPackages/Models/PackageItem.cs new file mode 100644 index 0000000..795b759 --- /dev/null +++ b/LiteCharms.Features.TechShop/CartPackages/Models/PackageItem.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.TechShop.CartPackages.Models; + +public class PackageItem +{ + public Guid Id { get; set; } + + public Guid PackageId { get; set; } + + public Guid ProductPriceId { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public bool Active { get; set; } +} diff --git a/LiteCharms.Features.TechShop/CartPackages/PackageService.cs b/LiteCharms.Features.TechShop/CartPackages/PackageService.cs new file mode 100644 index 0000000..da2f986 --- /dev/null +++ b/LiteCharms.Features.TechShop/CartPackages/PackageService.cs @@ -0,0 +1,242 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.CartPackages.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Postgres; + +namespace LiteCharms.Features.TechShop.CartPackages; + +public class PackageService(IDbContextFactory contextFactory) +{ + public async ValueTask> AddPackageItemAsync(Guid packageId, Guid productPriceId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == packageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {packageId}"); + + if (!await context.ProductPrices.AnyAsync(p => p.Id == productPriceId && p.Active == true, cancellationToken)) + return Result.Fail($"Could not find an active product price by ID {productPriceId}"); + + if (await context.PackageItems.AnyAsync(p => p.ProductPriceId == productPriceId && p.PackageId == packageId, cancellationToken)) + return Result.Fail($"Product price {productPriceId} is already added to this package {packageId}"); + + var newPackageItem = context.PackageItems.Add(new Entities.PackageItem + { + PackageId = packageId, + ProductPriceId = productPriceId, + Active = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newPackageItem.Entity.Id) + : Result.Fail($"Failed to add new package item by ID {productPriceId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreatePackageAsync(string? name, string? summary, string? description, string? ImageUrl, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Packages.AnyAsync(p => p.Name == name, cancellationToken)) + return Result.Fail($"A package by the same name already exists: {name}"); + + var newPackage = context.Packages.Add(new Entities.Package + { + Name = name, + Summary = summary, + Description = description, + ImageUrl = ImageUrl, + Active = true + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newPackage.Entity.Id) + : Result.Fail($"Failed to create a new package by the name: {name}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeletePackageItemAsync(Guid packageId, Guid packageItemId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == packageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {packageId}"); + + var item = await context.PackageItems.FirstOrDefaultAsync(p => p.Id == packageItemId && p.PackageId == packageId, cancellationToken); + + if (item is null) + return Result.Fail($"Product item {packageItemId} is already added to this package {packageId}"); + + context.PackageItems.Remove(item); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to delete package item by id {packageItemId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeletePackageItemsAsync(Guid packageId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == packageId, cancellationToken)) + return Result.Fail($"Could not find package by ID {packageId}"); + + var items = await context.PackageItems.Where(i => i.PackageId == packageId).ToArrayAsync(cancellationToken); + + context.PackageItems.RemoveRange(items); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to delete package {packageId} items"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPackageAsync(Guid packageId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == packageId, cancellationToken); + + return package is not null + ? Result.Ok(package.ToModel()) + : Result.Fail($"Failed to find package by ID {packageId}"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPackageItemsAsync(Guid packageId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == packageId, cancellationToken)) + return Result.Fail($"Package could not be found with ID {packageId}"); + + var items = await context.PackageItems.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(p => p.PackageId == packageId) + .ToArrayAsync(cancellationToken); + + return items?.Length > 0 + ? Result.Ok(items.Select(i => i.ToModel()).ToArray()) + : Result.Fail($"Could not find package items by package ID {packageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetPackagesAsync(Guid packageId, DateRange range, bool active, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var packages = await context.Packages + .AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) + .Where(p => p.Active == active) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return packages?.Length > 0 + ? Result.Ok(packages.Select(o => o.ToModel()).ToArray()) + : Result.Fail(new Error($"No packages found for the specified date range {range.From} - {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> UpdatePackageAsync(Guid packageId, string? name, string? summary, string? description, string? ImageUrl, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Packages.AnyAsync(p => p.Name == name, cancellationToken)) + return Result.Fail($"A package by the same name already exists: {name}"); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == packageId, cancellationToken); + + if (package is null) + return Result.Fail($"Could not find package by id {packageId}"); + + package.Name = name; + package.Summary = summary; + package.Description = description; + package.ImageUrl = ImageUrl; + package.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update package with id {packageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> UpdatePackageStatusAsync(Guid packageId, bool active, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var package = await context.Packages.FirstOrDefaultAsync(p => p.Id == packageId, cancellationToken); + + if (package is null) + return Result.Fail($"Could not find package by id {packageId}"); + + package.Active = active; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update package with id {packageId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.TechShop/Customers/CustomerService.cs b/LiteCharms.Features.TechShop/Customers/CustomerService.cs new file mode 100644 index 0000000..c4983c6 --- /dev/null +++ b/LiteCharms.Features.TechShop/Customers/CustomerService.cs @@ -0,0 +1,132 @@ +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Customers.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Postgres; + +namespace LiteCharms.Features.TechShop.Customers; + +public class CustomerService(IDbContextFactory contextFactory) +{ + public async ValueTask> CreateCustomerAsync(CreateCustomer request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customerEmail = request.Email.ToLower().Trim(); + + if (await context.Customers.AnyAsync(c => c.Email == customerEmail, cancellationToken)) + return Result.Fail(new Error($"A customer with the email {customerEmail} already exists")); + + var newCustomer = context.Customers.Add(new Entities.Customer + { + Company = request.Company, + Name = request.Name, + LastName = request.LastName, + Tax = request.Tax, + Email = customerEmail, + Discord = request.Discord, + Slack = request.Slack, + LinkedIn = request.LinkedIn, + Whatsapp = request.Whatsapp, + Website = request.Website, + Phone = request.Phone, + Address = request.Address, + City = request.City, + Region = request.Region, + Country = request.Country, + PostalCode = request.PostalCode, + Active = true, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newCustomer.Entity.Id) + : Result.Fail(new Error($"Failed to create customer {customerEmail}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerAsync(Guid customerId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); + + return customer is not null + ? Result.Ok(customer.ToModel()) + : Result.Fail($"Customer not found with id {customerId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomersAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customers = await context.Customers.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(c => c.CreatedAt >= fromDate && c.CreatedAt <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return customers?.Length > 0 + ? Result.Ok(customers.Select(c => c.ToModel()).ToArray()) + : Result.Fail(new Error("No customers found in the specified date range.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateCustomerAsync(UpdateCustomer request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == request.CustomerId, cancellationToken); + + if (customer is null) + return Result.Fail(new Error($"Customer with ID {request.CustomerId} not found.")); + + customer.Name = request.Name; + customer.LastName = request.LastName; + customer.Email = request.Email; + customer.Company = request.Company; + customer.Address = request.Address; + customer.City = request.City; + customer.Region = request.Region; + customer.Country = request.Country; + customer.PostalCode = request.PostalCode; + customer.Phone = request.Phone; + customer.Tax = request.Tax; + customer.City = request.City; + customer.Discord = request.Discord; + customer.Slack = request.Slack; + customer.LinkedIn = request.LinkedIn; + customer.Whatsapp = request.Whatsapp; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update the customer {request.CustomerId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Entities/Customer.cs b/LiteCharms.Features.TechShop/Customers/Entities/Customer.cs similarity index 56% rename from LiteCharms.Entities/Customer.cs rename to LiteCharms.Features.TechShop/Customers/Entities/Customer.cs index bd3bf40..d021a92 100644 --- a/LiteCharms.Entities/Customer.cs +++ b/LiteCharms.Features.TechShop/Customers/Entities/Customer.cs @@ -1,6 +1,9 @@ -using LiteCharms.Entities.Configuration; +using LiteCharms.Features.TechShop.Leads.Entities; +using LiteCharms.Features.TechShop.Orders.Entities; +using LiteCharms.Features.TechShop.Quotes.Entities; +using LiteCharms.Features.TechShop.ShoppingCarts.Entities; -namespace LiteCharms.Entities; +namespace LiteCharms.Features.TechShop.Customers.Entities; [EntityTypeConfiguration] public class Customer : Models.Customer diff --git a/LiteCharms.Entities/Configuration/CustomerConfiguration.cs b/LiteCharms.Features.TechShop/Customers/Entities/CustomerConfiguration.cs similarity index 74% rename from LiteCharms.Entities/Configuration/CustomerConfiguration.cs rename to LiteCharms.Features.TechShop/Customers/Entities/CustomerConfiguration.cs index 9792251..e6a23a0 100644 --- a/LiteCharms.Entities/Configuration/CustomerConfiguration.cs +++ b/LiteCharms.Features.TechShop/Customers/Entities/CustomerConfiguration.cs @@ -1,14 +1,14 @@ -namespace LiteCharms.Entities.Configuration; +namespace LiteCharms.Features.TechShop.Customers.Entities; public class CustomerConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Customer)); + builder.ToTable("Customers"); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); + builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); builder.Property(f => f.Company); builder.Property(f => f.Name).IsRequired(); builder.Property(f => f.LastName).IsRequired(); @@ -26,10 +26,5 @@ public class CustomerConfiguration : IEntityTypeConfiguration builder.Property(f => f.Country); builder.Property(f => f.PostalCode); builder.Property(f => f.Active).HasDefaultValue(true); - - builder.HasMany(f => f.Leads) - .WithOne(f => f.Customer) - .HasForeignKey(f => f.CustomerId) - .OnDelete(DeleteBehavior.NoAction); } } \ No newline at end of file diff --git a/LiteCharms.Models/Customer.cs b/LiteCharms.Features.TechShop/Customers/Models/Customer.cs similarity index 83% rename from LiteCharms.Models/Customer.cs rename to LiteCharms.Features.TechShop/Customers/Models/Customer.cs index b322f8a..2904a3e 100644 --- a/LiteCharms.Models/Customer.cs +++ b/LiteCharms.Features.TechShop/Customers/Models/Customer.cs @@ -1,12 +1,12 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.TechShop.Customers.Models; public class Customer { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public string? Company { get; set; } diff --git a/LiteCharms.Features.TechShop/Customers/Models/Records.cs b/LiteCharms.Features.TechShop/Customers/Models/Records.cs new file mode 100644 index 0000000..35c8e45 --- /dev/null +++ b/LiteCharms.Features.TechShop/Customers/Models/Records.cs @@ -0,0 +1,73 @@ +namespace LiteCharms.Features.TechShop.Customers.Models; + +public record CreateCustomer +{ + public string? Company { get; set; } + + public required string Name { get; set; } + + public required string LastName { get; set; } + + public string? Tax { get; set; } + + public required string Email { get; set; } + + public string? Discord { get; set; } + + public string? Slack { get; set; } + + public string? LinkedIn { get; set; } + + public string? Whatsapp { get; set; } + + public string? Website { get; set; } + + public string? Phone { get; set; } + + public string? Address { get; set; } + + public string? City { get; set; } + + public string? Region { get; set; } + + public string? Country { get; set; } + + public string? PostalCode { get; set; } +} + +public record UpdateCustomer +{ + public required Guid CustomerId { get; set; } + + public string? Company { get; set; } + + public string? Name { get; set; } + + public string? LastName { get; set; } + + public string? Tax { get; set; } + + public string? Email { get; set; } + + public string? Discord { get; set; } + + public string? Slack { get; set; } + + public string? LinkedIn { get; set; } + + public string? Whatsapp { get; set; } + + public string? Website { get; set; } + + public string? Phone { get; set; } + + public string? Address { get; set; } + + public string? City { get; set; } + + public string? Region { get; set; } + + public string? Country { get; set; } + + public string? PostalCode { get; set; } +} \ No newline at end of file diff --git a/LiteCharms.Models/Enums.cs b/LiteCharms.Features.TechShop/Enums.cs similarity index 55% rename from LiteCharms.Models/Enums.cs rename to LiteCharms.Features.TechShop/Enums.cs index 96bda99..d228c10 100644 --- a/LiteCharms.Models/Enums.cs +++ b/LiteCharms.Features.TechShop/Enums.cs @@ -1,4 +1,28 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.TechShop; + +public enum CorrelationIdTypes : int +{ + None = 0, + Email = 1, + Discord = 2, + Slack = 3, + Whatsapp = 4, + Customer = 5, + Order = 6, + Refund = 7, + Lead = 8, + Quote = 9, + LinkedIn = 10 +} + +public enum NotificationPlatforms : int +{ + Email = 1, + Discord = 2, + Slack = 3, + WhatsApp = 4, + System = 5 +} public enum QuoteStatus : int { @@ -35,3 +59,4 @@ public enum NotificationDirection : int Outgoing = 1, Neutral = 2 } + diff --git a/LiteCharms.Features.TechShop/Extensions/HealthChecks.cs b/LiteCharms.Features.TechShop/Extensions/HealthChecks.cs new file mode 100644 index 0000000..a09ffec --- /dev/null +++ b/LiteCharms.Features.TechShop/Extensions/HealthChecks.cs @@ -0,0 +1,22 @@ +using LiteCharms.Features.TechShop.HealthChecks; +using static LiteCharms.Features.TechShop.Extensions.Postgres; + +namespace LiteCharms.Features.TechShop.Extensions; + +public static class HealthChecks +{ + public static IServiceCollection AddShopQuartzHealthCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck("ShopQuartz"); + + return services; + } + + + public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services) + { + services.AddHealthChecks().AddCheck(TechShopDbConfigName); + + return services; + } +} diff --git a/LiteCharms.Features.TechShop/Extensions/Mappers.cs b/LiteCharms.Features.TechShop/Extensions/Mappers.cs new file mode 100644 index 0000000..2190f0e --- /dev/null +++ b/LiteCharms.Features.TechShop/Extensions/Mappers.cs @@ -0,0 +1,202 @@ +using LiteCharms.Features.TechShop.CartPackages.Models; +using LiteCharms.Features.TechShop.Customers.Models; +using LiteCharms.Features.TechShop.Leads.Models; +using LiteCharms.Features.TechShop.Notifications.Models; +using LiteCharms.Features.TechShop.Orders.Models; +using LiteCharms.Features.TechShop.Products.Entities; +using LiteCharms.Features.TechShop.Products.Models; +using LiteCharms.Features.TechShop.Quotes.Models; +using LiteCharms.Features.TechShop.ShoppingCarts.Models; + +namespace LiteCharms.Features.TechShop.Extensions; + +public static class Mappers +{ + public static ShoppingCartPackage ToModel(this ShoppingCarts.Entities.ShoppingCartPackage entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + PackageId = entity.PackageId, + ShoppingCartId = entity.ShoppingCartId + }; + + public static PackageItem ToModel(this CartPackages.Entities.PackageItem entity) => + new() + { + Id = entity.Id, + Active = entity.Active, + CreatedAt = entity.CreatedAt, + PackageId = entity.PackageId, + ProductPriceId = entity.ProductPriceId + }; + + public static Package ToModel(this CartPackages.Entities.Package entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + Active = entity.Active, + Description = entity.Description, + Name = entity.Name, + UpdatedAt = entity.UpdatedAt, + ImageUrl = entity.ImageUrl, + Summary = entity.Summary + }; + + public static ShoppingCartItem ToModel(this ShoppingCarts.Entities.ShoppingCartItem entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + ProductPriceId = entity.ProductPriceId, + Quantity = entity.Quantity, + ShoppingCartId = entity.ShoppingCartId + }; + + public static ShoppingCart ToModel(this ShoppingCarts.Entities.ShoppingCart entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + CustomerId = entity.CustomerId, + OrderId = entity.OrderId + }; + + public static Quote ToModel(this Quotes.Entities.Quote entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + CustomerId = entity.CustomerId, + ExpiredAt = entity.ExpiredAt, + Reason = entity.Reason, + ShoppingCartId = entity.ShoppingCartId, + Status = entity.Status, + InvoiceUrl = entity.InvoiceUrl, + OrderId = entity.OrderId + }; + + public static Notification ToModel(this Notifications.Entities.Notification entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + Message = entity.Message, + Direction = entity.Direction, + CorrelationId = entity.CorrelationId, + CorrelationIdType = entity.CorrelationIdType, + IsInternal = entity.IsInternal, + SenderAddress = entity.SenderAddress, + Platform = entity.Platform, + RecipientName = entity.RecipientName, + Subject = entity.Subject, + 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 Customers.Entities.Customer entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Active = entity.Active, + Address = entity.Address, + City = entity.City, + Company = entity.Company, + Country = entity.Country, + Discord = entity.Discord, + Email = entity.Email, + LastName = entity.LastName, + LinkedIn = entity.LinkedIn, + Name = entity.Name, + Phone = entity.Phone, + PostalCode = entity.PostalCode, + Region = entity.Region, + Slack = entity.Slack, + Tax = entity.Tax, + Website = entity.Website, + Whatsapp = entity.Whatsapp + }; + + public static Lead ToModel(this Leads.Entities.Lead entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + AdGroupId = entity.AdGroupId, + AdName = entity.AdName, + AppClickId = entity.AppClickId, + AttributionHash = entity.AttributionHash, + CampaignId = entity.CampaignId, + ClickLocation = entity.ClickLocation, + CustomerId = entity.CustomerId, + FeedItemId = entity.FeedItemId, + Source = entity.Source, + ClickId = entity.ClickId, + TargetId = entity.TargetId, + WebClickId = entity.WebClickId, + Status = entity.Status + }; + + public static Order ToModel(this Orders.Entities.Order entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + CustomerId = entity.CustomerId, + Notes = entity.Notes, + Status = entity.Status, + Requirements = entity.Requirements, + Terms = entity.Terms, + InvoiceUrl = entity.InvoiceUrl + }; + + public static OrderRefund ToModel(this Orders.Entities.OrderRefund entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + OrderId = entity.OrderId, + Reason = entity.Reason, + Amount = entity.Amount + }; + + public static Products.Models.Product ToModel(this Products.Entities.Product entity) => + new() + { + Id = entity.Id, + CreatedAt = entity.CreatedAt, + Name = entity.Name, + Description = entity.Description, + Active = entity.Active, + Summary = entity.Summary, + ImageUrl = entity.ImageUrl, + Thumbnails = entity.Thumbnails, + Metadata = entity.Metadata, + }; + + public static Products.Models.ProductPrice ToModel(this Products.Entities.ProductPrice entity) => + new() + { + Id = entity.Id, + ProductId = entity.ProductId, + Price = entity.Price, + Active = entity.Active, + CreatedAt = entity.CreatedAt, + Discount = entity.Discount, + UpdatedAt = entity.UpdatedAt + }; +} diff --git a/LiteCharms.Features.TechShop/Extensions/Postgres.cs b/LiteCharms.Features.TechShop/Extensions/Postgres.cs new file mode 100644 index 0000000..fdf8555 --- /dev/null +++ b/LiteCharms.Features.TechShop/Extensions/Postgres.cs @@ -0,0 +1,16 @@ +using LiteCharms.Features.TechShop.Postgres; + +namespace LiteCharms.Features.TechShop.Extensions; + +public static class Postgres +{ + public const string TechShopDbConfigName = "PostgresShop"; + + public static IServiceCollection AddTechShopDatabase(this IServiceCollection services, IConfiguration configuration) + { + services.AddPooledDbContextFactory(options => + options.UseNpgsql(configuration.GetConnectionString(TechShopDbConfigName))); + + return services; + } +} diff --git a/LiteCharms.Features.TechShop/Extensions/Shop.cs b/LiteCharms.Features.TechShop/Extensions/Shop.cs new file mode 100644 index 0000000..fd96fc6 --- /dev/null +++ b/LiteCharms.Features.TechShop/Extensions/Shop.cs @@ -0,0 +1,27 @@ +using LiteCharms.Features.TechShop.CartPackages; +using LiteCharms.Features.TechShop.Customers; +using LiteCharms.Features.TechShop.Leads; +using LiteCharms.Features.TechShop.Notifications; +using LiteCharms.Features.TechShop.Orders; +using LiteCharms.Features.TechShop.Products; +using LiteCharms.Features.TechShop.Quotes; +using LiteCharms.Features.TechShop.ShoppingCarts; + +namespace LiteCharms.Features.TechShop.Extensions; + +public static class Shop +{ + public static IServiceCollection AddTechShopServices(this IServiceCollection services) + { + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + return services; + } +} diff --git a/LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs b/LiteCharms.Features.TechShop/HealthChecks/PostgresShopHealthCheck.cs similarity index 60% rename from LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs rename to LiteCharms.Features.TechShop/HealthChecks/PostgresShopHealthCheck.cs index 6680c17..6e69231 100644 --- a/LiteCharms.Infrastructure/HealthChecks/PostgresHealthCheck.cs +++ b/LiteCharms.Features.TechShop/HealthChecks/PostgresShopHealthCheck.cs @@ -1,8 +1,10 @@ -namespace LiteCharms.Infrastructure.HealthChecks; +using static LiteCharms.Features.TechShop.Extensions.Postgres; -public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck +namespace LiteCharms.Features.TechShop.HealthChecks; + +public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthCheck { - private readonly string connectionString = configuration.GetConnectionString("PostgresLeadGenerator")!; + private readonly string connectionString = configuration.GetConnectionString(TechShopDbConfigName)!; public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { @@ -16,11 +18,11 @@ public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck await command.ExecuteScalarAsync(cancellationToken); - return HealthCheckResult.Healthy("PostgreSQL is responsive."); + return HealthCheckResult.Healthy($"{TechShopDbConfigName} is responsive."); } catch (Exception ex) { - return HealthCheckResult.Unhealthy("PostgreSQL is unreachable.", ex); + return HealthCheckResult.Unhealthy($"{TechShopDbConfigName} is unreachable.", ex); } } } \ No newline at end of file diff --git a/LiteCharms.Features.TechShop/HealthChecks/ShopQuartzHealthCheck.cs b/LiteCharms.Features.TechShop/HealthChecks/ShopQuartzHealthCheck.cs new file mode 100644 index 0000000..3fd4e20 --- /dev/null +++ b/LiteCharms.Features.TechShop/HealthChecks/ShopQuartzHealthCheck.cs @@ -0,0 +1,28 @@ +using static LiteCharms.Features.Extensions.Quartz; + +namespace LiteCharms.Features.TechShop.HealthChecks; + +public class ShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck +{ + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var scheduler = await schedulerFactory.GetScheduler(TechShopSchedulerName, cancellationToken); + + if(scheduler == null) + return HealthCheckResult.Unhealthy($"Scheduler with name '{TechShopSchedulerName}' not found."); + + if (!scheduler.IsStarted) + return HealthCheckResult.Unhealthy($"{TechShopSchedulerName} Quartz scheduler is not running"); + + await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken); + + return HealthCheckResult.Healthy($"{TechShopSchedulerName} Quartz scheduler is ready"); + } + catch (SchedulerException) + { + return HealthCheckResult.Unhealthy($"{TechShopSchedulerName} Quartz scheduler cannot connect to the store"); + } + } +} diff --git a/LiteCharms.Entities/Lead.cs b/LiteCharms.Features.TechShop/Leads/Entities/Lead.cs similarity index 55% rename from LiteCharms.Entities/Lead.cs rename to LiteCharms.Features.TechShop/Leads/Entities/Lead.cs index b1bdf62..8ce5357 100644 --- a/LiteCharms.Entities/Lead.cs +++ b/LiteCharms.Features.TechShop/Leads/Entities/Lead.cs @@ -1,6 +1,6 @@ -using LiteCharms.Entities.Configuration; +using LiteCharms.Features.TechShop.Customers.Entities; -namespace LiteCharms.Entities; +namespace LiteCharms.Features.TechShop.Leads.Entities; [EntityTypeConfiguration] public class Lead : Models.Lead diff --git a/LiteCharms.Entities/Configuration/LeadConfiguration.cs b/LiteCharms.Features.TechShop/Leads/Entities/LeadConfiguration.cs similarity index 65% rename from LiteCharms.Entities/Configuration/LeadConfiguration.cs rename to LiteCharms.Features.TechShop/Leads/Entities/LeadConfiguration.cs index 8c8d004..c5beca0 100644 --- a/LiteCharms.Entities/Configuration/LeadConfiguration.cs +++ b/LiteCharms.Features.TechShop/Leads/Entities/LeadConfiguration.cs @@ -1,15 +1,15 @@ -namespace LiteCharms.Entities.Configuration; +namespace LiteCharms.Features.TechShop.Leads.Entities; public class LeadConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(Lead)); + builder.ToTable("Leads"); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); - builder.Property(f => f.CustomerId).IsRequired(false); + builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); + builder.Property(f => f.CustomerId); builder.Property(f => f.Source); builder.Property(f => f.ClickId); builder.Property(f => f.WebClickId); @@ -22,5 +22,10 @@ public class LeadConfiguration : IEntityTypeConfiguration builder.Property(f => f.ClickLocation); builder.Property(f => f.Status).IsRequired(); builder.Property(f => f.AttributionHash).IsRequired(true); + + builder.HasOne(f => f.Customer) + .WithMany(f => f.Leads) + .HasForeignKey(f => f.CustomerId) + .OnDelete(DeleteBehavior.Restrict); } } diff --git a/LiteCharms.Features.TechShop/Leads/LeadService.cs b/LiteCharms.Features.TechShop/Leads/LeadService.cs new file mode 100644 index 0000000..ecaf004 --- /dev/null +++ b/LiteCharms.Features.TechShop/Leads/LeadService.cs @@ -0,0 +1,117 @@ +using LiteCharms.Features.Hasher; +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Leads.Models; +using LiteCharms.Features.TechShop.Postgres; + +namespace LiteCharms.Features.TechShop.Leads; + +public class LeadService(IDbContextFactory contextFactory) +{ + public async ValueTask> CreateLeadAsync(CreateLead request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var newLead = context.Leads.Add(new Entities.Lead + { + WebClickId = request.WebClickId, + AppClickId = request.AppClickId, + Source = request.Source, + ClickId = request.ClickId, + AdGroupId = request.AdGroupId, + AdName = request.AdName, + CampaignId = request.CampaignId, + ClickLocation = request.ClickLocation, + CustomerId = request.CustomerId, + FeedItemId = request.FeedItemId, + Status = LeadStatus.New, + TargetId = request.TargetId, + AttributionHash = HashService.StringToSha256Hash($"{request.ClickId}{request.AppClickId}{request.WebClickId}") + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newLead.Entity.Id) + : Result.Fail(new Error($"Failed to create lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerLeadsAsync(Guid customerId, DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var leads = await context.Leads.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(lead => lead.CustomerId == customerId) + .Where(lead => lead.CreatedAt.Date >= fromDate && lead.CreatedAt.Date <= toDate) + .ToArrayAsync(cancellationToken); + + return leads?.Length > 0 + ? Result.Ok(leads.Select(l => l.ToModel()).ToArray()) + : Result.Fail(new Error($"No customer {customerId} leads found for the specified date range {range.From} to {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetLeadsAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var leads = await context.Leads.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(l => l.CreatedAt.Date >= fromDate && l.CreatedAt.Date <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return leads?.Length > 0 + ? Result.Ok(leads.Select(l => l.ToModel()).ToArray()) + : Result.Fail(new Error($"No leads found for the specified date range {range.From} to {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateLeadAsync(Guid leadId, LeadStatus status, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var lead = await context.Leads.FirstOrDefaultAsync(l => l.Id == leadId, cancellationToken); + + if (lead is null) + return Result.Fail(new Error($"Lead with ID {leadId} not found.")); + + lead.Status = status; + lead.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update the lead {leadId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Models/Lead.cs b/LiteCharms.Features.TechShop/Leads/Models/Lead.cs similarity index 81% rename from LiteCharms.Models/Lead.cs rename to LiteCharms.Features.TechShop/Leads/Models/Lead.cs index 5222114..71bd6b7 100644 --- a/LiteCharms.Models/Lead.cs +++ b/LiteCharms.Features.TechShop/Leads/Models/Lead.cs @@ -1,12 +1,12 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.TechShop.Leads.Models; public class Lead { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public Guid? CustomerId { get; set; } diff --git a/LiteCharms.Features.TechShop/Leads/Models/Records.cs b/LiteCharms.Features.TechShop/Leads/Models/Records.cs new file mode 100644 index 0000000..0d84a14 --- /dev/null +++ b/LiteCharms.Features.TechShop/Leads/Models/Records.cs @@ -0,0 +1,28 @@ +namespace LiteCharms.Features.TechShop.Leads.Models; + +public record CreateLead +{ + public Guid? CustomerId { get; set; } + + public required string Source { get; set; } + + public required string ClickId { get; set; } + + public required string WebClickId { get; set; } + + public required string AppClickId { get; set; } + + public long? CampaignId { get; set; } + + public long? AdGroupId { get; set; } + + public long? AdName { get; set; } + + public long? TargetId { get; set; } + + public long? FeedItemId { get; set; } + + public string? ClickLocation { get; set; } + + public string? AttribusionHash { get; set; } +} diff --git a/LiteCharms.Extensions/LiteCharms.Extensions.csproj b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj similarity index 55% rename from LiteCharms.Extensions/LiteCharms.Extensions.csproj rename to LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj index 3f531b7..5d01984 100644 --- a/LiteCharms.Extensions/LiteCharms.Extensions.csproj +++ b/LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj @@ -6,26 +6,16 @@ enable True ..\LiteCharms.snk - true + fbd8f4a2-0420-44e2-baff-4678d9e7eee1 - - - $(NoWarn);MA0004 - - $(NoWarn);AD0001 - true - $(NoWarn);IL2080;IL2065;IL2075;IL2087;IL2057;IL2060;IL2070;IL2067;IL2072;IL2026;IL2104 - $(NoWarn);IL2110;IL2111 - - - LiteCharms.Extensions + LiteCharms.Features.TechShop 1.0.20 Khwezi Mngoma Lite Charms (PTY) Ltd - Extension components for Lite Charms applications. + TechShop feature components for Lite Charms applications. https://gitea.khongisa.co.za/litecharms/components https://gitea.khongisa.co.za/litecharms/components.git git @@ -33,12 +23,43 @@ utility;dotnet icon.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -46,10 +67,9 @@ - - - - + + + @@ -58,12 +78,12 @@ - - + + - + @@ -75,17 +95,17 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -94,20 +114,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + - - - - + + PreserveNewest + - + diff --git a/LiteCharms.Entities/Notification.cs b/LiteCharms.Features.TechShop/Notifications/Entities/Notification.cs similarity index 60% rename from LiteCharms.Entities/Notification.cs rename to LiteCharms.Features.TechShop/Notifications/Entities/Notification.cs index 2f3bf11..b94b428 100644 --- a/LiteCharms.Entities/Notification.cs +++ b/LiteCharms.Features.TechShop/Notifications/Entities/Notification.cs @@ -1,6 +1,4 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; +namespace LiteCharms.Features.TechShop.Notifications.Entities; [EntityTypeConfiguration] public class Notification : Models.Notification; diff --git a/LiteCharms.Features.TechShop/Notifications/Entities/NotificationConfiguration.cs b/LiteCharms.Features.TechShop/Notifications/Entities/NotificationConfiguration.cs new file mode 100644 index 0000000..089e749 --- /dev/null +++ b/LiteCharms.Features.TechShop/Notifications/Entities/NotificationConfiguration.cs @@ -0,0 +1,28 @@ +namespace LiteCharms.Features.TechShop.Notifications.Entities; + +public class NotificationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Notification"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); + builder.Property(f => f.Direction).IsRequired().HasConversion(); + builder.Property(f => f.Platform).IsRequired().HasConversion(); + builder.Property(f => f.Priority).IsRequired().HasConversion(); + builder.Property(f => f.CorrelationIdType).IsRequired().HasConversion(); + builder.Property(f => f.SenderAddress).IsRequired(); + builder.Property(f => f.Subject).IsRequired(); + builder.Property(f => f.Message).IsRequired(); + builder.Property(f => f.RecipientName).IsRequired(); + builder.Property(f => f.RecipientAddress).IsRequired(); + builder.Property(f => f.CorrelationId).IsRequired(); + builder.Property(f => f.IsHtml).HasDefaultValue(false); + builder.Property(f => f.IsInternal).HasDefaultValue(true); + builder.Property(f => f.Processed).HasDefaultValue(false); + builder.Property(f => f.HasError).HasDefaultValue(false); + builder.Property(f => f.Errors).HasColumnType("jsonb").IsRequired(false); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.TechShop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs b/LiteCharms.Features.TechShop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs new file mode 100644 index 0000000..5a2cdd2 --- /dev/null +++ b/LiteCharms.Features.TechShop/Notifications/Events/Handlers/ProcessEmailNotificationsEventHandler.cs @@ -0,0 +1,113 @@ +using LiteCharms.Features.Email; +using LiteCharms.Features.TechShop.Notifications.Models; +using LiteCharms.Features.TechShop.Postgres; + +namespace LiteCharms.Features.TechShop.Notifications.Events.Handlers; + +public class ProcessEmailNotificationsEventHandler(IDbContextFactory contextFactory, ILogger logger, + EmailService emailService) : INotificationHandler +{ + private bool dropBatch = false; + + public async ValueTask Handle(ProcessEmailNotificationsEvent message, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (emailService.Status != EmailStatuses.Connected) + await emailService.ConnectAsync(cancellationToken); + + var notifications = await context.Notifications + .OrderByDescending(o => o.CreatedAt) + .ThenBy(o => o.Priority) + .Where(n => n.Platform == NotificationPlatforms.Email && + n.Direction == NotificationDirection.Outgoing && n.Processed == false) + .Take(message.MaxRecords) + .ToListAsync(cancellationToken); + + foreach (var notification in notifications) + { + if (dropBatch) break; + + var sendResult = await SendEmailAsync(notification,emailService, cancellationToken); + + if(sendResult.IsFailed) + { + var errors = new List(1000); + + errors.AddRange(sendResult.Errors.Select(e => e.Message)); + + if (sendResult.Reasons?.Count > 0) + errors.AddRange(sendResult.Reasons.Select(e => e.Message)); + + notification.HasError = true; + notification.Errors = [.. errors]; + } + + notification.Processed = true; + notification.UpdatedAt = DateTime.UtcNow; + } + + await context.SaveChangesAsync(cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, ex.Message); + } + finally + { + await emailService.DisconnectAsync(cancellationToken); + } + } + + private async Task SendEmailAsync(Notification notification, EmailService service, CancellationToken cancellationToken = default) + { + try + { + using Email.Models.Message message = CreateMessage(notification); + + var sendResult = await service.SendEmailAsync(message, cancellationToken); + + if (sendResult.IsFailed) + { + if (emailService.Status != EmailStatuses.Success && emailService.Status != EmailStatuses.Connected) dropBatch = true; + + return Result.Fail(sendResult.Errors); + } + + return sendResult.IsFailed + ? Result.Fail(sendResult.Errors) + : Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + private static Email.Models.Message CreateMessage(Notification notification) => + new() + { + Sender = new Email.Models.Party + { + Name = notification.SenderName, + Address = notification.SenderAddress + }, + Recipient = new Email.Models.Party + { + Name = notification.RecipientName, + Address = notification.RecipientAddress + }, + Subject = notification.Subject, + Body = new Email.Models.Body + { + Properties = new Email.Models.BodyProperties + { + HasAttachments = false, + IsHtml = notification.IsHtml + }, + Message = notification.Message + } + }; +} diff --git a/LiteCharms.Features.TechShop/Notifications/Events/Handlers/SendShopEmailEnquiryEventHandler.cs b/LiteCharms.Features.TechShop/Notifications/Events/Handlers/SendShopEmailEnquiryEventHandler.cs new file mode 100644 index 0000000..c6bc984 --- /dev/null +++ b/LiteCharms.Features.TechShop/Notifications/Events/Handlers/SendShopEmailEnquiryEventHandler.cs @@ -0,0 +1,25 @@ +using static LiteCharms.Features.Extensions.Email; + +namespace LiteCharms.Features.TechShop.Notifications.Events.Handlers; + +public class SendShopEmailEnquiryEventHandler(NotificationService notificationService) : + INotificationHandler +{ + public async ValueTask Handle(SendShopEmailEnquiryEvent notification, CancellationToken cancellationToken) => + await notificationService.CreateNotificationAsync(new Models.CreateNotification + { + CorrelationId = notification.CorrelationId, + CorrelationIdType = CorrelationIdTypes.None, + Direction = NotificationDirection.Outgoing, + IsHtml = false, + IsInternal = true, + Message = notification.Message, + Platform = NotificationPlatforms.Email, + Priority = notification.Priority, + Subject = notification.Subject!, + Sender = notification.SenderName!, + SenderAddress = notification.SenderAddress!, + Recipient = ShopEmailFromName, + RecipientAddress = ShopEmailFromAddress + }, cancellationToken); +} diff --git a/LiteCharms.Features.TechShop/Notifications/Events/ProcessEmailNotificationsEvent.cs b/LiteCharms.Features.TechShop/Notifications/Events/ProcessEmailNotificationsEvent.cs new file mode 100644 index 0000000..06a7029 --- /dev/null +++ b/LiteCharms.Features.TechShop/Notifications/Events/ProcessEmailNotificationsEvent.cs @@ -0,0 +1,16 @@ +using LiteCharms.Features.Abstractions; + +namespace LiteCharms.Features.TechShop.Notifications.Events; + +public class ProcessEmailNotificationsEvent : EventBase, IEvent +{ + public string Name { get; set; } = nameof(ProcessEmailNotificationsEvent); + + public int MaxRecords { get; set; } + + public ProcessEmailNotificationsEvent() { MaxRecords = 1000; } + + private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords; + + public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords); +} diff --git a/LiteCharms.Features.TechShop/Notifications/Events/SendShopEmailEnquiryEvent.cs b/LiteCharms.Features.TechShop/Notifications/Events/SendShopEmailEnquiryEvent.cs new file mode 100644 index 0000000..246f433 --- /dev/null +++ b/LiteCharms.Features.TechShop/Notifications/Events/SendShopEmailEnquiryEvent.cs @@ -0,0 +1,39 @@ +using LiteCharms.Features.Abstractions; + +namespace LiteCharms.Features.TechShop.Notifications.Events; + +public class SendShopEmailEnquiryEvent : EventBase, IEvent +{ + public string Name { get; set; } = nameof(SendShopEmailEnquiryEvent); + + public string? SenderName { get; set; } + + public string? SenderAddress { get; set; } + + public string? Subject { get; set; } + + public string? Message { get; set; } + + public Priorities Priority { get; set; } + + public SendShopEmailEnquiryEvent() { } + + private SendShopEmailEnquiryEvent(string senderName, string senderAddress, string subject, string message, Priorities priority = Priorities.Medium) + { + SenderName = senderName; + SenderAddress = senderAddress; + Subject = subject; + Message = message; + Priority = priority; + } + + public static SendShopEmailEnquiryEvent Create(string senderName, string senderAddress, string subject, string message, Priorities priority = Priorities.Medium) + { + ArgumentNullException.ThrowIfNullOrWhiteSpace(senderName, nameof(senderName)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(senderAddress, nameof(senderAddress)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(subject, nameof(subject)); + ArgumentNullException.ThrowIfNullOrWhiteSpace(message, nameof(message)); + + return new(senderName, senderAddress, subject, message, priority); + } +} diff --git a/LiteCharms.Features.TechShop/Notifications/Models/Notification.cs b/LiteCharms.Features.TechShop/Notifications/Models/Notification.cs new file mode 100644 index 0000000..71ca0b3 --- /dev/null +++ b/LiteCharms.Features.TechShop/Notifications/Models/Notification.cs @@ -0,0 +1,44 @@ +using LiteCharms.Features.TechShop; + +namespace LiteCharms.Features.TechShop.Notifications.Models; + +public class Notification +{ + public Guid Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public NotificationDirection Direction { get; set; } + + public NotificationPlatforms Platform { get; set; } + + public Priorities Priority { get; set; } + + public CorrelationIdTypes CorrelationIdType { get; set; } + + public string? SenderAddress { get; set; } + + public string? SenderName { get; set; } + + public string? Subject { get; set; } + + public string? Message { get; set; } + + public string? RecipientName { get; set; } + + public string? RecipientAddress { get; set; } + + public string? CorrelationId { get; set; } + + public bool IsHtml { get; set; } + + public bool IsInternal { get; set; } + + public bool Processed { get; set; } + + public bool HasError { get; set; } + + public string[]? Errors { get; set; } +} diff --git a/LiteCharms.Features.TechShop/Notifications/Models/Records.cs b/LiteCharms.Features.TechShop/Notifications/Models/Records.cs new file mode 100644 index 0000000..d72e5e7 --- /dev/null +++ b/LiteCharms.Features.TechShop/Notifications/Models/Records.cs @@ -0,0 +1,41 @@ +namespace LiteCharms.Features.TechShop.Notifications.Models; + +public record CreateNotification +{ + public required NotificationDirection Direction { get; set; } + + public required string Sender { get; set; } + + public required string SenderAddress { get; set; } + + public required string Subject { get; set; } + + public string? Message { get; set; } + + public required NotificationPlatforms Platform { get; set; } + + public required Priorities Priority { get; set; } + + public required string Recipient { get; set; } + + public required string RecipientAddress { get; set; } + + public string? CorrelationId { get; set; } + + public CorrelationIdTypes CorrelationIdType { get; set; } + + public bool IsInternal { get; set; } + + public bool IsHtml { get; set; } +} + +public class UpdateNotification +{ + public required Guid NotificationId { get; set; } + + public required bool Processed { get; set; } + + public bool HasError { get; set; } + + public string[]? Errors { get; set; } +} diff --git a/LiteCharms.Features.TechShop/Notifications/NotificationService.cs b/LiteCharms.Features.TechShop/Notifications/NotificationService.cs new file mode 100644 index 0000000..8826294 --- /dev/null +++ b/LiteCharms.Features.TechShop/Notifications/NotificationService.cs @@ -0,0 +1,113 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Notifications.Models; +using LiteCharms.Features.TechShop.Postgres; + +namespace LiteCharms.Features.TechShop.Notifications; + +public class NotificationService(IDbContextFactory contextFactory) +{ + public async ValueTask> CreateNotificationAsync(CreateNotification request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var newNotification = context.Notifications.Add(new Entities.Notification + { + Direction = request.Direction, + SenderName = request.Sender, + SenderAddress = request.SenderAddress, + RecipientName = request.Recipient, + RecipientAddress = request.RecipientAddress, + Subject = request.Subject, + Message = request.Message, + Platform = request.Platform, + Priority = request.Priority, + CorrelationId = request.CorrelationId, + CorrelationIdType = request.CorrelationIdType, + IsInternal = request.IsInternal, + IsHtml = request.IsHtml, + Processed = false + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newNotification.Entity.Id) + : Result.Fail(new Error("Failed to create notification")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetNotificationAsync(Guid notificationId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var notification = await context.Notifications.FirstOrDefaultAsync(n => n.Id == notificationId, cancellationToken); + + return notification is not null + ? Result.Ok(notification.ToModel()) + : Result.Fail(new Error($"Notification with id {notificationId} not found")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetNotificationsAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var notifications = await context.Notifications.AsNoTracking() + .Where(n => n.CreatedAt >= fromDate && n.CreatedAt <= toDate) + .OrderByDescending(n => n.CreatedAt) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return notifications?.Length > 0 + ? Result.Ok(notifications.Select(n => n.ToModel()).ToArray()) + : Result.Fail(new Error($"No notifications found for the specified date range {range.From} to {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateNotificationAsync(UpdateNotification request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var notification = await context.Notifications.FirstOrDefaultAsync(n => n.Id == request.NotificationId, cancellationToken); + + if (notification is null) + return Result.Fail(new Error($"Notification with id {request.NotificationId} not found.")); + + notification.Processed = request.Processed; + notification.UpdatedAt = DateTime.UtcNow; + notification.HasError = request.HasError; + notification.Errors = request.Errors; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update notification with id {request.NotificationId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.TechShop/Orders/Entities/Order.cs b/LiteCharms.Features.TechShop/Orders/Entities/Order.cs new file mode 100644 index 0000000..3996795 --- /dev/null +++ b/LiteCharms.Features.TechShop/Orders/Entities/Order.cs @@ -0,0 +1,17 @@ +using LiteCharms.Features.TechShop.Customers.Entities; +using LiteCharms.Features.TechShop.Quotes.Entities; +using LiteCharms.Features.TechShop.ShoppingCarts.Entities; + +namespace LiteCharms.Features.TechShop.Orders.Entities; + +[EntityTypeConfiguration] +public class Order : Models.Order +{ + public virtual ICollection? Refunds { get; set; } + + public virtual Customer? Customer { get; set; } + + public virtual Quote? Quote { get; set; } + + public virtual ShoppingCart? ShoppingCart { get; set; } +} diff --git a/LiteCharms.Features.TechShop/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features.TechShop/Orders/Entities/OrderConfiguration.cs new file mode 100644 index 0000000..187c916 --- /dev/null +++ b/LiteCharms.Features.TechShop/Orders/Entities/OrderConfiguration.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Features.TechShop.Orders.Entities; + +public class OrderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Orders"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); + builder.Property(f => f.CustomerId).IsRequired(); + builder.Property(f => f.Status).HasConversion().IsRequired(); + builder.Property(f => f.Requirements).HasColumnType("jsonb").IsRequired(false); + builder.Property(f => f.Notes).HasColumnType("jsonb").IsRequired(false); + builder.Property(f => f.Terms).HasColumnType("jsonb").IsRequired(false); + builder.Property(f => f.InvoiceUrl).IsRequired(false).HasMaxLength(2048); + + builder.HasOne(o => o.Customer) + .WithMany(c => c.Orders) + .HasForeignKey(o => o.CustomerId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Entities/OrderRefund.cs b/LiteCharms.Features.TechShop/Orders/Entities/OrderRefund.cs similarity index 68% rename from LiteCharms.Entities/OrderRefund.cs rename to LiteCharms.Features.TechShop/Orders/Entities/OrderRefund.cs index e5a285d..e832b46 100644 --- a/LiteCharms.Entities/OrderRefund.cs +++ b/LiteCharms.Features.TechShop/Orders/Entities/OrderRefund.cs @@ -1,9 +1,8 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; +namespace LiteCharms.Features.TechShop.Orders.Entities; [EntityTypeConfiguration] public class OrderRefund : Models.OrderRefund { + public virtual Order? Order { get; set; } } diff --git a/LiteCharms.Entities/Configuration/OrderRefundConfiguration.cs b/LiteCharms.Features.TechShop/Orders/Entities/OrderRefundConfiguration.cs similarity index 57% rename from LiteCharms.Entities/Configuration/OrderRefundConfiguration.cs rename to LiteCharms.Features.TechShop/Orders/Entities/OrderRefundConfiguration.cs index 5550805..33a336b 100644 --- a/LiteCharms.Entities/Configuration/OrderRefundConfiguration.cs +++ b/LiteCharms.Features.TechShop/Orders/Entities/OrderRefundConfiguration.cs @@ -1,19 +1,21 @@ -namespace LiteCharms.Entities.Configuration; +namespace LiteCharms.Features.TechShop.Orders.Entities; public class OrderRefundConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(OrderRefund)); + builder.ToTable("OrderRefunds"); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd(); + builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(f => f.OrderId).IsRequired(); builder.Property(f => f.Reason).IsRequired(); builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); - builder.HasOne(f => f.Order) - .WithOne(o => o.Refund) - .HasForeignKey(o => o.OrderId); + builder.HasOne(r => r.Order) + .WithMany(o => o.Refunds) + .HasForeignKey(r => r.OrderId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); } } diff --git a/LiteCharms.Features.TechShop/Orders/Models/Order.cs b/LiteCharms.Features.TechShop/Orders/Models/Order.cs new file mode 100644 index 0000000..b050337 --- /dev/null +++ b/LiteCharms.Features.TechShop/Orders/Models/Order.cs @@ -0,0 +1,22 @@ +namespace LiteCharms.Features.TechShop.Orders.Models; + +public class Order +{ + public Guid Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public Guid CustomerId { get; set; } + + public OrderStatus Status { get; set; } + + public string[]? Requirements { get; set; } + + public string[]? Notes { get; set; } + + public string[]? Terms { get; set; } + + public string? InvoiceUrl { get; set; } +} diff --git a/LiteCharms.Models/OrderRefund.cs b/LiteCharms.Features.TechShop/Orders/Models/OrderRefund.cs similarity index 64% rename from LiteCharms.Models/OrderRefund.cs rename to LiteCharms.Features.TechShop/Orders/Models/OrderRefund.cs index 292e918..b8ac3fb 100644 --- a/LiteCharms.Models/OrderRefund.cs +++ b/LiteCharms.Features.TechShop/Orders/Models/OrderRefund.cs @@ -1,10 +1,10 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.TechShop.Orders.Models; public class OrderRefund { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } public Guid OrderId { get; set; } diff --git a/LiteCharms.Features.TechShop/Orders/Models/Records.cs b/LiteCharms.Features.TechShop/Orders/Models/Records.cs new file mode 100644 index 0000000..1325c97 --- /dev/null +++ b/LiteCharms.Features.TechShop/Orders/Models/Records.cs @@ -0,0 +1,42 @@ +using LiteCharms.Features.TechShop; + +namespace LiteCharms.Features.TechShop.Orders.Models; + +public record CreateOrder +{ + public required Guid CustomerId { get; set; } + + public required Guid ShoppingCartId { get; set; } + + public Guid? QuoteId { get; set; } + + public string[]? Requirements { get; set; } + + public string[]? Notes { get; set; } + + public string[]? Terms { get; set; } +} + +public record UpdateOrder +{ + public required Guid OrderId { get; set; } + + public required OrderStatus Status { get; set; } + + public string? InvoiceUrl { get; set; } + + public string[]? Notes { get; set; } + + public string[]? Requirements { get; set; } +} + +public record RefundCustomer +{ + public required Guid OrderId { get; set; } + + public required Guid CustomerId { get; set; } + + public required string Reason { get; set; } + + public required decimal Amount { get; set; } +} \ No newline at end of file diff --git a/LiteCharms.Features.TechShop/Orders/OrderService.cs b/LiteCharms.Features.TechShop/Orders/OrderService.cs new file mode 100644 index 0000000..9d5299b --- /dev/null +++ b/LiteCharms.Features.TechShop/Orders/OrderService.cs @@ -0,0 +1,260 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Orders.Models; +using LiteCharms.Features.TechShop.Postgres; + +namespace LiteCharms.Features.TechShop.Orders; + +public class OrderService(IDbContextFactory contextFactory) +{ + public async ValueTask> CreateOrderAsync(CreateOrder request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) + return Result.Fail(new Error($"Customer {request.CustomerId} does not exist.")); + + if (!await context.ShoppingCarts.AnyAsync(sc => sc.Id == request.ShoppingCartId, cancellationToken)) + return Result.Fail(new Error($"Shopping cart {request.ShoppingCartId} does not exist.")); + + if (request.QuoteId.HasValue && !await context.Quotes.AnyAsync(q => q.Id == request.QuoteId.Value, cancellationToken)) + return Result.Fail(new Error($"Quote {request.QuoteId.Value} does not exist.")); + + var newOrder = context.Orders.Add(new Entities.Order + { + Status = OrderStatus.Pending, + CustomerId = request.CustomerId, + Requirements = request.Requirements, + Notes = request.Notes, + Terms = request.Terms + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newOrder.Entity.Id) + : Result.Fail(new Error($"Failed to create customer {request.CustomerId} order using shopping cart {request.ShoppingCartId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerOrderRefundsAsync(Guid customerId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with Id {customerId} does not exist.")); + + var refunds = await context.OrderRefunds.AsNoTracking().AsSplitQuery() + .OrderByDescending(o => o.CreatedAt) + .Where(r => r.Order!.CustomerId == customerId).ToArrayAsync(cancellationToken); + + return refunds?.Length > 0 + ? Result.Ok(refunds.Select(r => r.ToModel()).ToArray()) + : Result.Fail(new Error($"No refunds found for customer with Id {customerId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerOrdersAsync(Guid customerId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AsNoTracking().AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with Id {customerId} does not exist.")); + + var orders = await context.Orders.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(o => o.CustomerId == customerId) + .ToArrayAsync(cancellationToken); + + return orders?.Length > 0 + ? Result.Ok(orders.Select(o => o.ToModel()).ToArray()) + : Result.Fail(new Error($"No orders found for customer with Id {customerId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrderRefundAsync(Guid orderId, Guid orderRefundId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var refund = await context.OrderRefunds.AsNoTracking() + .FirstOrDefaultAsync(r => r.OrderId == orderId && r.Id == orderRefundId, cancellationToken); + + return refund is not null + ? Result.Ok(refund.ToModel()) + : Result.Fail(new Error($"Refund {orderRefundId} not found for the given OrderId: {orderId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrderRefundAsync(Guid orderRefundId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var refund = await context.OrderRefunds.AsNoTracking().FirstOrDefaultAsync(r => r.Id == orderRefundId, cancellationToken); + + return refund is not null + ? Result.Ok(refund.ToModel()) + : Result.Fail($"Order refund could not be found with id {orderRefundId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrderRefundsAsync(Guid orderId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var refunds = await context.OrderRefunds.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(r => r.OrderId == orderId) + .ToArrayAsync(cancellationToken); + + return refunds?.Length > 0 + ? Result.Ok(refunds.Select(r => r.ToModel()).ToArray()) + : Result.Fail($"Order refunds could not be found with order id {orderId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetOrdersAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var orders = await context.Orders + .AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return orders?.Length > 0 + ? Result.Ok(orders.Select(o => o.ToModel()).ToArray()) + : Result.Fail(new Error($"No orders found for the specified date range {range.From} - {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> RefundCustomerAsync(RefundCustomer request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) + return Result.Fail(new Error($"Order with Id: {request.OrderId} does not exist")); + + if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) + return Result.Fail(new Error($"Customer with Id: {request.CustomerId} does not exist")); + + if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId && o.CustomerId == request.CustomerId, cancellationToken)) + return Result.Fail(new Error($"Order with Id: {request.OrderId} does not belong to Customer with Id: {request.CustomerId}")); + + var refund = context.OrderRefunds.Add(new Entities.OrderRefund + { + OrderId = request.OrderId, + Reason = request.Reason, + Amount = request.Amount + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(refund.Entity.Id) + : Result.Fail(new Error($"Failed to create refund for OrderId: {request.OrderId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateOrderRefundAsync(Guid orderRefundId, string reason, decimal amount, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var refund = await context.OrderRefunds.FirstOrDefaultAsync(r => r.Id == orderRefundId, cancellationToken); + + if (refund is null) + return Result.Fail($"Order refund not found with id {orderRefundId}"); + + refund.Reason = reason; + refund.Amount = amount; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update order refund {orderRefundId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateOrderStatusAsync(UpdateOrder request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken); + + if (order is null) + return Result.Fail(new Error($"Order {request.OrderId} not found")); + + order.Status = request.Status; + order.UpdatedAt = DateTime.UtcNow; + + if(!string.IsNullOrWhiteSpace(request.InvoiceUrl)) order.InvoiceUrl = request.InvoiceUrl; + + if(request.Requirements?.Length > 0) order.Requirements = request.Requirements; + if(request.Notes?.Length > 0) order.Notes = request.Notes; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update order {request.OrderId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.Designer.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.Designer.cs similarity index 68% rename from LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.Designer.cs rename to LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.Designer.cs index 3707216..f7407e8 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.Designer.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.Designer.cs @@ -1,6 +1,6 @@ // using System; -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.TechShop.Postgres; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -9,11 +9,11 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace LiteCharms.Infrastructure.Database.Migrations +namespace LiteCharms.Features.TechShop.Postgres.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] - [Migration("20260505202859_AddedQuoteShoppingCartalteredOrderCustomer")] - partial class AddedQuoteShoppingCartalteredOrderCustomer + [DbContext(typeof(ShopDbContext))] + [Migration("20260512065421_Init")] + partial class Init { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -50,7 +50,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Discord") .HasColumnType("text"); @@ -86,7 +87,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("text"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.Property("Website") @@ -130,7 +130,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); @@ -148,7 +149,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("bigint"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.Property("WebClickId") @@ -167,51 +167,76 @@ namespace LiteCharms.Infrastructure.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("Author") - .IsRequired() - .HasColumnType("text"); - b.Property("CorrelationId") .IsRequired() .HasColumnType("text"); - b.Property("CorrelationIdType") - .IsRequired() - .HasColumnType("text"); + b.Property("CorrelationIdType") + .HasColumnType("integer"); b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Direction") .HasColumnType("integer"); + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("IsInternal") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(true); - b.Property("Platform") + b.Property("Message") .IsRequired() .HasColumnType("text"); - b.Property("PlatformAddress") - .IsRequired() - .HasColumnType("text"); + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); b.Property("Processed") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(false); - b.Property("Title") + b.Property("Recipient") .IsRequired() .HasColumnType("text"); + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("Sender") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + b.HasKey("Id"); b.ToTable("Notification", (string)null); @@ -225,40 +250,35 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + b.PrimitiveCollection("Notes") .HasColumnType("jsonb"); - b.Property("QuoteId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("ShoppingCartId") - .HasColumnType("uuid"); + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); b.Property("Status") .HasColumnType("integer"); + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); - b.HasIndex("QuoteId") - .IsUnique(); - - b.HasIndex("ShoppingCartId") - .IsUnique(); - b.ToTable("Order", (string)null); }); @@ -274,7 +294,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("OrderId") .HasColumnType("uuid"); @@ -285,12 +306,80 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.HasKey("Id"); - b.HasIndex("OrderId") - .IsUnique(); + b.HasIndex("OrderId"); b.ToTable("OrderRefund", (string)null); }); + modelBuilder.Entity("LiteCharms.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("PackageItem", (string)null); + }); + modelBuilder.Entity("LiteCharms.Entities.Product", b => { b.Property("Id") @@ -304,12 +393,25 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("Description") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); b.Property("Name") .IsRequired() .HasColumnType("text"); + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("Thumbnails") + .HasColumnType("jsonb"); + b.HasKey("Id"); b.ToTable("Product", (string)null); @@ -326,7 +428,8 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Discount") .HasPrecision(18, 2) @@ -340,7 +443,6 @@ namespace LiteCharms.Infrastructure.Database.Migrations .HasColumnType("uuid"); b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -358,35 +460,40 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); - b.Property("CustomerId1") - .HasColumnType("uuid"); - b.Property("ExpiredAt") .HasColumnType("timestamp with time zone"); + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + b.Property("Reason") .HasColumnType("text"); - b.Property("ShoppingCartId") + b.Property("ShoppingCartId") .HasColumnType("uuid"); b.Property("Status") .HasColumnType("integer"); - b.Property("UpdatedAt") - .ValueGeneratedOnAddOrUpdate() + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); - b.HasIndex("CustomerId1"); + b.HasIndex("OrderId") + .IsUnique(); b.HasIndex("ShoppingCartId") .IsUnique(); @@ -402,25 +509,25 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); - b.Property("CustomerId") + b.Property("CustomerId") .HasColumnType("uuid"); b.Property("OrderId") .HasColumnType("uuid"); - b.Property("QuoteId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnAddOrUpdate() + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); + b.HasIndex("OrderId") + .IsUnique(); + b.ToTable("ShoppingCart", (string)null); }); @@ -454,12 +561,37 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.ToTable("ShoppingCartItems"); }); + modelBuilder.Entity("LiteCharms.Entities.ShoppingCartPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartPackage", (string)null); + }); + modelBuilder.Entity("LiteCharms.Entities.Lead", b => { b.HasOne("LiteCharms.Entities.Customer", "Customer") .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); + .HasForeignKey("CustomerId"); b.Navigation("Customer"); }); @@ -472,35 +604,39 @@ namespace LiteCharms.Infrastructure.Database.Migrations .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("LiteCharms.Entities.Quote", "Quote") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "QuoteId") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - b.Navigation("Customer"); - - b.Navigation("Quote"); - - b.Navigation("ShoppingCart"); }); modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => { b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") + .WithMany("Refunds") + .HasForeignKey("OrderId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Order"); }); + modelBuilder.Entity("LiteCharms.Entities.PackageItem", b => + { + b.HasOne("LiteCharms.Entities.Package", "Package") + .WithMany("PackageItems") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ProductPrice"); + }); + modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => { b.HasOne("LiteCharms.Entities.Product", "Product") @@ -515,23 +651,23 @@ namespace LiteCharms.Infrastructure.Database.Migrations modelBuilder.Entity("LiteCharms.Entities.Quote", b => { b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany() + .WithMany("Quotes") .HasForeignKey("CustomerId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Entities.Customer", null) - .WithMany("Quotes") - .HasForeignKey("CustomerId1"); + b.HasOne("LiteCharms.Entities.Order", "Order") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Entities.Quote", "OrderId"); b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") .WithOne("Quote") - .HasForeignKey("LiteCharms.Entities.Quote", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); + .HasForeignKey("LiteCharms.Entities.Quote", "ShoppingCartId"); b.Navigation("Customer"); + b.Navigation("Order"); + b.Navigation("ShoppingCart"); }); @@ -540,9 +676,17 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.HasOne("LiteCharms.Entities.Customer", "Customer") .WithMany("ShoppingCarts") .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.Order", "Order") + .WithOne("ShoppingCart") + .HasForeignKey("LiteCharms.Entities.ShoppingCart", "OrderId") + .OnDelete(DeleteBehavior.SetNull); b.Navigation("Customer"); + + b.Navigation("Order"); }); modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => @@ -564,6 +708,25 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Navigation("ShoppingCart"); }); + modelBuilder.Entity("LiteCharms.Entities.ShoppingCartPackage", b => + { + b.HasOne("LiteCharms.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartPackages") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ShoppingCart"); + }); + modelBuilder.Entity("LiteCharms.Entities.Customer", b => { b.Navigation("Leads"); @@ -577,7 +740,16 @@ namespace LiteCharms.Infrastructure.Database.Migrations modelBuilder.Entity("LiteCharms.Entities.Order", b => { - b.Navigation("Refund"); + b.Navigation("Quote"); + + b.Navigation("Refunds"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Entities.Package", b => + { + b.Navigation("PackageItems"); }); modelBuilder.Entity("LiteCharms.Entities.Product", b => @@ -585,18 +757,13 @@ namespace LiteCharms.Infrastructure.Database.Migrations b.Navigation("ProductPrices"); }); - modelBuilder.Entity("LiteCharms.Entities.Quote", b => - { - b.Navigation("Order"); - }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => { - b.Navigation("Order"); - b.Navigation("Quote"); b.Navigation("ShoppingCartItems"); + + b.Navigation("ShoppingCartPackages"); }); #pragma warning restore 612, 618 } diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.cs new file mode 100644 index 0000000..201f8d5 --- /dev/null +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260512065421_Init.cs @@ -0,0 +1,474 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.TechShop.Postgres.Migrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Customer", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Company = table.Column(type: "text", nullable: true), + Name = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + Tax = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: false), + Discord = table.Column(type: "text", nullable: true), + Slack = table.Column(type: "text", nullable: true), + LinkedIn = table.Column(type: "text", nullable: true), + Whatsapp = table.Column(type: "text", nullable: true), + Website = table.Column(type: "text", nullable: true), + Phone = table.Column(type: "text", nullable: true), + Address = table.Column(type: "text", nullable: true), + City = table.Column(type: "text", nullable: true), + Region = table.Column(type: "text", nullable: true), + Country = table.Column(type: "text", nullable: true), + PostalCode = table.Column(type: "text", nullable: true), + Active = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Customer", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Notification", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Direction = table.Column(type: "integer", nullable: false), + Platform = table.Column(type: "integer", nullable: false), + Priority = table.Column(type: "integer", nullable: false), + CorrelationIdType = table.Column(type: "integer", nullable: false), + Sender = table.Column(type: "text", nullable: false), + SenderName = table.Column(type: "text", nullable: true), + Subject = table.Column(type: "text", nullable: false), + Message = table.Column(type: "text", nullable: false), + Recipient = table.Column(type: "text", nullable: false), + RecipientAddress = table.Column(type: "text", nullable: false), + CorrelationId = table.Column(type: "text", nullable: false), + IsHtml = table.Column(type: "boolean", nullable: false, defaultValue: false), + IsInternal = table.Column(type: "boolean", nullable: false, defaultValue: true), + Processed = table.Column(type: "boolean", nullable: false, defaultValue: false), + HasError = table.Column(type: "boolean", nullable: false, defaultValue: false), + Errors = table.Column(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Notification", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Package", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Name = table.Column(type: "text", nullable: false), + Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Description = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Package", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Product", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Description = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Thumbnails = table.Column(type: "jsonb", nullable: true), + Active = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Product", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Lead", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: true), + Source = table.Column(type: "text", nullable: true), + ClickId = table.Column(type: "text", nullable: true), + WebClickId = table.Column(type: "text", nullable: true), + AppClickId = table.Column(type: "text", nullable: true), + CampaignId = table.Column(type: "bigint", nullable: true), + AdGroupId = table.Column(type: "bigint", nullable: true), + AdName = table.Column(type: "bigint", nullable: true), + TargetId = table.Column(type: "bigint", nullable: true), + FeedItemId = table.Column(type: "bigint", nullable: true), + ClickLocation = table.Column(type: "text", nullable: true), + AttributionHash = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Lead", x => x.Id); + table.ForeignKey( + name: "FK_Lead_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Order", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Requirements = table.Column(type: "jsonb", nullable: true), + Notes = table.Column(type: "jsonb", nullable: true), + Terms = table.Column(type: "jsonb", nullable: true), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Order", x => x.Id); + table.ForeignKey( + name: "FK_Order_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ProductPrice", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + ProductId = table.Column(type: "uuid", nullable: false), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Discount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductPrice", x => x.Id); + table.ForeignKey( + name: "FK_ProductPrice_Product_ProductId", + column: x => x.ProductId, + principalTable: "Product", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "OrderRefund", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + OrderId = table.Column(type: "uuid", nullable: false), + Reason = table.Column(type: "text", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderRefund", x => x.Id); + table.ForeignKey( + name: "FK_OrderRefund_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCart", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCart", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCart_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingCart_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "PackageItem", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PackageId = table.Column(type: "uuid", nullable: false), + ProductPriceId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PackageItem", x => x.Id); + table.ForeignKey( + name: "FK_PackageItem_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PackageItem_ProductPrice_ProductPriceId", + column: x => x.ProductPriceId, + principalTable: "ProductPrice", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Quote", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true), + ShoppingCartId = table.Column(type: "uuid", nullable: true), + Status = table.Column(type: "integer", nullable: false), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Reason = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Quote", x => x.Id); + table.ForeignKey( + name: "FK_Quote_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Quote_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Quote_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCartItems", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + ProductPriceId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Quantity = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCartItems", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCartItems_ProductPrice_ProductPriceId", + column: x => x.ProductPriceId, + principalTable: "ProductPrice", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingCartItems_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCartPackage", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + PackageId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCartPackage", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCartPackage_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ShoppingCartPackage_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Lead_CustomerId", + table: "Lead", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Order_CustomerId", + table: "Order", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderRefund_OrderId", + table: "OrderRefund", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_PackageItem_PackageId", + table: "PackageItem", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_PackageItem_ProductPriceId", + table: "PackageItem", + column: "ProductPriceId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductPrice_ProductId", + table: "ProductPrice", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Quote_CustomerId", + table: "Quote", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Quote_OrderId", + table: "Quote", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Quote_ShoppingCartId", + table: "Quote", + column: "ShoppingCartId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCart_CustomerId", + table: "ShoppingCart", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCart_OrderId", + table: "ShoppingCart", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ProductPriceId", + table: "ShoppingCartItems", + column: "ProductPriceId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ShoppingCartId", + table: "ShoppingCartItems", + column: "ShoppingCartId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_PackageId", + table: "ShoppingCartPackage", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_ShoppingCartId", + table: "ShoppingCartPackage", + column: "ShoppingCartId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Lead"); + + migrationBuilder.DropTable( + name: "Notification"); + + migrationBuilder.DropTable( + name: "OrderRefund"); + + migrationBuilder.DropTable( + name: "PackageItem"); + + migrationBuilder.DropTable( + name: "Quote"); + + migrationBuilder.DropTable( + name: "ShoppingCartItems"); + + migrationBuilder.DropTable( + name: "ShoppingCartPackage"); + + migrationBuilder.DropTable( + name: "ProductPrice"); + + migrationBuilder.DropTable( + name: "Package"); + + migrationBuilder.DropTable( + name: "ShoppingCart"); + + migrationBuilder.DropTable( + name: "Product"); + + migrationBuilder.DropTable( + name: "Order"); + + migrationBuilder.DropTable( + name: "Customer"); + } + } +} diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs new file mode 100644 index 0000000..275d93a --- /dev/null +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.Designer.cs @@ -0,0 +1,871 @@ +// +using System; +using LiteCharms.Features.TechShop.Postgres; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.TechShop.Postgres.Migrations +{ + [DbContext(typeof(ShopDbContext))] + [Migration("20260514004002_UsedStringTableNames")] + partial class UsedStringTableNames + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("PackageItem", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discord") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LinkedIn") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Region") + .HasColumnType("text"); + + b.Property("Slack") + .HasColumnType("text"); + + b.Property("Tax") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.Property("Whatsapp") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discord") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("LinkedIn") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Region") + .HasColumnType("text"); + + b.Property("Slack") + .HasColumnType("text"); + + b.Property("Tax") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.Property("Whatsapp") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdGroupId") + .HasColumnType("bigint"); + + b.Property("AdName") + .HasColumnType("bigint"); + + b.Property("AppClickId") + .HasColumnType("text"); + + b.Property("AttributionHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("CampaignId") + .HasColumnType("bigint"); + + b.Property("ClickId") + .HasColumnType("text"); + + b.Property("ClickLocation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("CustomerId1") + .HasColumnType("uuid"); + + b.Property("FeedItemId") + .HasColumnType("bigint"); + + b.Property("Source") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TargetId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebClickId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("CustomerId1"); + + b.ToTable("Leads", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationIdType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsInternal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Processed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); + + b.Property("Status") + .HasColumnType("integer"); + + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderRefunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("Thumbnails") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductPrices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShoppingCartId") + .IsUnique(); + + b.ToTable("Quotes", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("ShoppingCarts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId1") + .HasColumnType("uuid"); + + b.Property("Quantity") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId1") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductPriceId"); + + b.HasIndex("ProductPriceId1"); + + b.HasIndex("ShoppingCartId"); + + b.HasIndex("ShoppingCartId1"); + + b.ToTable("ShoppingCartItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartPackages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany("PackageItems") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Models.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", null) + .WithMany("Leads") + .HasForeignKey("CustomerId1"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product") + .WithMany("ProductPrices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Quotes") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId"); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId"); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("ShoppingCarts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("ShoppingCart") + .HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Customer"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", null) + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId1"); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", null) + .WithMany("ShoppingCartItems") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany() + .HasForeignKey("ShoppingCartId1"); + + b.Navigation("ProductPrice"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartPackages") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Navigation("PackageItems"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Navigation("Leads"); + + b.Navigation("Orders"); + + b.Navigation("Quotes"); + + b.Navigation("ShoppingCarts"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Navigation("Quote"); + + b.Navigation("Refunds"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Navigation("ProductPrices"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Navigation("Quote"); + + b.Navigation("ShoppingCartItems"); + + b.Navigation("ShoppingCartPackages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs new file mode 100644 index 0000000..22dc364 --- /dev/null +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260514004002_UsedStringTableNames.cs @@ -0,0 +1,936 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.TechShop.Postgres.Migrations +{ + /// + public partial class UsedStringTableNames : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_PackageItem_ProductPrice_ProductPriceId", + table: "PackageItem"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ProductPrice_ProductPriceId", + table: "ShoppingCartItems"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ShoppingCart_ShoppingCartId", + table: "ShoppingCartItems"); + + migrationBuilder.DropTable( + name: "Lead"); + + migrationBuilder.DropTable( + name: "OrderRefund"); + + migrationBuilder.DropTable( + name: "ProductPrice"); + + migrationBuilder.DropTable( + name: "Quote"); + + migrationBuilder.DropTable( + name: "ShoppingCartPackage"); + + migrationBuilder.DropTable( + name: "Product"); + + migrationBuilder.DropTable( + name: "ShoppingCart"); + + migrationBuilder.DropTable( + name: "Order"); + + migrationBuilder.RenameColumn( + name: "Sender", + table: "Notification", + newName: "SenderAddress"); + + migrationBuilder.RenameColumn( + name: "Recipient", + table: "Notification", + newName: "RecipientName"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ShoppingCartItems", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "ShoppingCartItems", + type: "integer", + nullable: false, + defaultValue: 1, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ShoppingCartItems", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone"); + + migrationBuilder.AddColumn( + name: "ProductPriceId1", + table: "ShoppingCartItems", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "ShoppingCartId1", + table: "ShoppingCartItems", + type: "uuid", + nullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customer", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "Customer", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Email", + table: "Customer", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Customer", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "Active", + table: "Customer", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: true); + + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Company = table.Column(type: "text", nullable: true), + Name = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + Tax = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: false), + Discord = table.Column(type: "text", nullable: true), + Slack = table.Column(type: "text", nullable: true), + LinkedIn = table.Column(type: "text", nullable: true), + Whatsapp = table.Column(type: "text", nullable: true), + Website = table.Column(type: "text", nullable: true), + Phone = table.Column(type: "text", nullable: true), + Address = table.Column(type: "text", nullable: true), + City = table.Column(type: "text", nullable: true), + Region = table.Column(type: "text", nullable: true), + Country = table.Column(type: "text", nullable: true), + PostalCode = table.Column(type: "text", nullable: true), + Active = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Description = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Thumbnails = table.Column(type: "jsonb", nullable: true), + Active = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Leads", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId1 = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: true), + Source = table.Column(type: "text", nullable: true), + ClickId = table.Column(type: "text", nullable: true), + WebClickId = table.Column(type: "text", nullable: true), + AppClickId = table.Column(type: "text", nullable: true), + CampaignId = table.Column(type: "bigint", nullable: true), + AdGroupId = table.Column(type: "bigint", nullable: true), + AdName = table.Column(type: "bigint", nullable: true), + TargetId = table.Column(type: "bigint", nullable: true), + FeedItemId = table.Column(type: "bigint", nullable: true), + ClickLocation = table.Column(type: "text", nullable: true), + AttributionHash = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Leads", x => x.Id); + table.ForeignKey( + name: "FK_Leads_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Leads_Customers_CustomerId1", + column: x => x.CustomerId1, + principalTable: "Customers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Requirements = table.Column(type: "jsonb", nullable: true), + Notes = table.Column(type: "jsonb", nullable: true), + Terms = table.Column(type: "jsonb", nullable: true), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + table.ForeignKey( + name: "FK_Orders_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ProductPrices", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + ProductId = table.Column(type: "uuid", nullable: false), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Discount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Active = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductPrices", x => x.Id); + table.ForeignKey( + name: "FK_ProductPrices_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "OrderRefunds", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + OrderId = table.Column(type: "uuid", nullable: false), + Reason = table.Column(type: "text", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderRefunds", x => x.Id); + table.ForeignKey( + name: "FK_OrderRefunds_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCarts", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCarts", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCarts_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingCarts_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "Quotes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true), + CustomerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true), + ShoppingCartId = table.Column(type: "uuid", nullable: true), + Status = table.Column(type: "integer", nullable: false), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Reason = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Quotes", x => x.Id); + table.ForeignKey( + name: "FK_Quotes_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Quotes_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Quotes_ShoppingCarts_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCarts", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCartPackages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + PackageId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCartPackages", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCartPackages_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ShoppingCartPackages_ShoppingCarts_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCarts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ProductPriceId1", + table: "ShoppingCartItems", + column: "ProductPriceId1"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ShoppingCartId1", + table: "ShoppingCartItems", + column: "ShoppingCartId1"); + + migrationBuilder.CreateIndex( + name: "IX_Leads_CustomerId", + table: "Leads", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Leads_CustomerId1", + table: "Leads", + column: "CustomerId1"); + + migrationBuilder.CreateIndex( + name: "IX_OrderRefunds_OrderId", + table: "OrderRefunds", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_CustomerId", + table: "Orders", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductPrices_ProductId", + table: "ProductPrices", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Quotes_CustomerId", + table: "Quotes", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Quotes_OrderId", + table: "Quotes", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Quotes_ShoppingCartId", + table: "Quotes", + column: "ShoppingCartId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackages_PackageId", + table: "ShoppingCartPackages", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackages_ShoppingCartId", + table: "ShoppingCartPackages", + column: "ShoppingCartId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCarts_CustomerId", + table: "ShoppingCarts", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCarts_OrderId", + table: "ShoppingCarts", + column: "OrderId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_PackageItem_ProductPrices_ProductPriceId", + table: "PackageItem", + column: "ProductPriceId", + principalTable: "ProductPrices", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId", + table: "ShoppingCartItems", + column: "ProductPriceId", + principalTable: "ProductPrices", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1", + table: "ShoppingCartItems", + column: "ProductPriceId1", + principalTable: "ProductPrices", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId", + table: "ShoppingCartItems", + column: "ShoppingCartId", + principalTable: "ShoppingCarts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1", + table: "ShoppingCartItems", + column: "ShoppingCartId1", + principalTable: "ShoppingCarts", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_PackageItem_ProductPrices_ProductPriceId", + table: "PackageItem"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId", + table: "ShoppingCartItems"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId", + table: "ShoppingCartItems"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropTable( + name: "Leads"); + + migrationBuilder.DropTable( + name: "OrderRefunds"); + + migrationBuilder.DropTable( + name: "ProductPrices"); + + migrationBuilder.DropTable( + name: "Quotes"); + + migrationBuilder.DropTable( + name: "ShoppingCartPackages"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "ShoppingCarts"); + + migrationBuilder.DropTable( + name: "Orders"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropIndex( + name: "IX_ShoppingCartItems_ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropIndex( + name: "IX_ShoppingCartItems_ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropColumn( + name: "ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropColumn( + name: "ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.RenameColumn( + name: "SenderAddress", + table: "Notification", + newName: "Sender"); + + migrationBuilder.RenameColumn( + name: "RecipientName", + table: "Notification", + newName: "Recipient"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ShoppingCartItems", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Quantity", + table: "ShoppingCartItems", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldDefaultValue: 1); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ShoppingCartItems", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customer", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "Customer", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Email", + table: "Customer", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Customer", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "Active", + table: "Customer", + type: "boolean", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.CreateTable( + name: "Lead", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: true), + AdGroupId = table.Column(type: "bigint", nullable: true), + AdName = table.Column(type: "bigint", nullable: true), + AppClickId = table.Column(type: "text", nullable: true), + AttributionHash = table.Column(type: "text", nullable: false), + CampaignId = table.Column(type: "bigint", nullable: true), + ClickId = table.Column(type: "text", nullable: true), + ClickLocation = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + FeedItemId = table.Column(type: "bigint", nullable: true), + Source = table.Column(type: "text", nullable: true), + Status = table.Column(type: "integer", nullable: false), + TargetId = table.Column(type: "bigint", nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + WebClickId = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Lead", x => x.Id); + table.ForeignKey( + name: "FK_Lead_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Order", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Notes = table.Column(type: "jsonb", nullable: true), + Requirements = table.Column(type: "jsonb", nullable: true), + Status = table.Column(type: "integer", nullable: false), + Terms = table.Column(type: "jsonb", nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Order", x => x.Id); + table.ForeignKey( + name: "FK_Order_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Product", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Active = table.Column(type: "boolean", nullable: false, defaultValue: true), + Description = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Name = table.Column(type: "text", nullable: false), + Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Thumbnails = table.Column(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Product", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OrderRefund", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Reason = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderRefund", x => x.Id); + table.ForeignKey( + name: "FK_OrderRefund_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCart", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCart", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCart_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingCart_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ProductPrice", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProductId = table.Column(type: "uuid", nullable: false), + Active = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Discount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductPrice", x => x.Id); + table.ForeignKey( + name: "FK_ProductPrice_Product_ProductId", + column: x => x.ProductId, + principalTable: "Product", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Quote", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: true), + ShoppingCartId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true), + InvoiceUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + Reason = table.Column(type: "text", nullable: true), + Status = table.Column(type: "integer", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Quote", x => x.Id); + table.ForeignKey( + name: "FK_Quote_Customer_CustomerId", + column: x => x.CustomerId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Quote_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Quote_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ShoppingCartPackage", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PackageId = table.Column(type: "uuid", nullable: false), + ShoppingCartId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingCartPackage", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingCartPackage_Package_PackageId", + column: x => x.PackageId, + principalTable: "Package", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ShoppingCartPackage_ShoppingCart_ShoppingCartId", + column: x => x.ShoppingCartId, + principalTable: "ShoppingCart", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Lead_CustomerId", + table: "Lead", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Order_CustomerId", + table: "Order", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderRefund_OrderId", + table: "OrderRefund", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductPrice_ProductId", + table: "ProductPrice", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Quote_CustomerId", + table: "Quote", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Quote_OrderId", + table: "Quote", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Quote_ShoppingCartId", + table: "Quote", + column: "ShoppingCartId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCart_CustomerId", + table: "ShoppingCart", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCart_OrderId", + table: "ShoppingCart", + column: "OrderId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_PackageId", + table: "ShoppingCartPackage", + column: "PackageId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartPackage_ShoppingCartId", + table: "ShoppingCartPackage", + column: "ShoppingCartId"); + + migrationBuilder.AddForeignKey( + name: "FK_PackageItem_ProductPrice_ProductPriceId", + table: "PackageItem", + column: "ProductPriceId", + principalTable: "ProductPrice", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ProductPrice_ProductPriceId", + table: "ShoppingCartItems", + column: "ProductPriceId", + principalTable: "ProductPrice", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ShoppingCart_ShoppingCartId", + table: "ShoppingCartItems", + column: "ShoppingCartId", + principalTable: "ShoppingCart", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs similarity index 52% rename from LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs rename to LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs index a057ff3..74a6cba 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.Designer.cs @@ -1,28 +1,100 @@ // using System; -using LiteCharms.Infrastructure.Database; +using LiteCharms.Features.TechShop.Postgres; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace LiteCharms.Infrastructure.Migrations +namespace LiteCharms.Features.TechShop.Postgres.Migrations { - [DbContext(typeof(LeadGeneratorDbContext))] - partial class LeadGeneratorDbContextModelSnapshot : ModelSnapshot + [DbContext(typeof(ShopDbContext))] + [Migration("20260515055221_FixedLeadCustomerRelationship")] + partial class FixedLeadCustomerRelationship { - protected override void BuildModel(ModelBuilder modelBuilder) + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("ProductVersion", "10.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("LiteCharms.Entities.Customer", b => + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("PackageItem", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -45,9 +117,10 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("Country") .HasColumnType("text"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Discord") .HasColumnType("text"); @@ -82,8 +155,7 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("Tax") .HasColumnType("text"); - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.Property("Website") @@ -94,10 +166,10 @@ namespace LiteCharms.Infrastructure.Migrations b.HasKey("Id"); - b.ToTable("Customer", (string)null); + b.ToTable("Customers", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Lead", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -125,9 +197,10 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("ClickLocation") .HasColumnType("text"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); @@ -144,8 +217,7 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("TargetId") .HasColumnType("bigint"); - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.Property("WebClickId") @@ -155,111 +227,131 @@ namespace LiteCharms.Infrastructure.Migrations b.HasIndex("CustomerId"); - b.ToTable("Lead", (string)null); + b.ToTable("Leads", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Notification", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("Author") - .IsRequired() - .HasColumnType("text"); - b.Property("CorrelationId") .IsRequired() .HasColumnType("text"); - b.Property("CorrelationIdType") - .IsRequired() - .HasColumnType("text"); + b.Property("CorrelationIdType") + .HasColumnType("integer"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Direction") .HasColumnType("integer"); + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + b.Property("IsInternal") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(true); - b.Property("Platform") + b.Property("Message") .IsRequired() .HasColumnType("text"); - b.Property("PlatformAddress") - .IsRequired() - .HasColumnType("text"); + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); b.Property("Processed") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(false); - b.Property("Title") + b.Property("RecipientAddress") .IsRequired() .HasColumnType("text"); + b.Property("RecipientName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + b.HasKey("Id"); b.ToTable("Notification", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Order", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + b.PrimitiveCollection("Notes") .HasColumnType("jsonb"); - b.Property("QuoteId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("ShoppingCartId") - .HasColumnType("uuid"); + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); b.Property("Status") .HasColumnType("integer"); - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); - b.HasIndex("QuoteId") - .IsUnique(); - - b.HasIndex("ShoppingCartId") - .IsUnique(); - - b.ToTable("Order", (string)null); + b.ToTable("Orders", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -269,9 +361,10 @@ namespace LiteCharms.Infrastructure.Migrations .HasPrecision(18, 2) .HasColumnType("numeric(18,2)"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("OrderId") .HasColumnType("uuid"); @@ -282,13 +375,12 @@ namespace LiteCharms.Infrastructure.Migrations b.HasKey("Id"); - b.HasIndex("OrderId") - .IsUnique(); + b.HasIndex("OrderId"); - b.ToTable("OrderRefund", (string)null); + b.ToTable("OrderRefunds", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Product", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -301,18 +393,31 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("Description") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); b.Property("Name") .IsRequired() .HasColumnType("text"); + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("Thumbnails") + .HasColumnType("jsonb"); + b.HasKey("Id"); - b.ToTable("Product", (string)null); + b.ToTable("Products", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -321,9 +426,10 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("Active") .HasColumnType("boolean"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Discount") .HasPrecision(18, 2) @@ -336,110 +442,118 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("ProductId") .HasColumnType("uuid"); - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("ProductId"); - b.ToTable("ProductPrice", (string)null); + b.ToTable("ProductPrices", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Quote", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CustomerId") .HasColumnType("uuid"); - b.Property("CustomerId1") - .HasColumnType("uuid"); - - b.Property("ExpiredAt") + b.Property("ExpiredAt") .HasColumnType("timestamp with time zone"); + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + b.Property("Reason") .HasColumnType("text"); - b.Property("ShoppingCartId") + b.Property("ShoppingCartId") .HasColumnType("uuid"); b.Property("Status") .HasColumnType("integer"); - b.Property("UpdatedAt") - .ValueGeneratedOnAddOrUpdate() + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); - b.HasIndex("CustomerId1"); + b.HasIndex("OrderId") + .IsUnique(); b.HasIndex("ShoppingCartId") .IsUnique(); - b.ToTable("Quote", (string)null); + b.ToTable("Quotes", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreatedAt") + b.Property("CreatedAt") .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); - b.Property("CustomerId") + b.Property("CustomerId") .HasColumnType("uuid"); b.Property("OrderId") .HasColumnType("uuid"); - b.Property("QuoteId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnAddOrUpdate() + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); b.HasIndex("CustomerId"); - b.ToTable("ShoppingCart", (string)null); + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("ShoppingCarts", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("ProductPriceId") .HasColumnType("uuid"); b.Property("Quantity") - .HasColumnType("integer"); + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); b.Property("ShoppingCartId") .HasColumnType("uuid"); - b.Property("UpdatedAt") + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -448,59 +562,89 @@ namespace LiteCharms.Infrastructure.Migrations b.HasIndex("ShoppingCartId"); - b.ToTable("ShoppingCartItems"); + b.ToTable("ShoppingCartItems", (string)null); }); - modelBuilder.Entity("LiteCharms.Entities.Lead", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => { - b.HasOne("LiteCharms.Entities.Customer", "Customer") + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartPackages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany("PackageItems") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") .WithMany("Leads") .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); + .OnDelete(DeleteBehavior.Restrict); b.Navigation("Customer"); }); - modelBuilder.Entity("LiteCharms.Entities.Order", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => { - b.HasOne("LiteCharms.Entities.Customer", "Customer") + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") .WithMany("Orders") .HasForeignKey("CustomerId") .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("LiteCharms.Entities.Quote", "Quote") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "QuoteId") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") - .WithOne("Order") - .HasForeignKey("LiteCharms.Entities.Order", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - b.Navigation("Customer"); - - b.Navigation("Quote"); - - b.Navigation("ShoppingCart"); }); - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => { - b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Order"); }); - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => { - b.HasOne("LiteCharms.Entities.Product", "Product") + b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product") .WithMany("ProductPrices") .HasForeignKey("ProductId") .OnDelete(DeleteBehavior.Restrict) @@ -509,48 +653,56 @@ namespace LiteCharms.Infrastructure.Migrations b.Navigation("Product"); }); - modelBuilder.Entity("LiteCharms.Entities.Quote", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany() + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Quotes") .HasForeignKey("CustomerId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Entities.Customer", null) - .WithMany("Quotes") - .HasForeignKey("CustomerId1"); - - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") .WithOne("Quote") - .HasForeignKey("LiteCharms.Entities.Quote", "ShoppingCartId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId"); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId"); b.Navigation("Customer"); + b.Navigation("Order"); + b.Navigation("ShoppingCart"); }); - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => { - b.HasOne("LiteCharms.Entities.Customer", "Customer") + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") .WithMany("ShoppingCarts") .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b => - { - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart") + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("ShoppingCart") + .HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Customer"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") .WithMany("ShoppingCartItems") .HasForeignKey("ShoppingCartId") .OnDelete(DeleteBehavior.Cascade) @@ -561,7 +713,31 @@ namespace LiteCharms.Infrastructure.Migrations b.Navigation("ShoppingCart"); }); - modelBuilder.Entity("LiteCharms.Entities.Customer", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartPackages") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Navigation("PackageItems"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => { b.Navigation("Leads"); @@ -572,28 +748,27 @@ namespace LiteCharms.Infrastructure.Migrations b.Navigation("ShoppingCarts"); }); - modelBuilder.Entity("LiteCharms.Entities.Order", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => { - b.Navigation("Refund"); + b.Navigation("Quote"); + + b.Navigation("Refunds"); + + b.Navigation("ShoppingCart"); }); - modelBuilder.Entity("LiteCharms.Entities.Product", b => + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => { b.Navigation("ProductPrices"); }); - modelBuilder.Entity("LiteCharms.Entities.Quote", b => + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => { - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b => - { - b.Navigation("Order"); - b.Navigation("Quote"); b.Navigation("ShoppingCartItems"); + + b.Navigation("ShoppingCartPackages"); }); #pragma warning restore 612, 618 } diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs new file mode 100644 index 0000000..a15cd6f --- /dev/null +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260515055221_FixedLeadCustomerRelationship.cs @@ -0,0 +1,166 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.TechShop.Postgres.Migrations +{ + /// + public partial class FixedLeadCustomerRelationship : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Leads_Customer_CustomerId", + table: "Leads"); + + migrationBuilder.DropForeignKey( + name: "FK_Leads_Customers_CustomerId1", + table: "Leads"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropTable( + name: "Customer"); + + migrationBuilder.DropIndex( + name: "IX_ShoppingCartItems_ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropIndex( + name: "IX_ShoppingCartItems_ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropIndex( + name: "IX_Leads_CustomerId1", + table: "Leads"); + + migrationBuilder.DropColumn( + name: "ProductPriceId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropColumn( + name: "ShoppingCartId1", + table: "ShoppingCartItems"); + + migrationBuilder.DropColumn( + name: "CustomerId1", + table: "Leads"); + + migrationBuilder.AddForeignKey( + name: "FK_Leads_Customers_CustomerId", + table: "Leads", + column: "CustomerId", + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Leads_Customers_CustomerId", + table: "Leads"); + + migrationBuilder.AddColumn( + name: "ProductPriceId1", + table: "ShoppingCartItems", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "ShoppingCartId1", + table: "ShoppingCartItems", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "CustomerId1", + table: "Leads", + type: "uuid", + nullable: true); + + migrationBuilder.CreateTable( + name: "Customer", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Active = table.Column(type: "boolean", nullable: false), + Address = table.Column(type: "text", nullable: true), + City = table.Column(type: "text", nullable: true), + Company = table.Column(type: "text", nullable: true), + Country = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Discord = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: true), + LastName = table.Column(type: "text", nullable: true), + LinkedIn = table.Column(type: "text", nullable: true), + Name = table.Column(type: "text", nullable: true), + Phone = table.Column(type: "text", nullable: true), + PostalCode = table.Column(type: "text", nullable: true), + Region = table.Column(type: "text", nullable: true), + Slack = table.Column(type: "text", nullable: true), + Tax = table.Column(type: "text", nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Website = table.Column(type: "text", nullable: true), + Whatsapp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Customer", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ProductPriceId1", + table: "ShoppingCartItems", + column: "ProductPriceId1"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingCartItems_ShoppingCartId1", + table: "ShoppingCartItems", + column: "ShoppingCartId1"); + + migrationBuilder.CreateIndex( + name: "IX_Leads_CustomerId1", + table: "Leads", + column: "CustomerId1"); + + migrationBuilder.AddForeignKey( + name: "FK_Leads_Customer_CustomerId", + table: "Leads", + column: "CustomerId", + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Leads_Customers_CustomerId1", + table: "Leads", + column: "CustomerId1", + principalTable: "Customers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1", + table: "ShoppingCartItems", + column: "ProductPriceId1", + principalTable: "ProductPrices", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1", + table: "ShoppingCartItems", + column: "ShoppingCartId1", + principalTable: "ShoppingCarts", + principalColumn: "Id"); + } + } +} diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs new file mode 100644 index 0000000..9013654 --- /dev/null +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs @@ -0,0 +1,789 @@ +// +using System; +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Postgres; +using LiteCharms.Features.TechShop.Products.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.TechShop.Postgres.Migrations +{ + [DbContext(typeof(ShopDbContext))] + [Migration("20260520191059_AddedProductMetadata")] + partial class AddedProductMetadata + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("PackageItem", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discord") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LinkedIn") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Region") + .HasColumnType("text"); + + b.Property("Slack") + .HasColumnType("text"); + + b.Property("Tax") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.Property("Whatsapp") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdGroupId") + .HasColumnType("bigint"); + + b.Property("AdName") + .HasColumnType("bigint"); + + b.Property("AppClickId") + .HasColumnType("text"); + + b.Property("AttributionHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("CampaignId") + .HasColumnType("bigint"); + + b.Property("ClickId") + .HasColumnType("text"); + + b.Property("ClickLocation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("FeedItemId") + .HasColumnType("bigint"); + + b.Property("Source") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TargetId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebClickId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Leads", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationIdType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsInternal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Processed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); + + b.Property("Status") + .HasColumnType("integer"); + + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderRefunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Metadata") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("Thumbnails") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductPrices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShoppingCartId") + .IsUnique(); + + b.ToTable("Quotes", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("ShoppingCarts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductPriceId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartPackages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany("PackageItems") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Leads") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product") + .WithMany("ProductPrices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Quotes") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId"); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId"); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("ShoppingCarts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("ShoppingCart") + .HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Customer"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartItems") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductPrice"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartPackages") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Navigation("PackageItems"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Navigation("Leads"); + + b.Navigation("Orders"); + + b.Navigation("Quotes"); + + b.Navigation("ShoppingCarts"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Navigation("Quote"); + + b.Navigation("Refunds"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Navigation("ProductPrices"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Navigation("Quote"); + + b.Navigation("ShoppingCartItems"); + + b.Navigation("ShoppingCartPackages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs new file mode 100644 index 0000000..06d1891 --- /dev/null +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs @@ -0,0 +1,72 @@ +using System; +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Products.Models; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.TechShop.Postgres.Migrations +{ + /// + public partial class AddedProductMetadata : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Active", + table: "Products", + type: "boolean", + nullable: false, + defaultValue: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: true); + + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "Products", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "Metadata", + table: "Products", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "Products", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "Products"); + + migrationBuilder.DropColumn( + name: "Metadata", + table: "Products"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "Products"); + + migrationBuilder.AlterColumn( + name: "Active", + table: "Products", + type: "boolean", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: false); + } + } +} diff --git a/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs b/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs new file mode 100644 index 0000000..8c1946e --- /dev/null +++ b/LiteCharms.Features.TechShop/Postgres/Migrations/ShopDbContextModelSnapshot.cs @@ -0,0 +1,786 @@ +// +using System; +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Postgres; +using LiteCharms.Features.TechShop.Products.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.TechShop.Postgres.Migrations +{ + [DbContext(typeof(ShopDbContext))] + partial class ShopDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("PackageItem", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discord") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LinkedIn") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Region") + .HasColumnType("text"); + + b.Property("Slack") + .HasColumnType("text"); + + b.Property("Tax") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.Property("Whatsapp") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdGroupId") + .HasColumnType("bigint"); + + b.Property("AdName") + .HasColumnType("bigint"); + + b.Property("AppClickId") + .HasColumnType("text"); + + b.Property("AttributionHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("CampaignId") + .HasColumnType("bigint"); + + b.Property("ClickId") + .HasColumnType("text"); + + b.Property("ClickLocation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("FeedItemId") + .HasColumnType("bigint"); + + b.Property("Source") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TargetId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebClickId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Leads", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationIdType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsInternal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Processed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); + + b.Property("Status") + .HasColumnType("integer"); + + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderRefunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Metadata") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("Thumbnails") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductPrices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShoppingCartId") + .IsUnique(); + + b.ToTable("Quotes", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("ShoppingCarts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductPriceId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartPackages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany("PackageItems") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Leads") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product") + .WithMany("ProductPrices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Quotes") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId"); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId"); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("ShoppingCarts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("ShoppingCart") + .HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Customer"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartItems") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductPrice"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartPackages") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Navigation("PackageItems"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Navigation("Leads"); + + b.Navigation("Orders"); + + b.Navigation("Quotes"); + + b.Navigation("ShoppingCarts"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Navigation("Quote"); + + b.Navigation("Refunds"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Navigation("ProductPrices"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Navigation("Quote"); + + b.Navigation("ShoppingCartItems"); + + b.Navigation("ShoppingCartPackages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.TechShop/Postgres/ShopDbContext.cs b/LiteCharms.Features.TechShop/Postgres/ShopDbContext.cs new file mode 100644 index 0000000..d88c141 --- /dev/null +++ b/LiteCharms.Features.TechShop/Postgres/ShopDbContext.cs @@ -0,0 +1,39 @@ +using LiteCharms.Features.TechShop.CartPackages.Entities; +using LiteCharms.Features.TechShop.Customers.Entities; +using LiteCharms.Features.TechShop.Leads.Entities; +using LiteCharms.Features.TechShop.Notifications.Entities; +using LiteCharms.Features.TechShop.Orders.Entities; +using LiteCharms.Features.TechShop.Products.Entities; +using LiteCharms.Features.TechShop.Quotes.Entities; +using LiteCharms.Features.TechShop.ShoppingCarts.Entities; + +namespace LiteCharms.Features.TechShop.Postgres; + +public class ShopDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Customers { get; set; } + + public DbSet Leads { get; set; } + + public DbSet Orders { get; set; } + + public DbSet OrderRefunds { get; set; } + + public DbSet Products { get; set; } + + public DbSet ProductPrices { get; set; } + + public DbSet Notifications { get; set; } + + public DbSet Quotes { get; set; } + + public DbSet ShoppingCarts { get; set; } + + public DbSet ShoppingCartItems { get; set; } + + public DbSet Packages { get; set; } + + public DbSet PackageItems { get; set; } + + public DbSet ShoppingCartPackages { get; set; } +} diff --git a/LiteCharms.Features.TechShop/Postgres/ShopDbContextFactory.cs b/LiteCharms.Features.TechShop/Postgres/ShopDbContextFactory.cs new file mode 100644 index 0000000..eda522c --- /dev/null +++ b/LiteCharms.Features.TechShop/Postgres/ShopDbContextFactory.cs @@ -0,0 +1,21 @@ +using static LiteCharms.Features.TechShop.Extensions.Postgres; + +namespace LiteCharms.Features.TechShop.Postgres; + +public class ShopDbContextFactory : IDesignTimeDbContextFactory +{ + public ShopDbContext CreateDbContext(string[] args) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddUserSecrets(typeof(ShopDbContext).Assembly) + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables() + .Build(); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(configuration.GetConnectionString(TechShopDbConfigName)); + + return new ShopDbContext(optionsBuilder.Options); + } +} diff --git a/LiteCharms.Entities/Product.cs b/LiteCharms.Features.TechShop/Products/Entities/Product.cs similarity index 70% rename from LiteCharms.Entities/Product.cs rename to LiteCharms.Features.TechShop/Products/Entities/Product.cs index 6b46ff7..2131272 100644 --- a/LiteCharms.Entities/Product.cs +++ b/LiteCharms.Features.TechShop/Products/Entities/Product.cs @@ -1,9 +1,8 @@ -using LiteCharms.Entities.Configuration; - -namespace LiteCharms.Entities; +namespace LiteCharms.Features.TechShop.Products.Entities; [EntityTypeConfiguration] public class Product : Models.Product { + public virtual ICollection? ProductPrices { get; set; } } diff --git a/LiteCharms.Features.TechShop/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.TechShop/Products/Entities/ProductConfiguration.cs new file mode 100644 index 0000000..b35bbd1 --- /dev/null +++ b/LiteCharms.Features.TechShop/Products/Entities/ProductConfiguration.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.TechShop.Products.Entities; + +public class ProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Products"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.Name).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); + builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); + builder.Property(f => f.Description).IsRequired().HasMaxLength(2048); + builder.Property(f => f.ImageUrl).IsRequired(false).HasMaxLength(2048); + builder.Property(f => f.Thumbnails).HasColumnType("jsonb").IsRequired(false); + builder.Property(f => f.Active).HasDefaultValue(false); + builder.Property(f => f.Metadata).HasColumnType("jsonb").IsRequired(false); + } +} diff --git a/LiteCharms.Features.TechShop/Products/Entities/ProductPrice.cs b/LiteCharms.Features.TechShop/Products/Entities/ProductPrice.cs new file mode 100644 index 0000000..cca5817 --- /dev/null +++ b/LiteCharms.Features.TechShop/Products/Entities/ProductPrice.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.TechShop.Products.Entities; + +[EntityTypeConfiguration] +public class ProductPrice : Models.ProductPrice +{ + public virtual Product? Product { get; set; } +} diff --git a/LiteCharms.Entities/Configuration/ProductPriceConfiguration.cs b/LiteCharms.Features.TechShop/Products/Entities/ProductPriceConfiguration.cs similarity index 57% rename from LiteCharms.Entities/Configuration/ProductPriceConfiguration.cs rename to LiteCharms.Features.TechShop/Products/Entities/ProductPriceConfiguration.cs index 5e7cfc9..658bd90 100644 --- a/LiteCharms.Entities/Configuration/ProductPriceConfiguration.cs +++ b/LiteCharms.Features.TechShop/Products/Entities/ProductPriceConfiguration.cs @@ -1,22 +1,23 @@ -namespace LiteCharms.Entities.Configuration; +namespace LiteCharms.Features.TechShop.Products.Entities; public class ProductPriceConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable(nameof(ProductPrice)); + builder.ToTable("ProductPrices"); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd(); - builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.ProductId).IsRequired(); builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2); builder.Property(f => f.Discount).HasPrecision(18, 2); builder.Property(f => f.Active); builder.HasOne(f => f.Product) - .WithMany(p => p.ProductPrices) - .HasForeignKey(f => f.ProductId) + .WithMany(f => f.ProductPrices) + .HasForeignKey(pp => pp.ProductId) + .IsRequired() .OnDelete(DeleteBehavior.Restrict); } } diff --git a/LiteCharms.Features.TechShop/Products/Models/CreateProductModel.cs b/LiteCharms.Features.TechShop/Products/Models/CreateProductModel.cs new file mode 100644 index 0000000..c8bf92a --- /dev/null +++ b/LiteCharms.Features.TechShop/Products/Models/CreateProductModel.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace LiteCharms.Features.TechShop.Products.Models; + +public class CreateProductModel +{ + [MaxLength(128)] + [Required(ErrorMessage = "Product name is required.")] + public string? Name { get; set; } + + [MaxLength(512)] + [Required(ErrorMessage = "Summary is required.")] + public string? Summary { get; set; } + + [MaxLength(2048)] + [Required(ErrorMessage = "Description is required.")] + public string? Description { get; set; } + + [Range(0.01, double.MaxValue, ErrorMessage = "Price must be greater than zero.")] + public decimal Price { get; set; } + + [MaxLength(128)] + [Required(ErrorMessage = "Author metadata is required.")] + public string? Author { get; set; } + + [Required(ErrorMessage = "Publication Date is required.")] + public DateTime PublishDate { get; set; } = DateTime.Today; + + [MaxLength(255)] + [Required(ErrorMessage = "Copyright Information field is required.")] + public string? CopyrightInfo { get; set; } + + [MaxLength(128)] + [Required(ErrorMessage = "ISBN code is required.")] + [RegularExpression(@"^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$", ErrorMessage = "Please enter a valid ISBN-10 or ISBN-13 string.")] + public string? Isbn { get; set; } + + [Required(ErrorMessage = "Primary image is required.")] + public string? ImageUrl { get; set; } + + public List Thumbnails { get; set; } = []; +} diff --git a/LiteCharms.Features.TechShop/Products/Models/Product.cs b/LiteCharms.Features.TechShop/Products/Models/Product.cs new file mode 100644 index 0000000..db9dba7 --- /dev/null +++ b/LiteCharms.Features.TechShop/Products/Models/Product.cs @@ -0,0 +1,26 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.TechShop.Products.Models; + +public class Product +{ + public Guid Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public string? Name { get; set; } + + public string? Summary { get; set; } + + public string? Description { get; set; } + + public string? ImageUrl { get; set; } + + public string[]? Thumbnails { get; set; } + + public bool Active { get; set; } + + public ProductMetadata? Metadata { get; set; } +} diff --git a/LiteCharms.Models/ProductPrice.cs b/LiteCharms.Features.TechShop/Products/Models/ProductPrice.cs similarity index 60% rename from LiteCharms.Models/ProductPrice.cs rename to LiteCharms.Features.TechShop/Products/Models/ProductPrice.cs index 49931ea..0464df1 100644 --- a/LiteCharms.Models/ProductPrice.cs +++ b/LiteCharms.Features.TechShop/Products/Models/ProductPrice.cs @@ -1,12 +1,12 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.TechShop.Products.Models; public class ProductPrice { public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public Guid ProductId { get; set; } diff --git a/LiteCharms.Features.TechShop/Products/Models/Records.cs b/LiteCharms.Features.TechShop/Products/Models/Records.cs new file mode 100644 index 0000000..8c7d274 --- /dev/null +++ b/LiteCharms.Features.TechShop/Products/Models/Records.cs @@ -0,0 +1,18 @@ +using LiteCharms.Features.Models; + +namespace LiteCharms.Features.TechShop.Products.Models; + +public record CreateProduct +{ + public required string Name { get; set; } + + public required string Summary { get; set; } + + public required string Description { get; set; } + + public required string ImageUrl { get; set; } + + public string[]? Thumbnails { get; set; } + + public ProductMetadata? Metadata { get; set; } +} diff --git a/LiteCharms.Features.TechShop/Products/ProductService.cs b/LiteCharms.Features.TechShop/Products/ProductService.cs new file mode 100644 index 0000000..c414262 --- /dev/null +++ b/LiteCharms.Features.TechShop/Products/ProductService.cs @@ -0,0 +1,278 @@ +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Postgres; +using LiteCharms.Features.TechShop.Products.Models; + +namespace LiteCharms.Features.TechShop.Products; + +public class ProductService(IDbContextFactory contextFactory) +{ + public async ValueTask ChangeProductPriceStatusAsync(Guid productPriceId, bool active, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var price = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + + if (price is null) + return Result.Fail($"Could not find product price with ID {productPriceId}"); + + price.Active = active; + price.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to change product price by ID {productPriceId}"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask ChangeProductStatusAsync(Guid productId, bool active, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + + if (product is null) + return Result.Fail($"Could not find product with ID {productId}"); + + product.Active = active; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to change product status by ID {productId}"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateProductAsync(CreateProduct request, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken)) + return Result.Fail($"A product by the same name '{request.Name}' already exists"); + + var newProduct = context.Products.Add(new Entities.Product + { + Name = request.Name, + Summary = request.Summary, + Description = request.Description, + ImageUrl = request.ImageUrl, + Thumbnails = request.Thumbnails, + Metadata = request.Metadata + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newProduct.Entity.Id) + : Result.Fail($"Failed to create new product '{request.Name}'"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateProductPriceAsync(Guid productId, decimal price, decimal discount = 0, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var newProductPrice = context.ProductPrices.Add(new Entities.ProductPrice + { + Price = price, + Discount = discount, + ProductId = productId + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newProductPrice.Entity.Id) + : Result.Fail($"Failed to create new product price for product id {productId}"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductAsync(Guid productId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + + return product is not null + ? Result.Ok(product.ToModel()) + : Result.Fail(new Error($"Product with ID {productId} not found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductPriceAsync(Guid productPriceId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Products.AnyAsync(p => p.Id == productPriceId, cancellationToken)) + return Result.Fail(new Error($"Product {productPriceId} not found.")); + + var productPrice = await context.ProductPrices.AsNoTracking() + .OrderByDescending(pp => pp.CreatedAt) + .FirstOrDefaultAsync(pp => pp.Id == productPriceId, cancellationToken); + + return productPrice is not null + ? Result.Ok(productPrice.ToModel()) + : Result.Fail(new Error($"Product price {productPriceId} not found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductPricesAsync(int maxRecords = 1000, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var products = await context.ProductPrices.AsNoTracking() + .OrderByDescending(o => o.Id) + .Take(maxRecords) + .ToArrayAsync(cancellationToken); + + return Result.Ok(products.Select(p => p.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductsAsync(int maxRecords = 1000, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var products = await context.Products.AsNoTracking() + .OrderByDescending(o => o.Id) + .Take(maxRecords) + .ToArrayAsync(cancellationToken); + + return Result.Ok(products.Select(p => p.ToModel()).ToArray()); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> ReplaceProductPriceAsync(Guid productPriceId, decimal price, decimal discount = 0, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var existingPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + + if (existingPrice is null) + return Result.Fail($"Could not find product price with ID {productPriceId}"); + + existingPrice.Active = false; + existingPrice.UpdatedAt = DateTime.UtcNow; + + if (!(await context.SaveChangesAsync(cancellationToken) > 0)) + return Result.Fail($"Failed to deactivate existing price of ID {productPriceId}, try again later"); + + var result = await CreateProductPriceAsync(existingPrice.ProductId, price, discount, cancellationToken); + + if (result.IsFailed) + { + var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + + existingPrice.Active = true; + existingPrice.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Fail("Reverted to old price, creation of new price failed") + : Result.Fail($"Failed to reactivate price of ID {productPriceId} after new price creation failed"); + } + + return Result.Ok(result.Value); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask SetProductPriceStatusAsync(Guid productPriceId, bool active, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var productPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + + if (productPrice is null) + return Result.Fail($"Could not find product price with ID {productPriceId}"); + + productPrice.Active = active; + productPrice.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to change product price status by ID {productPriceId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateProductMetadataAsync(Guid productId, ProductMetadata metadata, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + + if (product is null) + return Result.Fail($"Could not find product with ID {productId}"); + + product.Metadata = metadata; + product.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update product metadata by ID {productId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Entities/Quote.cs b/LiteCharms.Features.TechShop/Quotes/Entities/Quote.cs similarity index 52% rename from LiteCharms.Entities/Quote.cs rename to LiteCharms.Features.TechShop/Quotes/Entities/Quote.cs index 8bacd89..aa4ada3 100644 --- a/LiteCharms.Entities/Quote.cs +++ b/LiteCharms.Features.TechShop/Quotes/Entities/Quote.cs @@ -1,13 +1,15 @@ -using LiteCharms.Entities.Configuration; +using LiteCharms.Features.TechShop.Customers.Entities; +using LiteCharms.Features.TechShop.Orders.Entities; +using LiteCharms.Features.TechShop.ShoppingCarts.Entities; -namespace LiteCharms.Entities; +namespace LiteCharms.Features.TechShop.Quotes.Entities; [EntityTypeConfiguration] public class Quote : Models.Quote { public virtual Customer? Customer { get; set; } - public virtual ShoppingCart? ShoppingCart { get; set; } - public virtual Order? Order { get; set; } + + public virtual ShoppingCart? ShoppingCart { get; set; } } diff --git a/LiteCharms.Features.TechShop/Quotes/Entities/QuoteConfiguration.cs b/LiteCharms.Features.TechShop/Quotes/Entities/QuoteConfiguration.cs new file mode 100644 index 0000000..3dfdc12 --- /dev/null +++ b/LiteCharms.Features.TechShop/Quotes/Entities/QuoteConfiguration.cs @@ -0,0 +1,33 @@ +namespace LiteCharms.Features.TechShop.Quotes.Entities; + +public class QuoteConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Quotes"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); + builder.Property(f => f.ExpiredAt).IsRequired(false).HasDefaultValueSql(null); + builder.Property(f => f.CustomerId).IsRequired(); + builder.Property(f => f.OrderId); + builder.Property(f => f.ShoppingCartId); + builder.Property(f => f.Status).IsRequired().HasConversion(); + builder.Property(f => f.InvoiceUrl).IsRequired(false).HasMaxLength(2048); + builder.Property(f => f.Reason).IsRequired(false); + + builder.HasOne(q => q.Customer) + .WithMany(c => c.Quotes) + .HasForeignKey(q => q.CustomerId) + .IsRequired(); + + builder.HasOne(q => q.Order) + .WithOne(o => o.Quote) + .HasForeignKey(q => q.OrderId); + + builder.HasOne(q => q.ShoppingCart) + .WithOne(o => o.Quote) + .HasForeignKey(q => q.ShoppingCartId); + } +} diff --git a/LiteCharms.Features.TechShop/Quotes/Models/Quote.cs b/LiteCharms.Features.TechShop/Quotes/Models/Quote.cs new file mode 100644 index 0000000..3effe97 --- /dev/null +++ b/LiteCharms.Features.TechShop/Quotes/Models/Quote.cs @@ -0,0 +1,24 @@ +namespace LiteCharms.Features.TechShop.Quotes.Models; + +public class Quote +{ + public Guid Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public DateTime? ExpiredAt { get; set; } + + public Guid CustomerId { get; set; } + + public Guid? OrderId { get; set; } + + public Guid? ShoppingCartId { get; set; } + + public QuoteStatus Status { get; set; } + + public string? InvoiceUrl { get; set; } + + public string? Reason { get; set; } +} diff --git a/LiteCharms.Features.TechShop/Quotes/QuoteService.cs b/LiteCharms.Features.TechShop/Quotes/QuoteService.cs new file mode 100644 index 0000000..4e08bb8 --- /dev/null +++ b/LiteCharms.Features.TechShop/Quotes/QuoteService.cs @@ -0,0 +1,156 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.Models; +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Postgres; +using LiteCharms.Features.TechShop.Quotes.Models; + +namespace LiteCharms.Features.TechShop.Quotes; + +public class QuoteService(IDbContextFactory contextFactory) +{ + public async ValueTask AssignQuoteToOrderAsync(Guid quoteId, Guid orderId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var quote = await context.Quotes.FirstOrDefaultAsync(o => o.Id == quoteId, cancellationToken); + + if (quote is null) + return Result.Fail(new Error($"Quote with id {orderId} not found")); + + if (!await context.Orders.AnyAsync(q => q.Id == orderId, cancellationToken)) + return Result.Fail(new Error($"Order with id {quoteId} not found")); + + if (quote.OrderId == orderId) + return Result.Fail(new Error($"Quote with id {quoteId} is already assigned to order with id {orderId}")); + + quote.OrderId = orderId; + quote.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to assign quote with id {quoteId} to order with id {orderId}")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask AssignQuoteToShoppingCartAsync(Guid quoteId, Guid shoppingCartId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var quote = await context.Quotes.FirstOrDefaultAsync(o => o.Id == quoteId, cancellationToken); + + if (quote is null) + return Result.Fail(new Error($"Quote with id {quoteId} not found")); + + if (!await context.ShoppingCarts.AnyAsync(q => q.Id == shoppingCartId, cancellationToken)) + return Result.Fail(new Error($"Shopping Cart with id {shoppingCartId} not found")); + + quote.ShoppingCartId = shoppingCartId; + quote.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to assign quote to shopping cart")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerQuotesAsync(Guid customerId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with Id {customerId} does not exist.")); + + var quotes = await context.Quotes.AsNoTracking() + .Where(q => q.CustomerId == customerId).ToArrayAsync(cancellationToken); + + return quotes?.Length > 0 + ? Result.Ok(quotes.Select(q => q.ToModel()).ToArray()) + : Result.Fail(new Error($"No quotes found for customer with Id {customerId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetQuoteAsync(Guid quoteId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var quote = await context.Quotes.AsNoTracking().FirstOrDefaultAsync(q => q.Id == quoteId, cancellationToken); + + return quote is not null + ? Result.Ok(quote.ToModel()) + : Result.Fail(new Error($"Quote with ID {quoteId} not found.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetQuotesAsync(DateRange range, CancellationToken cancellationToken = default) + { + try + { + var fromDate = range.From.ToDateTime(TimeOnly.MinValue); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var quotes = await context.Quotes.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) + .Take(range.MaxRecords) + .ToArrayAsync(cancellationToken); + + return quotes?.Length > 0 + ? Result.Ok(quotes.Select(o => o.ToModel()).ToArray()) + : Result.Fail(new Error($"No quotes found for the specified date range {range.From} - {range.To}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateQuoteStatusAsync(Guid quoteId, QuoteStatus status, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var quote = await context.Quotes.FirstOrDefaultAsync(q => q.Id == quoteId, cancellationToken); + + if (quote is null) + return Result.Fail(new Error("Quote not found.")); + + quote.Status = status; + quote.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail(new Error("Failed to update quote status.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Entities/ShoppingCart.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCart.cs similarity index 52% rename from LiteCharms.Entities/ShoppingCart.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCart.cs index 3c974dc..153962e 100644 --- a/LiteCharms.Entities/ShoppingCart.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCart.cs @@ -1,6 +1,8 @@ -using LiteCharms.Entities.Configuration; +using LiteCharms.Features.TechShop.Customers.Entities; +using LiteCharms.Features.TechShop.Orders.Entities; +using LiteCharms.Features.TechShop.Quotes.Entities; -namespace LiteCharms.Entities; +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; [EntityTypeConfiguration] public class ShoppingCart : Models.ShoppingCart @@ -12,4 +14,6 @@ public class ShoppingCart : Models.ShoppingCart public virtual Quote? Quote { get; set; } public virtual ICollection? ShoppingCartItems { get; set; } + + public virtual ICollection? ShoppingCartPackages { get; set; } } diff --git a/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs new file mode 100644 index 0000000..a59b01d --- /dev/null +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartConfiguration.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; + +public class ShoppingCartConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ShoppingCarts"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); + builder.Property(f => f.CustomerId).IsRequired(); + builder.Property(f => f.OrderId); + + builder.HasOne(s => s.Customer) + .WithMany(c => c.ShoppingCarts) + .HasForeignKey(s => s.CustomerId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(s => s.Order) + .WithOne(o => o.ShoppingCart) + .HasForeignKey(s => s.OrderId) + .OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItem.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItem.cs new file mode 100644 index 0000000..5171b61 --- /dev/null +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItem.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.TechShop.Products.Entities; + +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; + +[EntityTypeConfiguration] +public class ShoppingCartItem : Models.ShoppingCartItem +{ + public virtual ShoppingCart? ShoppingCart { get; set; } + + public virtual ProductPrice? ProductPrice { get; set; } +} diff --git a/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs new file mode 100644 index 0000000..1e7b18a --- /dev/null +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartItemConfiguration.cs @@ -0,0 +1,28 @@ +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; + +public class ShoppingCartItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ShoppingCartItems"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null); + builder.Property(f => f.Quantity).IsRequired().HasDefaultValue(1); + builder.Property(f => f.ShoppingCartId).IsRequired(); + builder.Property(f => f.ProductPriceId).IsRequired(); + + builder.HasOne(f => f.ShoppingCart) + .WithMany(s => s.ShoppingCartItems) + .HasForeignKey(f => f.ShoppingCartId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.ProductPrice) + .WithMany() + .HasForeignKey(f => f.ProductPriceId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackage.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackage.cs new file mode 100644 index 0000000..7745f4d --- /dev/null +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackage.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.TechShop.CartPackages.Entities; + +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; + +[EntityTypeConfiguration] +public class ShoppingCartPackage : Models.ShoppingCartPackage +{ + public virtual ShoppingCart? ShoppingCart { get; set; } + + public virtual Package? Package { get; set; } +} diff --git a/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs new file mode 100644 index 0000000..43f88a6 --- /dev/null +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Entities/ShoppingCartPackageConfiguration.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities; + +public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ShoppingCartPackages"); + + builder.HasKey(f => f.Id); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.ShoppingCartId).IsRequired(); + builder.Property(f => f.PackageId).IsRequired(); + + builder.HasOne(f => f.ShoppingCart) + .WithMany(s => s.ShoppingCartPackages) + .HasForeignKey(scp => scp.ShoppingCartId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(f => f.Package) + .WithMany() + .HasForeignKey(scp => scp.PackageId) + .IsRequired() + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCart.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCart.cs new file mode 100644 index 0000000..0894a52 --- /dev/null +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCart.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.TechShop.ShoppingCarts.Models; + +public class ShoppingCart +{ + public Guid Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public Guid CustomerId { get; set; } + + public Guid? OrderId { get; set; } +} diff --git a/LiteCharms.Models/ShoppingCartItem.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartItem.cs similarity index 56% rename from LiteCharms.Models/ShoppingCartItem.cs rename to LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartItem.cs index 3530515..e3760e4 100644 --- a/LiteCharms.Models/ShoppingCartItem.cs +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartItem.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Models; +namespace LiteCharms.Features.TechShop.ShoppingCarts.Models; public class ShoppingCartItem { @@ -8,9 +8,9 @@ public class ShoppingCartItem public Guid ProductPriceId { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset UpdatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } public int Quantity { get; set; } } diff --git a/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartPackage.cs b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartPackage.cs new file mode 100644 index 0000000..c040c5e --- /dev/null +++ b/LiteCharms.Features.TechShop/ShoppingCarts/Models/ShoppingCartPackage.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.TechShop.ShoppingCarts.Models; + +public class ShoppingCartPackage +{ + public Guid Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public Guid ShoppingCartId { get; set; } + + public Guid PackageId { get; set; } +} diff --git a/LiteCharms.Features.TechShop/ShoppingCarts/ShoppingCartService.cs b/LiteCharms.Features.TechShop/ShoppingCarts/ShoppingCartService.cs new file mode 100644 index 0000000..c8413a8 --- /dev/null +++ b/LiteCharms.Features.TechShop/ShoppingCarts/ShoppingCartService.cs @@ -0,0 +1,297 @@ +using LiteCharms.Features.TechShop.Extensions; +using LiteCharms.Features.TechShop.Postgres; +using LiteCharms.Features.TechShop.ShoppingCarts.Models; + +namespace LiteCharms.Features.TechShop.ShoppingCarts; + +public class ShoppingCartService(IDbContextFactory contextFactory) +{ + public async ValueTask AddItemToShoppingCartAsync(Guid shoppingCartId, Guid productPriceId, int quantity, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ProductPrices.AnyAsync(c => c.Id == productPriceId, cancellationToken)) + return Result.Fail($"Product item could not be found with id {productPriceId}"); + + var cart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == shoppingCartId, cancellationToken); + + if (cart is null) + return Result.Fail($"Shopping cart could not be found with id {shoppingCartId}"); + + if (cart.ShoppingCartItems?.Any(i => i.ProductPriceId == productPriceId) == true) + return Result.Fail($"Item already in shopping cart with id {shoppingCartId}"); + + context.ShoppingCartItems.Add(new Entities.ShoppingCartItem + { + ShoppingCartId = shoppingCartId, + ProductPriceId = productPriceId, + Quantity = quantity + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to add cart item with id {productPriceId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask AddPackageToShoppingCartAsync(Guid shoppingCartId, Guid packageId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Packages.AnyAsync(p => p.Id == packageId, cancellationToken)) + return Result.Fail($"Package cold not be found by ID {packageId}"); + + var shoppingCart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == shoppingCartId, cancellationToken); + + if (shoppingCart is null) + return Result.Fail($"Shopping cart could not be found by ID {shoppingCartId}"); + + if (!await context.ShoppingCartPackages.AnyAsync(cp => cp.ShoppingCartId == shoppingCartId, cancellationToken)) + return Result.Fail($"Package {packageId} is already in the cart"); + + var newShoppingCartPackage = context.ShoppingCartPackages.Add(new Entities.ShoppingCartPackage + { + ShoppingCartId = shoppingCartId, + PackageId = packageId + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Could not add package of id {packageId} to shopping cart {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateShoppingCartAsync(Guid customerId, Guid orderId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail($"Customer could not be found with id {customerId}"); + + var cart = context.ShoppingCarts.Add(new Entities.ShoppingCart + { + CustomerId = customerId, + OrderId = orderId + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(cart.Entity.Id) + : Result.Fail($"Failed to create shopping cart for customer id {customerId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask EmptyShoppingCartAsync(Guid shoppingCartId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == shoppingCartId, cancellationToken)) + return Result.Fail($"Shopping could not be found with id {shoppingCartId}"); + + if (await context.ShoppingCartItems.CountAsync(i => i.ShoppingCartId == shoppingCartId, cancellationToken) == 0) + return Result.Ok(); + + var cartItems = await context.ShoppingCartItems.Where(i => i.ShoppingCartId == shoppingCartId).ToListAsync(cancellationToken); + + context.RemoveRange(cartItems); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Could not empty cart with id {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCustomerShoppingCartsAsync(Guid customerId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) + return Result.Fail(new Error($"Customer with Id {customerId} does not exist.")); + + var shoppingCarts = await context.ShoppingCarts.Where(sc => sc.CustomerId == customerId).ToArrayAsync(cancellationToken); + + return shoppingCarts?.Length > 0 + ? Result.Ok(shoppingCarts.Select(c => c.ToModel()).ToArray()) + : Result.Fail(new Error($"No shopping carts found for customer with Id {customerId}.")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShoppingCartAsync(Guid shoppingCartId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var cart = await context.ShoppingCarts.AsNoTracking().FirstOrDefaultAsync(c => c.Id == shoppingCartId, cancellationToken); + + return cart is not null + ? Result.Ok(cart.ToModel()) + : Result.Fail($"Failed to find shopping cart with id {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShoppingCartItemsAsync(Guid shoppingCartId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(i => i.Id == shoppingCartId, cancellationToken)) + return Result.Fail($"Shopping cart could not be found with id {shoppingCartId}"); + + var items = await context.ShoppingCartItems.AsNoTracking() + .Where(i => i.ShoppingCartId == shoppingCartId).ToArrayAsync(cancellationToken); + + return items?.Length > 0 + ? Result.Ok(items.Select(i => i.ToModel()).ToArray()) + : Result.Fail($"Failed to retrieve shopping cart items with id {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetShoppingCartPackagesAsync(Guid shoppingCartId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == shoppingCartId, cancellationToken)) + return Result.Fail($"Shopping cart could not be found by ID {shoppingCartId}"); + + var packages = await context.ShoppingCartPackages.AsNoTracking() + .OrderByDescending(o => o.CreatedAt) + .Where(cp => cp.ShoppingCartId == shoppingCartId) + .ToArrayAsync(cancellationToken); + + return packages?.Length > 0 + ? Result.Ok(packages.Select(p => p.ToModel()).ToArray()) + : Result.Fail($"Could not find packaged in shopping cart by ID {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask RemovePackageFromShoppingCartAsync(Guid shoppingCartId, Guid shoppingCartPackageId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == shoppingCartId, cancellationToken)) + return Result.Fail($"Shopping cart could not be found by ID {shoppingCartId}"); + + if (!await context.ShoppingCartPackages.AnyAsync(p => p.Id == shoppingCartPackageId, cancellationToken)) + return Result.Fail($"Shopping cart package {shoppingCartPackageId} is not in the shopping cart {shoppingCartId}"); + + var shoppingCartPackage = await context.ShoppingCartPackages.FirstOrDefaultAsync(cp => cp.Id == shoppingCartPackageId, cancellationToken); + + if (shoppingCartPackage is null) + return Result.Ok(); + + context.ShoppingCartPackages.Remove(shoppingCartPackage!); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Could remove package of id {shoppingCartPackageId} from shopping cart {shoppingCartId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask RemoveShoppingCartItemAsync(Guid shoppingCartId, Guid shoppingCartItemId, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ProductPrices.AnyAsync(c => c.Id == shoppingCartItemId, cancellationToken)) + return Result.Fail($"Product item could not be found with id {shoppingCartItemId}"); + + var cart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == shoppingCartId, cancellationToken); + + if (cart is null) + return Result.Fail($"Shopping cart item could not be found with id {shoppingCartId}"); + + var item = await context.ShoppingCartItems.FirstOrDefaultAsync(i => i.Id == shoppingCartItemId, cancellationToken); + + if (item is null) return Result.Ok(); + + context.ShoppingCartItems.Remove(item); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to remove shopping cart item with id {shoppingCartItemId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateShoppingCartItemAsync(Guid shoppingCartId, Guid shoppingCartItemId, int quantity, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.ShoppingCarts.AnyAsync(c => c.Id == shoppingCartId, cancellationToken)) + return Result.Fail($"Shopping could not be found with id {shoppingCartId}"); + + var item = await context.ShoppingCartItems.FirstOrDefaultAsync(i => i.ShoppingCartId == shoppingCartId, cancellationToken); + + if (item is null) + return Result.Fail($"Shopping cart item could not be found with id {shoppingCartItemId}"); + + item.Quantity = quantity; + item.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update cart item quntity"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.TechShop/appsettings.json b/LiteCharms.Features.TechShop/appsettings.json new file mode 100644 index 0000000..aec5c2e --- /dev/null +++ b/LiteCharms.Features.TechShop/appsettings.json @@ -0,0 +1,22 @@ +{ + "Email": { + "Credentials": { + "Username": "shop@litecharms.co.za" + }, + "Port": 465, + "Host": "mail.litecharms.co.za", + "UseSsl": true + }, + "Monitoring": { + "ApiKey": "", + "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", + "ServiceName": "LiteCharms.LeadGenerator" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/LiteCharms.Features.Tests.Common/Fixture.cs b/LiteCharms.Features.Tests.Common/Fixture.cs new file mode 100644 index 0000000..5694f5f --- /dev/null +++ b/LiteCharms.Features.Tests.Common/Fixture.cs @@ -0,0 +1,46 @@ +using LiteCharms.Features.Extensions; +using LiteCharms.Features.MidrandBooks.Extensions; + +namespace LiteCharms.Features.Tests.Common; + +public class Fixture : IDisposable +{ + public IConfiguration Configuration { get; set; } + + public IServiceProvider Services { get; set; } + + public IMediator Mediator { get; set; } + + private readonly CancellationTokenSource cancellationTokenSource = new(); + + public CancellationToken CancellationToken => cancellationTokenSource.Token; + + public Fixture() + { + Configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddUserSecrets() + .AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .Build(); + + Services = new ServiceCollection() + .AddLogging() + .AddMediator() + .AddEmailServiceBus() + .AddGarageS3(Configuration) + .AddMidrandShopDatabase(Configuration) + .AddEmailServices(Configuration) + .AddSingleton(Configuration) + .AddShopServices() + .AddHashServices(Configuration) + .AddLiteCharmsApiSecurity(Configuration) + .AddSecurityApiSdk(Configuration) + .AddPayfastServices(Configuration) + .BuildServiceProvider(); ; + + Mediator = Services.GetRequiredService(); + } + + public void Dispose() { } +} diff --git a/LiteCharms.Features.Tests.Common/IntegrationFactAttribute.cs b/LiteCharms.Features.Tests.Common/IntegrationFactAttribute.cs new file mode 100644 index 0000000..f62577c --- /dev/null +++ b/LiteCharms.Features.Tests.Common/IntegrationFactAttribute.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.Tests.Common; + +public class IntegrationFactAttribute : FactAttribute +{ + public IntegrationFactAttribute() + { + if(!Debugger.IsAttached) + Skip = "This test requires the debugger to be attached."; + } +} diff --git a/LiteCharms.Features.Tests.Common/LiteCharms.Features.Tests.Common.csproj b/LiteCharms.Features.Tests.Common/LiteCharms.Features.Tests.Common.csproj new file mode 100644 index 0000000..44d6d70 --- /dev/null +++ b/LiteCharms.Features.Tests.Common/LiteCharms.Features.Tests.Common.csproj @@ -0,0 +1,76 @@ + + + + net10.0 + enable + enable + 0521f45a-eba0-457f-bb5e-c3680f65d8b1 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/LiteCharms.Features.Tests.Common/appsettings.json b/LiteCharms.Features.Tests.Common/appsettings.json new file mode 100644 index 0000000..5bc3088 --- /dev/null +++ b/LiteCharms.Features.Tests.Common/appsettings.json @@ -0,0 +1,49 @@ +{ + "PayfastSettings": { + "CheckoutUrl": "https://sandbox.payfast.co.za/eng/process", + "ValidHosts": [ + "www.payfast.co.za", + "sandbox.payfast.co.za", + "ips.payfast.co.za", + "api.payfast.co.za", + "payment.payfast.io" + ] + }, + "LiteCharmsSettings": { + "Authority": "https://sts.security.khongisa.co.za", + "Audience": "midrandbooks-api" + }, + "LiteCharmsClientSettings": { + "Authority": "https://sts.security.khongisa.co.za", + "GrantType": "client_credentials", + "Scope": "midrandbooks-api" + }, + "HasherSettings": { + "MinHashLength": 11 + }, + "BookshopS3Settings": { + "ServiceUrl": "http://192.168.1.177:30900", + "Region": "garage", + "BucketName": "bookshop", + "CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za" + }, + "Email": { + "Credentials": { + "Username": "shop@litecharms.co.za" + }, + "Port": 465, + "Host": "mail.litecharms.co.za", + "UseSsl": true + }, + "Monitoring": { + "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", + "ServiceName": "LiteCharms.LeadGenerator" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/LiteCharms.Features.Tests/HashServiceFeatureTests.cs b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs new file mode 100644 index 0000000..8f2a331 --- /dev/null +++ b/LiteCharms.Features.Tests/HashServiceFeatureTests.cs @@ -0,0 +1,131 @@ +using LiteCharms.Features.Hasher; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.Tests; + +public class HashServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly HashService hashService = fixture.Services.GetRequiredService(); + + [Fact] + public void StringToSha256Hash_Should_GenerateHash() + { + var input = "We are the best"; + var expectedHash = "96E17275B53F6BEB7A0D1C4F789F226D3C71CBE398585F25B3028F2B432E78AB"; + + var result = HashService.StringToSha256Hash(input); + + Assert.NotNull(result); + Assert.True(HashService.IsSha256Hash(result)); + Assert.Equal(expectedHash, result); + } + + [Fact] + public void StreamToSha256Hash_Should_GenerateHash() + { + var input = "We are successful"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(input)); + var expectedHash = "C27872EE494B09D72203C98FC858268F3CD3492D62AA7B766A873520C2C73AFB"; + + var result = HashService.StreamToSha256Hash(stream); + + Assert.NotNull(result); + Assert.True(HashService.IsSha256Hash(result)); + Assert.Equal(expectedHash, result); + } + + [Fact] + public void BytesToSha256Hash_Should_GenerateHash() + { + var inputBytes = Encoding.UTF8.GetBytes("We are wealthy"); + var expectedHash = "3876BF98F6E4A8E42B22C40415687D6FF13F0E887F3F508B71852298FC665737"; + + var result = HashService.BytesToSha256Hash(inputBytes); + + Assert.NotNull(result); + Assert.True(HashService.IsSha256Hash(result)); + Assert.Equal(expectedHash, result); + } + + [Fact] + public void ToMd5Hash_Should_GenerateHash() + { + var input = "We manifest our desired destiny"; + var expectedMd5Lowercase = "6c7816869bcebe4634f7afe9c66dfa08"; + + var result = HashService.ToMd5Hash(input); + + Assert.True(result.IsSuccess); + Assert.True(HashService.IsMd5Hash(result.Value)); + Assert.Equal(expectedMd5Lowercase, result.Value); + } + + [Fact] + public void HashEncodeHex_Should_GenerateHash() + { + var validHexInput = "DEADBEEF42"; + + var result = hashService.HashEncodeHex(validHexInput); + + Assert.True(result.IsSuccess); + Assert.False(string.IsNullOrWhiteSpace(result.Value)); + } + + [Fact] + public void HashEncodeIntId_Should_GenerateHash() + { + int targetId = 42; + + var result = hashService.HashEncodeIntId(targetId); + + Assert.True(result.IsSuccess); + Assert.True(result.Value.Length >= 10); + } + + [Fact] + public void HashEncodeLongId_Should_GenerateHash() + { + long targetId = 9904185012L; + + var result = hashService.HashEncodeLongId(targetId); + + Assert.True(result.IsSuccess); + Assert.True(result.Value.Length >= 10); + } + + [Fact] + public void DecodeIntIdHash_Should_GenerateHash() + { + int originalId = 88041; + var hashedString = hashService.HashEncodeIntId(originalId).Value; + + var result = hashService.DecodeIntIdHash(hashedString); + + Assert.True(result.IsSuccess); + Assert.Equal(originalId, result.Value); + } + + [Fact] + public void DecodeLongIdHash_Should_GenerateHash() + { + long originalId = 9081230491823L; + var hashedString = hashService.HashEncodeLongId(originalId).Value; + + var result = hashService.DecodeLongIdHash(hashedString); + + Assert.True(result.IsSuccess); + Assert.Equal(originalId, result.Value); + } + + [Fact] + public void DecodeHexHash_Should_GenerateHash() + { + var originalHex = "ABCDEF12345"; + var hashedString = hashService.HashEncodeHex(originalHex).Value; + + var result = hashService.DecodeHexHash(hashedString); + + Assert.True(result.IsSuccess); + Assert.Equal(originalHex, result.Value); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj new file mode 100644 index 0000000..239381b --- /dev/null +++ b/LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj @@ -0,0 +1,48 @@ + + + + net10.0 + enable + enable + false + 62fa604a-1340-4edb-9ddd-3305fcf46fca + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LiteCharms.Features.Tests/LiteCharmsApiFeatureTests.cs b/LiteCharms.Features.Tests/LiteCharmsApiFeatureTests.cs new file mode 100644 index 0000000..d33f897 --- /dev/null +++ b/LiteCharms.Features.Tests/LiteCharmsApiFeatureTests.cs @@ -0,0 +1,19 @@ +using LiteCharms.Features.Api; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.Tests; + +public sealed class LiteCharmsApiFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly TokenService tokenService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task TokenService_GenerateTokenAsync_ShouldReturn_TokenInResult() + { + var result = await tokenService.GenerateAsync(fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + Assert.NotEmpty(result.Value.AccessToken!); + } +} diff --git a/LiteCharms.Features.Tests/PayfastFeatureTests.cs b/LiteCharms.Features.Tests/PayfastFeatureTests.cs new file mode 100644 index 0000000..e1fec0f --- /dev/null +++ b/LiteCharms.Features.Tests/PayfastFeatureTests.cs @@ -0,0 +1,18 @@ +using LiteCharms.Features.Api.Configuration; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.Tests; + +public sealed class PayfastFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly PayfastSettings payfastSettings = fixture.Services.GetRequiredService>().Value; + + [IntegrationFact] + public void PayfastSettings_ShouldFail_IfNotLoaded() + { + Assert.NotEmpty(payfastSettings.CheckoutUrl!); + Assert.NotEmpty(payfastSettings.MerchantId!); + Assert.NotEmpty(payfastSettings.MerchantKey!); + Assert.NotEmpty(payfastSettings.Passphrase!); + } +} diff --git a/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs b/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs new file mode 100644 index 0000000..b355eac --- /dev/null +++ b/LiteCharms.Features.Tests/S3ServiceFeatureTests.cs @@ -0,0 +1,55 @@ +using LiteCharms.Features.S3.Abstractions; +using LiteCharms.Features.Tests.Common; + +namespace LiteCharms.Features.Tests; + +public class S3ServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture +{ + [Fact] + public async Task BookshopS3Service_MustReturnUrl() + { + var service = fixture.Services.GetKeyedService(S3.Constants.BookshopBucketName); + + var fileName = "appsettings.json"; + + string path = Path.Combine(Directory.GetCurrentDirectory(), fileName); + + Assert.True(File.Exists(path)); + + var stream = File.OpenRead(path); + + var result = await service!.UploadFileAsync(fileName, stream, MimeKit.MimeTypes.GetMimeType(fileName)); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + Assert.NotEmpty(result.Value); + + output.WriteLine(result.Value); + } + + [Fact] + public async Task BookshopS3Service_MustDeleteFile() + { + var service = fixture.Services.GetKeyedService(S3.Constants.BookshopBucketName); + + var fileName = "appsettings.json"; + + string path = Path.Combine(Directory.GetCurrentDirectory(), fileName); + + Assert.True(File.Exists(path)); + + var stream = File.OpenRead(path); + + var uploadResult = await service!.UploadFileAsync(fileName, stream, MimeKit.MimeTypes.GetMimeType(fileName)); + + Assert.True(uploadResult.IsSuccess); + Assert.NotNull(uploadResult.Value); + Assert.NotEmpty(uploadResult.Value); + + var fileKey = uploadResult.Value.Split('/').Last(); + + var deleteResult = await service!.DeleteFileAsync(fileKey); + + Assert.True(deleteResult.IsSuccess); + } +} diff --git a/LiteCharms.Features.Tests/http/litecharms/app.http b/LiteCharms.Features.Tests/http/litecharms/app.http new file mode 100644 index 0000000..09e5d9f --- /dev/null +++ b/LiteCharms.Features.Tests/http/litecharms/app.http @@ -0,0 +1,6 @@ +### Authentik Token Request (Service Account Explicit) +POST {{authority}}/connect/token +Content-Type: application/x-www-form-urlencoded +Accept-Encoding: identity + +grant_type={{grantType}}&client_id={{clientId}}&client_secret={{clientSecret}}&scope={{scope}} diff --git a/LiteCharms.Features.Tests/http/litecharms/http-client.env.json b/LiteCharms.Features.Tests/http/litecharms/http-client.env.json new file mode 100644 index 0000000..c14a05b --- /dev/null +++ b/LiteCharms.Features.Tests/http/litecharms/http-client.env.json @@ -0,0 +1,9 @@ +{ + "uat": { + "authority": "https://sts.security.khongisa.co.za", + "grantType": "client_credentials", + "clientId": "midrandbooks-api-scaler-uat", + "clientSecret": "secret_0a8dc1f99061590a52b1272db3a1871d2761c79fbd058b2a968911029e4b208a", + "scope": "midrandbooks-api" + } +} diff --git a/LiteCharms.Features.Tests/http/midrandshop-api/app.http b/LiteCharms.Features.Tests/http/midrandshop-api/app.http new file mode 100644 index 0000000..7297a16 --- /dev/null +++ b/LiteCharms.Features.Tests/http/midrandshop-api/app.http @@ -0,0 +1,8 @@ +## Payfast Payment Confirmation +# This endpoint is used by Payfast to confirm the payment status of a transaction. +# It receives a POST request with the payment details and updates the order status accordingly. + +POST {{baseUrl}}/v1/payments/payfast/confirm +Content-Type: application/x-www-form-urlencoded + +amount={{amount}}&item_name={{item_name}}&m_payment_id={{paymentId}}&signature={{signature}} diff --git a/LiteCharms.Features/Abstractions/EventBase.cs b/LiteCharms.Features/Abstractions/EventBase.cs new file mode 100644 index 0000000..f64d71e --- /dev/null +++ b/LiteCharms.Features/Abstractions/EventBase.cs @@ -0,0 +1,13 @@ +using LiteCharms.Features.Extensions; +using static LiteCharms.Features.Extensions.Timezones; + +namespace LiteCharms.Features.Abstractions; + +public abstract class EventBase +{ + public Guid Id { get; set; } = Guid.CreateVersion7(); + + public DateTimeOffset EnqueueAt { get; set; } = (DateTimeOffset)SouthAfricanTimeZone.UtcNow(); + + public string CorrelationId { get; set; } = Guid.CreateVersion7().ToString(); +} diff --git a/LiteCharms.Features/Abstractions/IEndpoint.cs b/LiteCharms.Features/Abstractions/IEndpoint.cs new file mode 100644 index 0000000..25bb977 --- /dev/null +++ b/LiteCharms.Features/Abstractions/IEndpoint.cs @@ -0,0 +1,6 @@ +namespace LiteCharms.Features.Abstractions; + +public interface IEndpoint +{ + void Map(IEndpointRouteBuilder builder); +} diff --git a/LiteCharms.Abstractions/IEvent.cs b/LiteCharms.Features/Abstractions/IEvent.cs similarity index 79% rename from LiteCharms.Abstractions/IEvent.cs rename to LiteCharms.Features/Abstractions/IEvent.cs index 08bc091..366ad86 100644 --- a/LiteCharms.Abstractions/IEvent.cs +++ b/LiteCharms.Features/Abstractions/IEvent.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Abstractions; +namespace LiteCharms.Features.Abstractions; public interface IEvent : INotification { diff --git a/LiteCharms.Features/Abstractions/IFeatures.cs b/LiteCharms.Features/Abstractions/IFeatures.cs new file mode 100644 index 0000000..48a49c5 --- /dev/null +++ b/LiteCharms.Features/Abstractions/IFeatures.cs @@ -0,0 +1,3 @@ +namespace LiteCharms.Features.Abstractions; + +public interface IFeatures; diff --git a/LiteCharms.Features/Abstractions/IJobOrchestrator.cs b/LiteCharms.Features/Abstractions/IJobOrchestrator.cs new file mode 100644 index 0000000..c0afd08 --- /dev/null +++ b/LiteCharms.Features/Abstractions/IJobOrchestrator.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.Abstractions; + +public interface IJobOrchestrator +{ + ValueTask SendAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : IEvent; + + ValueTask ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) + where TNotification : IEvent; + + ValueTask InterruptAsync(string eventName, string? correlationId = null, CancellationToken cancellationToken = default); +} diff --git a/LiteCharms.Features/Abstractions/IService.cs b/LiteCharms.Features/Abstractions/IService.cs new file mode 100644 index 0000000..17ec5e0 --- /dev/null +++ b/LiteCharms.Features/Abstractions/IService.cs @@ -0,0 +1,3 @@ +namespace LiteCharms.Features.Abstractions; + +public interface IService; diff --git a/LiteCharms.Features/Api/ApiVersionTargetAttribute.cs b/LiteCharms.Features/Api/ApiVersionTargetAttribute.cs new file mode 100644 index 0000000..63d1598 --- /dev/null +++ b/LiteCharms.Features/Api/ApiVersionTargetAttribute.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.Api; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class ApiVersionTargetAttribute(int majorVersion) : Attribute +{ + public int MajorVersion { get; } = majorVersion; +} diff --git a/LiteCharms.Features/Api/Configuration/LiteCharmsClientSettings.cs b/LiteCharms.Features/Api/Configuration/LiteCharmsClientSettings.cs new file mode 100644 index 0000000..aff2931 --- /dev/null +++ b/LiteCharms.Features/Api/Configuration/LiteCharmsClientSettings.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.Api.Configuration; + +public sealed class LiteCharmsClientSettings +{ + public string? Authority { get; set; } + + public string? GrantType { get; set; } + + public string? ClientId { get; set; } + + public string? ClientSecret { get; set; } + + public string? Scope { get; set; } +} diff --git a/LiteCharms.Features/Api/Configuration/LiteCharmsSettings.cs b/LiteCharms.Features/Api/Configuration/LiteCharmsSettings.cs new file mode 100644 index 0000000..0bc1ecc --- /dev/null +++ b/LiteCharms.Features/Api/Configuration/LiteCharmsSettings.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.Api.Configuration; + +public sealed class LiteCharmsSettings +{ + public string? Authority { get; set; } + + public string? ClientId { get; set; } + + public string? ClientSecret { get; set; } + + public string? Audience { get; set; } +} diff --git a/LiteCharms.Features/Api/Configuration/PayfastSettings.cs b/LiteCharms.Features/Api/Configuration/PayfastSettings.cs new file mode 100644 index 0000000..0a9d5c1 --- /dev/null +++ b/LiteCharms.Features/Api/Configuration/PayfastSettings.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.Api.Configuration; + +public sealed class PayfastSettings +{ + public string? CheckoutUrl { get; set; } + + public string? Passphrase { get; set; } + + public string? MerchantId { get; set; } + + public string? MerchantKey { get; set; } + + public string[]? ValidHosts { get; set; } +} diff --git a/LiteCharms.Features/Api/Models/TokenErrorResponse.cs b/LiteCharms.Features/Api/Models/TokenErrorResponse.cs new file mode 100644 index 0000000..0d31195 --- /dev/null +++ b/LiteCharms.Features/Api/Models/TokenErrorResponse.cs @@ -0,0 +1,13 @@ +namespace LiteCharms.Features.Api.Models; + +public sealed class TokenErrorResponse +{ + [JsonPropertyName("error")] + public string? Error { get; set; } + + [JsonPropertyName("error_description")] + public string? ErrorDescription { get; set; } + + [JsonPropertyName("error_uri")] + public string? ErrorUri { get; set; } +} diff --git a/LiteCharms.Features/Api/Models/TokenRequest.cs b/LiteCharms.Features/Api/Models/TokenRequest.cs new file mode 100644 index 0000000..68b1abc --- /dev/null +++ b/LiteCharms.Features/Api/Models/TokenRequest.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.Api.Models; + +public sealed class TokenRequest +{ + [JsonPropertyName("grant_type")] + [AliasAs("grant_type")] + public string? GrantType { get; set; } + + [JsonPropertyName("client_id")] + [AliasAs("client_id")] + public string? ClientId { get; set; } + + [JsonPropertyName("client_secret")] + [AliasAs("client_secret")] + public string? ClientSecret { get; set; } + + [JsonPropertyName("scope")] + [AliasAs("scope")] + public string? Scope { get; set; } +} diff --git a/LiteCharms.Features/Api/Models/TokenResponse.cs b/LiteCharms.Features/Api/Models/TokenResponse.cs new file mode 100644 index 0000000..3fb090a --- /dev/null +++ b/LiteCharms.Features/Api/Models/TokenResponse.cs @@ -0,0 +1,17 @@ +namespace LiteCharms.Features.Api.Models; + +public sealed class TokenResponse +{ + [JsonPropertyName("access_token")] + public string? AccessToken { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + [JsonPropertyName("token_type")] + public string? TokenType { get; set; } + + [JsonPropertyName("scope")] + public string? Scope { get; set; } +} + diff --git a/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs b/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs new file mode 100644 index 0000000..4f1f231 --- /dev/null +++ b/LiteCharms.Features/Api/OpenApiBearerSecuritySchemeTransformer.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Features.Api; + +public sealed class OpenApiBearerSecuritySchemeTransformer : IOpenApiDocumentTransformer +{ + public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) + { + var bearerScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http, + Scheme = "bearer", + Description = "JWT Authorization header using the Bearer scheme", + }; + + document.AddComponent("Bearer", bearerScheme); + } +} diff --git a/LiteCharms.Features/Api/Sdk/IConnectApi.cs b/LiteCharms.Features/Api/Sdk/IConnectApi.cs new file mode 100644 index 0000000..b7d3430 --- /dev/null +++ b/LiteCharms.Features/Api/Sdk/IConnectApi.cs @@ -0,0 +1,10 @@ +using LiteCharms.Features.Api.Models; + +namespace LiteCharms.Features.Api.Sdk; + +public interface IConnectApi +{ + [Post("/connect/token")] + ValueTask GetToken([Body(BodySerializationMethod.UrlEncoded)] TokenRequest request, + CancellationToken cancellationToken = default); +} diff --git a/LiteCharms.Features/Api/TokenService.cs b/LiteCharms.Features/Api/TokenService.cs new file mode 100644 index 0000000..a1872c5 --- /dev/null +++ b/LiteCharms.Features/Api/TokenService.cs @@ -0,0 +1,66 @@ +using LiteCharms.Features.Api.Configuration; +using LiteCharms.Features.Api.Models; +using LiteCharms.Features.Api.Sdk; + +namespace LiteCharms.Features.Api; + +public sealed class TokenService(IConnectApi connectApi, IOptions clientOptions) +{ + private readonly LiteCharmsClientSettings clientSettings = clientOptions.Value; + + public async Task> GenerateAsync(CancellationToken cancellationToken = default) + { + try + { + var request = new TokenRequest + { + ClientId = clientSettings.ClientId, + ClientSecret = clientSettings.ClientSecret, + GrantType = clientSettings.GrantType, + Scope = clientSettings.Scope, + }; + + using var response = await connectApi.GetToken(request, cancellationToken); + + var contentRaw = await response.Content.ReadAsStringAsync(cancellationToken); + + if (string.IsNullOrWhiteSpace(contentRaw)) + return Result.Fail(new Error($"The authentication endpoint returned an empty payload. Status code: {response.StatusCode}")); + + if (response.IsSuccessStatusCode) + { + var tokenResponse = JsonSerializer.Deserialize(contentRaw); + + return !string.IsNullOrWhiteSpace(tokenResponse?.AccessToken) + ? Result.Ok(tokenResponse) + : Result.Fail(new Error("Authentication succeeded, but no access token was found in the response payload.")); + } + + try + { + var errorResult = JsonSerializer.Deserialize(contentRaw); + + if (errorResult != null) + { + string summary = $"{errorResult.Error}: {errorResult.ErrorDescription}"; + + return Result.Fail(new Error(summary)); + } + } + catch + { + return Result.Fail(new Error($"Authentication failed: {contentRaw}")); + } + + return Result.Fail(new Error($"Authentication failed with status code: {response.StatusCode}")); + } + catch (OperationCanceledException ex) + { + return Result.Fail(new Error("The token generation request was canceled.").CausedBy(ex)); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} \ No newline at end of file diff --git a/LiteCharms.Features/Browser/LocalStorageService.cs b/LiteCharms.Features/Browser/LocalStorageService.cs new file mode 100644 index 0000000..9abf93b --- /dev/null +++ b/LiteCharms.Features/Browser/LocalStorageService.cs @@ -0,0 +1,78 @@ +namespace LiteCharms.Features.Browser; + +public sealed class LocalStorageService(ProtectedLocalStorage storage) +{ + public async ValueTask DeleteAsync(string key) + { + try + { + await storage.DeleteAsync(key); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask SaveAsync(string key, string value) + { + try + { + await storage.SetAsync(key, value); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask SaveAsync(string key, TValue value) where TValue : class + { + try + { + await storage.SetAsync(key, value); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAsync(string key) + { + try + { + var retrieval = await storage.GetAsync(key); + + return retrieval.Success && !string.IsNullOrWhiteSpace(retrieval.Value) + ? Result.Ok(retrieval.Value) + : Result.Fail($"Could not find object by key {key}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetAsync(string key) where TValue : class + { + try + { + var retrieval = await storage.GetAsync(key); + + return retrieval.Success && retrieval.Value is not null + ? Result.Ok(retrieval.Value) + : Result.Fail($"Could not find object by key {key}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/Customers/Commands/CreateCustomerCommand.cs b/LiteCharms.Features/Customers/Commands/CreateCustomerCommand.cs deleted file mode 100644 index f0b3560..0000000 --- a/LiteCharms.Features/Customers/Commands/CreateCustomerCommand.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace LiteCharms.Features.Customers.Commands; - -public class CreateCustomerCommand : IRequest> -{ - public string? Company { get; set; } - - public string Name { get; set; } - - public string LastName { get; set; } - - public string? Tax { get; set; } - - public string Email { get; set; } - - public string? Discord { get; set; } - - public string? Slack { get; set; } - - public string? LinkedIn { get; set; } - - public string? Whatsapp { get; set; } - - public string? Website { get; set; } - - public string? Phone { get; set; } - - public string? Address { get; set; } - - public string? City { get; set; } - - public string? Region { get; set; } - - public string? Country { get; set; } - - public string? PostalCode { get; set; } - - private CreateCustomerCommand(string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode) - { - Name = name; - LastName = lastName; - Company = company; - Tax = tax; - Email = email; - Discord = discord; - Slack = slack; - LinkedIn = linkedIn; - Whatsapp = whatsapp; - Website = website; - Phone = phone; - Address = address; - City = city; - Region = region; - Country = country; - PostalCode = postalCode; - } - - public static CreateCustomerCommand Create(string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode) - { - if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(lastName) && string.IsNullOrWhiteSpace(email)) - throw new ArgumentException("At the following fields must be provided: Name, LastName, Email"); - - return new(name, lastName, company, tax, email, discord, slack, linkedIn, whatsapp, website, phone, address, city, region, country, postalCode); - } -} diff --git a/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs b/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs deleted file mode 100644 index d49e7c1..0000000 --- a/LiteCharms.Features/Customers/Commands/Handlers/CreateCustomerCommandHandler.cs +++ /dev/null @@ -1,48 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Customers.Commands.Handlers; - -public class CreateCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(CreateCustomerCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var customerEmail = request.Email.ToLower().Trim(); - - if (await context.Customers.AnyAsync(c => c.Email == customerEmail, cancellationToken)) - return Result.Fail(new Error($"A customer with the email {customerEmail} already exists")); - - var newCustomer = context.Customers.Add(new Entities.Customer - { - Company = request.Company, - Name = request.Name, - LastName = request.LastName, - Tax = request.Tax, - Email = customerEmail, - Discord = request.Discord, - Slack = request.Slack, - LinkedIn = request.LinkedIn, - Whatsapp = request.Whatsapp, - Website = request.Website, - Phone = request.Phone, - Address = request.Address, - City = request.City, - Region = request.Region, - Country = request.Country, - PostalCode = request.PostalCode, - Active = true, - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(newCustomer.Entity.Id) - : Result.Fail(new Error($"Failed to create customer {customerEmail}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs b/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs deleted file mode 100644 index e16469a..0000000 --- a/LiteCharms.Features/Customers/Commands/Handlers/UpdateCustomerCommandHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Customers.Commands.Handlers; - -public class UpdateCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateCustomerCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == request.CustomerId, cancellationToken); - - if (customer is null) - return Result.Fail(new Error($"Customer with ID {request.CustomerId} not found.")); - - customer.Name = request.Name; - customer.LastName = request.LastName; - customer.Email = request.Email; - customer.Company = request.Company; - customer.Address = request.Address; - customer.City = request.City; - customer.Region = request.Region; - customer.Country = request.Country; - customer.PostalCode = request.PostalCode; - customer.Phone = request.Phone; - customer.Tax = request.Tax; - customer.City = request.City; - customer.Discord = request.Discord; - customer.Slack = request.Slack; - customer.LinkedIn = request.LinkedIn; - customer.Whatsapp = request.Whatsapp; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error($"Failed to update the customer {request.CustomerId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} \ No newline at end of file diff --git a/LiteCharms.Features/Customers/Commands/UpdateCustomerCommand.cs b/LiteCharms.Features/Customers/Commands/UpdateCustomerCommand.cs deleted file mode 100644 index ac4f0cd..0000000 --- a/LiteCharms.Features/Customers/Commands/UpdateCustomerCommand.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace LiteCharms.Features.Customers.Commands; - -public class UpdateCustomerCommand : IRequest -{ - public Guid CustomerId { get; set; } - - public string? Company { get; set; } - - public string? Name { get; set; } - - public string? LastName { get; set; } - - public string? Tax { get; set; } - - public string? Email { get; set; } - - public string? Discord { get; set; } - - public string? Slack { get; set; } - - public string? LinkedIn { get; set; } - - public string? Whatsapp { get; set; } - - public string? Website { get; set; } - - public string? Phone { get; set; } - - public string? Address { get; set; } - - public string? City { get; set; } - - public string? Region { get; set; } - - public string? Country { get; set; } - - public string? PostalCode { get; set; } - - private UpdateCustomerCommand(Guid customerId, string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode) - { - CustomerId = customerId; - Name = name; - LastName = lastName; - Company = company; - Tax = tax; - Email = email; - Discord = discord; - Slack = slack; - LinkedIn = linkedIn; - Whatsapp = whatsapp; - Website = website; - Phone = phone; - Address = address; - City = city; - Region = region; - Country = country; - PostalCode = postalCode; - } - - public static UpdateCustomerCommand Create(Guid customerId, string name, string lastName, string? company, string? tax, string email, string? discord, string? slack, string? linkedIn, string? whatsapp, string? website, string? phone, string? address, string? city, string? region, string? country, string? postalCode) - { - if (customerId == Guid.Empty) - throw new ArgumentException("Customer ID is required.", nameof(customerId)); - - if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(lastName) && string.IsNullOrWhiteSpace(email)) - throw new ArgumentException("At the following fields must be provided: Name, LastName, Email"); - - return new(customerId, name, lastName, company, tax, email, discord, slack, linkedIn, whatsapp, website, phone, address, city, region, country, postalCode); - } -} diff --git a/LiteCharms.Features/Customers/Queries/GetCustomerQuery.cs b/LiteCharms.Features/Customers/Queries/GetCustomerQuery.cs deleted file mode 100644 index 5ea7394..0000000 --- a/LiteCharms.Features/Customers/Queries/GetCustomerQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Customers.Queries; - -public class GetCustomerQuery : IRequest> -{ - public Guid CustomerId { get; set; } - - private GetCustomerQuery(Guid customerId) => CustomerId = customerId; - - public static GetCustomerQuery Create(Guid customerId) - { - if(customerId == Guid.Empty) - throw new ArgumentException("Customer ID is required.", nameof(customerId)); - - return new(customerId); - } -} diff --git a/LiteCharms.Features/Customers/Queries/GetCustomersQuery.cs b/LiteCharms.Features/Customers/Queries/GetCustomersQuery.cs deleted file mode 100644 index 3f271b3..0000000 --- a/LiteCharms.Features/Customers/Queries/GetCustomersQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Customers.Queries; - -public class GetCustomersQuery : IRequest> -{ - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - public int MaxRecords { get; set; } - - private GetCustomersQuery(DateOnly from, DateOnly to, int maxRecords = 1000) - { - From = from; - To = to; - MaxRecords = maxRecords; - } - - public static GetCustomersQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000) - { - if (from > to) - throw new ArgumentException("From date cannot be greater than To date."); - - if(maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer."); - - return new(from, to, maxRecords); - } -} diff --git a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs b/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs deleted file mode 100644 index 09f1f9b..0000000 --- a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomerQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Customers.Queries.Handlers; - -public class GetCustomerQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == request.CustomerId, cancellationToken); - - return customer is not null - ? Result.Ok(customer.ToModel()) - : Result.Fail($"Customer not found with id {request.CustomerId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs b/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs deleted file mode 100644 index e3e3d73..0000000 --- a/LiteCharms.Features/Customers/Queries/Handlers/GetCustomersQueryHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Customers.Queries.Handlers; - -public class GetCustomersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomersQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var customers = await context.Customers.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(c => c.CreatedAt >= fromDate && c.CreatedAt <= toDate) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return customers?.Length > 0 - ? Result.Ok(customers.Select(c => c.ToModel()).ToArray()) - : Result.Fail(new Error("No customers found in the specified date range.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Models/Configuraton/Email/Account.cs b/LiteCharms.Features/Email/Configuration/Account.cs similarity index 52% rename from LiteCharms.Models/Configuraton/Email/Account.cs rename to LiteCharms.Features/Email/Configuration/Account.cs index 96424ce..02694d7 100644 --- a/LiteCharms.Models/Configuraton/Email/Account.cs +++ b/LiteCharms.Features/Email/Configuration/Account.cs @@ -1,6 +1,6 @@ -namespace LiteCharms.Models.Configuraton.Email; +namespace LiteCharms.Features.Email.Configuration; -public class Account +public sealed class Account { public string? Username { get; set; } diff --git a/LiteCharms.Models/Configuraton/Email/SmtpSettings.cs b/LiteCharms.Features/Email/Configuration/SmtpSettings.cs similarity index 65% rename from LiteCharms.Models/Configuraton/Email/SmtpSettings.cs rename to LiteCharms.Features/Email/Configuration/SmtpSettings.cs index c44fbbe..27ab574 100644 --- a/LiteCharms.Models/Configuraton/Email/SmtpSettings.cs +++ b/LiteCharms.Features/Email/Configuration/SmtpSettings.cs @@ -1,6 +1,6 @@ -namespace LiteCharms.Models.Configuraton.Email; +namespace LiteCharms.Features.Email.Configuration; -public class SmtpSettings +public sealed class SmtpSettings { public Account? Credentials { get; set; } diff --git a/LiteCharms.Features/Email/EmailService.cs b/LiteCharms.Features/Email/EmailService.cs new file mode 100644 index 0000000..00d5f63 --- /dev/null +++ b/LiteCharms.Features/Email/EmailService.cs @@ -0,0 +1,209 @@ +using LiteCharms.Features.Email.Configuration; +using LiteCharms.Features.Email.Extensions; +using LiteCharms.Features.Email.Models; + +namespace LiteCharms.Features.Email; + +public sealed class EmailService(IOptions options) : IDisposable +{ + private readonly SmtpSettings settings = options.Value; + private readonly SmtpClient client = new(); + private readonly int sendMaxCount = 10; + private int sendCount = 0; + + public EmailStatuses Status { get; private set; } = EmailStatuses.Disconnected; + + public async ValueTask> SendEmailAsync(Message message, CancellationToken cancellationToken = default) + { + using var activity = EmailTelemetry.Source.StartActivity("Email Send"); + + activity?.SetTag("email.recipient", message.Recipient?.Address); + + try + { + if (Status != EmailStatuses.Connected) + { + activity?.SetStatus(ActivityStatusCode.Error, "Disconnected"); + + return Result.Fail("Smtp service is disconnected."); + } + + var email = ConstructEmail(message, cancellationToken); + + var response = await client.SendAsync(email, cancellationToken); + + bool emailSent = response.Contains("OK", StringComparison.InvariantCultureIgnoreCase); + + message.Dispose(); + + Interlocked.Increment(ref sendCount); + + if (sendCount % sendMaxCount == 0) + { + using var delayActivity = EmailTelemetry.Source.StartActivity("Rate Limit Pause"); + + sendCount = 0; + + await Task.Delay(1000, cancellationToken); + } + + if (emailSent) + { + EmailTelemetry.EmailsSent.Add(1, new TagList { { "host", settings.Host } }); + + return Result.Ok(Response.Create(EmailStatuses.Success)); + } + + await DisconnectAsync(cancellationToken); + + var failCheckResult = HandleNegativeResponse(response); + + if (failCheckResult.IsFailed) return failCheckResult; + + Status = EmailStatuses.Disconnected; + + return Result.Fail("General error, disconnected"); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + private static MimeMessage ConstructEmail(Message message, CancellationToken cancellationToken) + { + var email = new MimeMessage(); + email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!)); + email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!)); + email.Subject = message.Subject!; + + var bodyBuilder = new BodyBuilder(); + + if (message.Body!.Properties.HasAttachments) + foreach (var attachment in message.Body?.Attachments!) + bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken); + + if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message; + if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message; + + email.Body = bodyBuilder.ToMessageBody(); + + return email; + } + + private Result HandleNegativeResponse(string response) + { + if (response.Contains("421", StringComparison.Ordinal)) + { + Status = EmailStatuses.TooManyConnections; + + return Result.Fail(response); + } + + if (response.Contains("451", StringComparison.Ordinal)) + { + Status = EmailStatuses.ConnectionAborted; + + return Result.Fail(response); + } + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } }); + + return Result.Fail(response); + } + + public async ValueTask> ConnectAsync(CancellationToken cancellationToken = default) + { + using var activity = EmailTelemetry.Source.StartActivity("Email Connect"); + activity?.SetTag("email.smtp.connect", settings.Host); + + try + { + if (Status is EmailStatuses.Connected) return Result.Ok(Response.Create(Status)); + + await client.ConnectAsync(settings.Host!, settings.Port, settings.UseSsl, cancellationToken); + await client.AuthenticateAsync(settings.Credentials!.Username!, settings.Credentials.Password!, cancellationToken); + + Status = EmailStatuses.Connected; + + activity?.SetStatus(ActivityStatusCode.Ok, "Connected"); + + return Result.Ok(Response.Create(Status)); + } + catch (MailKit.ProtocolException ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.ProtocolError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + catch (Exception ex) when (ex is MailKit.Security.SslHandshakeException || ex is MailKit.Security.AuthenticationException) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.AuthenticationError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.GeneralError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DisconnectAsync(CancellationToken cancellationToken = default) + { + using var activity = EmailTelemetry.Source.StartActivity("Email Disconnect"); + activity?.SetTag("email.smtp.disconnect", settings.Host); + + try + { + if (Status is EmailStatuses.Disconnected) return Result.Ok(); + + await client.DisconnectAsync(true, cancellationToken); + + activity?.SetStatus(ActivityStatusCode.Ok, "Disconnected"); + + Status = EmailStatuses.Disconnected; + + return Result.Ok(); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.AddException(ex); + + EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); + + Status = EmailStatuses.GeneralError; + + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public void Dispose() + { + client.Dispose(); + + GC.SuppressFinalize(this); + } +} diff --git a/LiteCharms.Features/Email/Extensions/EmailTelemetry.cs b/LiteCharms.Features/Email/Extensions/EmailTelemetry.cs new file mode 100644 index 0000000..48a9a4a --- /dev/null +++ b/LiteCharms.Features/Email/Extensions/EmailTelemetry.cs @@ -0,0 +1,9 @@ +namespace LiteCharms.Features.Email.Extensions; + +public static class EmailTelemetry +{ + public static readonly ActivitySource Source = new("LiteCharms.EmailService"); + public static readonly Meter Meter = new("LiteCharms.EmailService"); + public static readonly Counter EmailsSent = Meter.CreateCounter("emails_sent_total", "count", "Total successful emails sent"); + public static readonly Counter EmailsFailed = Meter.CreateCounter("emails_failed_total", "count", "Total failed email attempts"); +} diff --git a/LiteCharms.Features/Email/Models/Attachment.cs b/LiteCharms.Features/Email/Models/Attachment.cs new file mode 100644 index 0000000..09dbaf8 --- /dev/null +++ b/LiteCharms.Features/Email/Models/Attachment.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.Email.Models; + +public sealed class Attachment +{ + public string? Name { get; set; } + + public Stream? FileStream { get; set; } +} diff --git a/LiteCharms.Features/Email/Models/Body.cs b/LiteCharms.Features/Email/Models/Body.cs new file mode 100644 index 0000000..42de5e3 --- /dev/null +++ b/LiteCharms.Features/Email/Models/Body.cs @@ -0,0 +1,26 @@ +namespace LiteCharms.Features.Email.Models; + +public sealed class Body : IDisposable +{ + public string? Message { get; set; } + + public ReadOnlyCollection? Attachments { get; set; } + + public BodyProperties Properties { get; set; } = new(); + + public void Dispose() + { + if (Attachments is null) return; + + foreach (var attachment in Attachments!) + { + if (attachment is not null) + { + attachment.FileStream!.Close(); + attachment.FileStream!.Dispose(); + } + } + + GC.SuppressFinalize(this); + } +} diff --git a/LiteCharms.Features/Email/Models/BodyProperties.cs b/LiteCharms.Features/Email/Models/BodyProperties.cs new file mode 100644 index 0000000..f3564c3 --- /dev/null +++ b/LiteCharms.Features/Email/Models/BodyProperties.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.Email.Models; + +public sealed class BodyProperties +{ + public bool IsHtml { get; set; } + + public bool HasAttachments { get; set; } +} diff --git a/LiteCharms.Models/EmailEnquiry.cs b/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs similarity index 50% rename from LiteCharms.Models/EmailEnquiry.cs rename to LiteCharms.Features/Email/Models/EmailEnquiryModel.cs index a858328..499f9c1 100644 --- a/LiteCharms.Models/EmailEnquiry.cs +++ b/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs @@ -1,29 +1,31 @@ -namespace LiteCharms.Models; +using System.ComponentModel.DataAnnotations; -public sealed class EmailEnquiry +namespace LiteCharms.Features.Email.Models; + +public sealed class EmailEnquiryModel { [Required] [MinLength(2)] [MaxLength(255)] - [Display(Name = "Full Name")] + [System.ComponentModel.DataAnnotations.Display(Name = "Full Name")] public string? FullName { get; set; } [Required] [EmailAddress] [MinLength(5)] [MaxLength(255)] - [Display(Name = "Email Address")] + [System.ComponentModel.DataAnnotations.Display(Name = "Email Address")] public string? EmailAddress { get; set; } [Required] [MinLength(2)] [MaxLength(255)] - [Display(Name = "Subject")] + [System.ComponentModel.DataAnnotations.Display(Name = "Subject")] public string? EmailSubject { get; set; } [Required] [MinLength(2)] [MaxLength(2000)] - [Display(Name = "Message")] + [System.ComponentModel.DataAnnotations.Display(Name = "Message")] public string? Message { get; set; } } diff --git a/LiteCharms.Features/Email/Models/Message.cs b/LiteCharms.Features/Email/Models/Message.cs new file mode 100644 index 0000000..44d7f3f --- /dev/null +++ b/LiteCharms.Features/Email/Models/Message.cs @@ -0,0 +1,19 @@ +namespace LiteCharms.Features.Email.Models; + +public sealed class Message : IDisposable +{ + public Party? Sender { get; set; } + + public Party? Recipient { get; set; } + + public string? Subject { get; set; } + + public Body? Body { get; set; } + + public void Dispose() + { + Body?.Dispose(); + + GC.SuppressFinalize(this); + } +} diff --git a/LiteCharms.Features/Email/Models/Party.cs b/LiteCharms.Features/Email/Models/Party.cs new file mode 100644 index 0000000..6aab9e3 --- /dev/null +++ b/LiteCharms.Features/Email/Models/Party.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.Email.Models; + +public sealed class Party +{ + public string? Name { get; set; } + + public string? Address { get; set; } +} diff --git a/LiteCharms.Features/Email/Models/Response.cs b/LiteCharms.Features/Email/Models/Response.cs new file mode 100644 index 0000000..5557095 --- /dev/null +++ b/LiteCharms.Features/Email/Models/Response.cs @@ -0,0 +1,20 @@ +namespace LiteCharms.Features.Email.Models; + +public sealed class Response +{ + public int Code { get; set; } + + public string? Error { get; set; } + + public EmailStatuses Status { get; set; } + + private Response(EmailStatuses status, int code = 0, string? error = null) + { + Status = status; + Code = code; + Error = error; + } + + public static Response Create(EmailStatuses status, int code = 0, string? error = null) => + new(status, code, error); +} diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs new file mode 100644 index 0000000..296848e --- /dev/null +++ b/LiteCharms.Features/Enums.cs @@ -0,0 +1,209 @@ +namespace LiteCharms.Features; + +public enum InventoryStatuses : int +{ + Adjustment = 0, + Reserved = 1, + Released = 2, + Sold = 3, + Replenished = 4, + Correction = 5, +} + +public enum LedgerStatuses : int +{ + Changed = 0, + Sent = 1, + Received = 2, + Refunded = 3, + Cancelled = 4, + Failed = 5, + Partial = 6, + Completed = 7, +} + +public enum PaymentStatuses : int +{ + NotPaid = 0, + Paid = 1, + Cancelled = 2, + Requested = 3, + Failed = 4, +} + +public enum ShippingProviderTypes : int +{ + Dsv = 0, + Pargo = 1, + Ram = 2, + TheCourierGuy = 3, + Paxi = 4, + FastWay = 5, + MdsCollivery = 6, + PostNet = 7, + Aramex = 8, + DHL = 9, + FedEx = 10, + UPS = 11, + USPS = 12, + AmazonLogistics = 13, + LocalCourier = 14, + Other = 15 +} + +public enum ShippingStatuses : int +{ + Pending = 0, + Shipped = 1, + Delivered = 2, + Returned = 3, + Cancelled = 4, +} + +public enum RefundTypes : int +{ + Full = 0, + Partial = 1, + StoreCredit = 2, + Exchange = 3, + Other = 4 +} + +public enum RefundStatus : int +{ + Pending = 0, + Approved = 1, + Rejected = 2, + Completed = 3, + Failed = 4, +} + +public enum OrderStatus : int +{ + Pending = 0, + Completed = 1, + Cancelled = 2, + Failed = 3, + Refunded = 4, + Error = 5, + OnHold = 6, +} + +public enum ContactTypes : int +{ + Personal = 0, + Business = 1, + Other = 2 +} + +public enum AddressType +{ + Billing = 1, + Shipping = 2, + Other = 3 +} + +public enum AddressBuildingTypes : int +{ + Residential = 0, + Commercial = 1, + Industrial = 2, + MixedUse = 3, + Agricultural = 4, + Institutional = 5, + Recreational = 6, +} + +public enum SocialMediaTypes : int +{ + Twitter = 0, + Facebook = 1, + Instagram = 2, + LinkedIn = 3, + TikTok = 4, + YouTube = 5, + Pinterest = 6, + Reddit = 7, + Tumblr = 8, + GitHub = 9 +} + +public enum EmailStatuses : int +{ + GeneralError = 0, + AuthenticationError = 1, + ProtocolError = 2, + Connected = 3, + Disconnected = 4, + TooManyConnections = 5, + ConnectionAborted = 6, + Success = 7 +} + +public enum Priorities : int +{ + Low = 0, + Medium = 1, + High = 2, +} + +public enum PublisherTypes : int +{ + Individual = 0, + Company = 1, + Organization = 2, + SelfPublished = 3, + UniversityPress = 4, + GovernmentAgency = 5, + NonProfit = 6, + Independent = 7 +} + +public enum BookTypes : int +{ + Fiction = 0, + NonFiction = 1, + Academic = 2, + SelfHelp = 3, + Biography = 4, + Poetry = 5, + Children = 6, + YoungAdult = 7, + ScienceFiction = 8, + Fantasy = 9 +} + +public enum BookContentTypes : int +{ + Text = 0, + Image = 1, + Video = 2, + Audio = 3, + Interactive = 4, + Markdown = 5, + Html = 6, + Json = 7, + Yaml = 8 +} + +public enum BookPageTypes : int +{ + Cover = 0, + Preface = 1, + Introduction = 2, + Content = 3, + Closing = 4, + Referencer = 5, + Credits = 6, + BackCover = 7 +} + +public enum ProductTypes : int +{ + Book = 1, + Journal = 2, + Magazine = 3, + EBook = 4, + Audiobook = 5, + Accessory = 6 +} \ No newline at end of file diff --git a/LiteCharms.Features/Extensions/Api.cs b/LiteCharms.Features/Extensions/Api.cs new file mode 100644 index 0000000..ae57c25 --- /dev/null +++ b/LiteCharms.Features/Extensions/Api.cs @@ -0,0 +1,249 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Api; +using LiteCharms.Features.Api.Configuration; +using LiteCharms.Features.Api.Sdk; + +namespace LiteCharms.Features.Extensions; + +public static class Api +{ + public const string Books = nameof(Books); + public const string Payments = nameof(Payments); + + public static IServiceCollection AddPayfastServices(this IServiceCollection services, IConfiguration configuration) + { + var configSection = configuration.GetSection(nameof(PayfastSettings)); + + services.Configure(configSection); + + return services; + } + + public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration) + { + var configSection = configuration.GetSection(nameof(LiteCharmsClientSettings)); + + var authOptions = new LiteCharmsClientSettings(); + configSection.Bind(authOptions); + + services.Configure(configSection); + + if (string.IsNullOrWhiteSpace(authOptions.Authority)) + return services; + + if (!authOptions.Authority.EndsWith("/", StringComparison.Ordinal)) authOptions.Authority += "/"; + + services.AddRefitClient() + .ConfigureHttpClient(config => + { + config.BaseAddress = new Uri(authOptions.Authority); + config.Timeout = TimeSpan.FromSeconds(15); + }) + .AddStandardResilienceHandler(options => + { + options.Retry.MaxRetryAttempts = 3; + options.Retry.Delay = TimeSpan.FromSeconds(1); + options.Retry.BackoffType = Polly.DelayBackoffType.Exponential; + }); + + services.AddScoped(); + + return services; + } + + public static IServiceCollection AddLiteCharmsWebSecurity(this IServiceCollection services, IConfiguration configuration) + { + var configSection = configuration.GetSection(nameof(LiteCharmsSettings)); + + var authOptions = new LiteCharmsSettings(); + configSection.Bind(authOptions); + + services.Configure(configSection); + + services.AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) + .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => + { + options.Authority = authOptions.Authority; + + options.ClientId = authOptions.ClientId; + options.ClientSecret = authOptions.ClientSecret; + options.ResponseType = "code"; + + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + + options.Scope.Clear(); + options.Scope.Add("openid"); + options.Scope.Add("profile"); + options.Scope.Add("email"); + + options.Events = new OpenIdConnectEvents + { + OnRedirectToIdentityProviderForSignOut = context => + { + var idToken = context.ProtocolMessage.IdTokenHint; + + if (string.IsNullOrEmpty(idToken)) + { + var tokens = context.Properties.GetTokens(); + var idTokenItem = tokens.FirstOrDefault(t => string.Equals(t.Name, "id_token", StringComparison.Ordinal)); + + if (idTokenItem != null) context.ProtocolMessage.IdTokenHint = idTokenItem.Value; + } + + return Task.CompletedTask; + }, + }; + }); + + services.AddCascadingAuthenticationState(); + + return services; + } + + public static IServiceCollection AddLiteCharmsApiSecurity(this IServiceCollection services, IConfiguration configuration) + { + var configSection = configuration.GetSection(nameof(LiteCharmsSettings)); + + var authOptions = new LiteCharmsSettings(); + configSection.Bind(authOptions); + + services.Configure(configSection); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.Authority = authOptions.Authority; + options.Audience = authOptions.Audience; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = authOptions.Authority, + ValidateAudience = true, + ValidateIssuer = true, + }; + }); + + services.AddAuthorization(); + + return services; + } + + public static WebApplication AddSecurityEndpoints(this WebApplication app) + { + app.MapGet("/login", async (HttpContext context, string redirectUri = "/") => + { + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties + { + RedirectUri = redirectUri, + }); + }); + + app.MapGet("/logout", async (HttpContext context) => + { + var idToken = await context.GetTokenAsync("id_token"); + + var authProperties = new AuthenticationProperties { RedirectUri = "/", }; + + if (!string.IsNullOrEmpty(idToken)) + authProperties.Parameters.Add("id_token_hint", idToken); + + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties); + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + }); + + return app; + } + + public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddHttpClient(); + + services.AddApiVersioning(options => + { + options.DefaultApiVersion = new ApiVersion(1); + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), + new QueryStringApiVersionReader("version"), + new QueryStringApiVersionReader("version"), + new MediaTypeApiVersionReader("version")); + }) + .AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }); + + var urls = configuration["ASPNETCORE_URLS"] ?? configuration["Urls"]; + var healthUrl = "http://localhost:8080/health"; + + if (!string.IsNullOrWhiteSpace(urls)) + { + string firstUrl = urls.Split(';').FirstOrDefault(s => s.Contains("http://"))! + .Replace("0.0.0.0", "localhost") + .Replace("*", "localhost") + .Replace("+", "localhost"); + + healthUrl = $"{firstUrl.TrimEnd('/')}/health"; + } + + services.AddHealthChecksUI(setup => + { + setup.SetNotifyUnHealthyOneTimeUntilChange(); + setup.AddHealthCheckEndpoint("primary, heal", healthUrl); + setup.SetHeaderText("Midrand Books"); + }) + .AddInMemoryStorage(); + + services.AddOutputCache(options => + { + options.AddBasePolicy(builder => builder.Cache()); + options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); + }); + + services.AddOpenApi(options => options.AddDocumentTransformer()); + + return services; + } + + public static IApplicationBuilder MapEndpoints(this WebApplication app, IDictionary versionGroups) + { + var endpoints = app.Services.GetRequiredService>(); + + foreach (var endpoint in endpoints) + { + var versionAttributes = endpoint.GetType().GetCustomAttributes().ToList(); + + if (versionAttributes.Count != 0) + { + foreach (var attr in versionAttributes) + if (versionGroups.TryGetValue(attr.MajorVersion, out var targetGroup)) + endpoint.Map(targetGroup); + } + else + endpoint.Map(app); + } + + return app; + } + + public static IServiceCollection AddEndpoints(this IServiceCollection services, Assembly assembly) + { + ServiceDescriptor[] discriptors = [.. assembly.DefinedTypes + .Where(t => t is { IsInterface: false, IsAbstract: false }) + .Where(t => t.IsAssignableTo(typeof(IEndpoint))) + .Select(t => ServiceDescriptor.Transient(typeof(IEndpoint), t))]; + + services.TryAddEnumerable(discriptors); + + return services; + } + + public static string ToEndpointName(this Type target, string? annotation = "") => + $"{target.Name.Replace("Endpoint", string.Empty)}{annotation}".ToLower(CultureInfo.CurrentCulture); +} diff --git a/LiteCharms.Features/Extensions/Email.cs b/LiteCharms.Features/Extensions/Email.cs new file mode 100644 index 0000000..d52f04a --- /dev/null +++ b/LiteCharms.Features/Extensions/Email.cs @@ -0,0 +1,23 @@ +using LiteCharms.Features.Email; +using LiteCharms.Features.Email.Configuration; + +namespace LiteCharms.Features.Extensions; + +public static class Email +{ + public const string ShopEmailFromName = "Khongisa Shop"; + public const string ShopEmailFromAddress = "shop@litecharms.co.za"; + + public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("Email")); + + services.AddSingleton(); + + services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddSource("LiteCharms.EmailService")) + .WithMetrics(metrics => metrics.AddMeter("LiteCharms.EmailService")); + + return services; + } +} diff --git a/LiteCharms.Features/Extensions/Hash.cs b/LiteCharms.Features/Extensions/Hash.cs new file mode 100644 index 0000000..555c423 --- /dev/null +++ b/LiteCharms.Features/Extensions/Hash.cs @@ -0,0 +1,23 @@ +using LiteCharms.Features.Hasher; +using LiteCharms.Features.Hasher.Configuration; + +namespace LiteCharms.Features.Extensions; + +public static class Hash +{ + public const string HasherConfigSectionName = "HasherSettings"; + + public static IServiceCollection AddHashServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection(HasherConfigSectionName)); + + var settings = configuration.GetSection(HasherConfigSectionName).Get(); + + services.AddSingleton(_ => + new Hashids(settings!.Salt, minHashLength: settings.MinHashLength)); + + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/LiteCharms.Extensions/Monitoring.cs b/LiteCharms.Features/Extensions/Monitoring.cs similarity index 97% rename from LiteCharms.Extensions/Monitoring.cs rename to LiteCharms.Features/Extensions/Monitoring.cs index a5b52da..8e5db29 100644 --- a/LiteCharms.Extensions/Monitoring.cs +++ b/LiteCharms.Features/Extensions/Monitoring.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Extensions; +namespace LiteCharms.Features.Extensions; public static class Monitoring { diff --git a/LiteCharms.Features/Extensions/Postgres.cs b/LiteCharms.Features/Extensions/Postgres.cs new file mode 100644 index 0000000..8a9541a --- /dev/null +++ b/LiteCharms.Features/Extensions/Postgres.cs @@ -0,0 +1,6 @@ +namespace LiteCharms.Features.Extensions; + +public static class Postgres +{ + public const string SchedulerDbConfigName = "PostgresScheduler"; +} diff --git a/LiteCharms.Extensions/Quartz.cs b/LiteCharms.Features/Extensions/Quartz.cs similarity index 79% rename from LiteCharms.Extensions/Quartz.cs rename to LiteCharms.Features/Extensions/Quartz.cs index ea71879..341d8c8 100644 --- a/LiteCharms.Extensions/Quartz.cs +++ b/LiteCharms.Features/Extensions/Quartz.cs @@ -1,22 +1,24 @@ -using LiteCharms.Abstractions; -using LiteCharms.Infrastructure.Quartz; +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Quartz; +using static LiteCharms.Features.Extensions.Postgres; -namespace LiteCharms.Extensions; +namespace LiteCharms.Features.Extensions; public static class Quartz { - private const string databaseConfigName = "PostgresScheduler"; + public const string TechShopSchedulerName = "tech-shop"; + public const string MidrandShopSchedulerName = "midrand-shop"; - public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, string schedulerId, IConfiguration configuration) + public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration) { - var connectionString = configuration.GetConnectionString(databaseConfigName); + var connectionString = configuration.GetConnectionString(SchedulerDbConfigName); services.ConfigureCommon(); services.AddQuartz(config => { config.SchedulerName = schedulerName; - config.SchedulerId = schedulerId; + config.SchedulerId = "AUTO"; config.UseSimpleTypeLoader(); config.UseDefaultThreadPool(options => options.MaxConcurrency = 0); @@ -28,13 +30,13 @@ public static class Quartz storage.UseSystemTextJsonSerializer(); storage.SetProperty("quartz.jobStore.clustered", "true"); - storage.SetProperty("quartz.jobStore.tablePrefix", "quartz_"); + storage.SetProperty("quartz.jobStore.tablePrefix", "qrtz_"); storage.UsePostgres(connectionString!); storage.UseClustering(cluster => { cluster.CheckinInterval = TimeSpan.FromSeconds(30); - cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2); + cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90); }); }); }); @@ -42,16 +44,18 @@ public static class Quartz return services; } - public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, string schedulerId, IConfiguration configuration) + public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration) { - var connectionString = configuration.GetConnectionString(databaseConfigName); + var connectionString = configuration.GetConnectionString(SchedulerDbConfigName); services.ConfigureCommon(); + services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); + services.AddQuartz(config => { config.SchedulerName = schedulerName; - config.SchedulerId = schedulerId; + config.SchedulerId = "AUTO"; config.InterruptJobsOnShutdown = true; config.InterruptJobsOnShutdownWithWait = true; config.MaxBatchSize = 5; @@ -60,19 +64,21 @@ public static class Quartz config.UseDefaultThreadPool(options => options.MaxConcurrency = 1); config.UseTimeZoneConverter(); + config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture)); + config.UsePersistentStore(storage => { storage.PerformSchemaValidation = false; storage.UseSystemTextJsonSerializer(); storage.SetProperty("quartz.jobStore.clustered", "true"); - storage.SetProperty("quartz.jobStore.tablePrefix", "quartz_"); + storage.SetProperty("quartz.jobStore.tablePrefix", "qrtz_"); storage.UsePostgres(connectionString!); storage.UseClustering(cluster => { cluster.CheckinInterval = TimeSpan.FromSeconds(30); - cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2); + cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90); }); }); }); @@ -86,14 +92,14 @@ public static class Quartz { options.Scheduling.IgnoreDuplicates = true; options.Scheduling.OverWriteExistingData = true; + options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins"; options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins"; }); services.AddTransient(); services.AddTransient(); - services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); - + return services; } } diff --git a/LiteCharms.Features/Extensions/S3.cs b/LiteCharms.Features/Extensions/S3.cs new file mode 100644 index 0000000..2b2fd1e --- /dev/null +++ b/LiteCharms.Features/Extensions/S3.cs @@ -0,0 +1,64 @@ +using LiteCharms.Features.S3; +using LiteCharms.Features.S3.Abstractions; +using static LiteCharms.Features.S3.Constants; + +namespace LiteCharms.Features.Extensions; + +public static class S3 +{ + public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration) + { + if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value)) + { + services.AddKeyedSingleton(BookshopBucketName, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopS3SettingsSection}:AccessKey").Value, + configuration.GetSection($"{BookshopS3SettingsSection}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{BookshopS3SettingsSection}:Region").Value, + ForcePathStyle = true, + EndpointDiscoveryEnabled = true, + UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://") + })); + + services.AddKeyedScoped(BookshopBucketName); + } + + if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:ServiceUrl").Value)) + { + services.AddKeyedSingleton(BookshopInvoicesBucketName, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:AccessKey").Value, + configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:Region").Value, + ForcePathStyle = true, + EndpointDiscoveryEnabled = true, + UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://") + })); + + services.AddKeyedScoped(BookshopInvoicesBucketName); + } + + if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopQuotesS3SettingsSection}:ServiceUrl").Value)) + { + services.AddKeyedSingleton(BookshopQuotesBucketName, (provider, client) => + new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopQuotesS3SettingsSection}:AccessKey").Value, + configuration.GetSection($"{BookshopQuotesS3SettingsSection}:SecretKey").Value), + new AmazonS3Config + { + ServiceURL = configuration.GetSection($"{BookshopQuotesS3SettingsSection}:ServiceUrl").Value, + AuthenticationRegion = configuration.GetSection($"{BookshopQuotesS3SettingsSection}:Region").Value, + ForcePathStyle = true, + EndpointDiscoveryEnabled = true, + UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://") + })); + + services.AddKeyedScoped(BookshopQuotesBucketName); + } + + return services; + } +} diff --git a/LiteCharms.Extensions/ServiceBus.cs b/LiteCharms.Features/Extensions/ServiceBus.cs similarity index 80% rename from LiteCharms.Extensions/ServiceBus.cs rename to LiteCharms.Features/Extensions/ServiceBus.cs index ab8d7c0..3a57832 100644 --- a/LiteCharms.Extensions/ServiceBus.cs +++ b/LiteCharms.Features/Extensions/ServiceBus.cs @@ -1,9 +1,9 @@ -using LiteCharms.Abstractions; -using LiteCharms.Infrastructure.ServiceBus; -using LiteCharms.Infrastructure.ServiceBus.Exchanges; -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.ServiceBus; +using LiteCharms.Features.ServiceBus.Abstractions; +using LiteCharms.Features.ServiceBus.Exchanges; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Extensions; +namespace LiteCharms.Features.Extensions; public static class ServiceBus { diff --git a/LiteCharms.Abstractions/Timezones.cs b/LiteCharms.Features/Extensions/Timezones.cs similarity index 82% rename from LiteCharms.Abstractions/Timezones.cs rename to LiteCharms.Features/Extensions/Timezones.cs index 5457130..2c2a068 100644 --- a/LiteCharms.Abstractions/Timezones.cs +++ b/LiteCharms.Features/Extensions/Timezones.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Abstractions; +namespace LiteCharms.Features.Extensions; public static class Timezones { @@ -20,8 +20,8 @@ public static class Timezones ? new DateTimeOffset(sourceDateAdjusted.Ticks, SouthAfricanTimeZone.BaseUtcOffset).LocaliseDateTimeOffset(SouthAfricanTimeZone.BaseUtcOffset) : new DateTimeOffset(sourceDateAdjusted.Ticks, timezone!.BaseUtcOffset).LocaliseDateTimeOffset(timezone.BaseUtcOffset); - return DateTimeOffset.Parse(localised!); + return DateTimeOffset.Parse(localised!, CultureInfo.InvariantCulture); } - public static DateTimeOffset UtcNow(this TimeZoneInfo timezone) => ToDateTimeWithTimeZone(DateTime.Now, timezone); + public static DateTime UtcNow(this TimeZoneInfo timezone) => ToDateTimeWithTimeZone(DateTime.Now, timezone).UtcDateTime; } diff --git a/LiteCharms.Features/Hasher/Configuration/HasherSettings.cs b/LiteCharms.Features/Hasher/Configuration/HasherSettings.cs new file mode 100644 index 0000000..e30fb40 --- /dev/null +++ b/LiteCharms.Features/Hasher/Configuration/HasherSettings.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.Hasher.Configuration; + +public sealed class HasherSettings +{ + public string? Salt { get; set; } + + public int MinHashLength { get; set; } + + public string? PayfastPassphrase { get; set; } +} diff --git a/LiteCharms.Features/Hasher/HashService.cs b/LiteCharms.Features/Hasher/HashService.cs new file mode 100644 index 0000000..b6b79e0 --- /dev/null +++ b/LiteCharms.Features/Hasher/HashService.cs @@ -0,0 +1,89 @@ +using LiteCharms.Features.Abstractions; + +namespace LiteCharms.Features.Hasher; + +public sealed partial class HashService(IHashids hasher) : IService +{ + [GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex HexHashRegex { get; } + + [GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex Md5Regex { get; } + + [GeneratedRegex(@"\A[0-9a-fA-F]{64}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex Sha256Regex { get; } + + public static bool IsMd5Hash(string? value) => + !string.IsNullOrWhiteSpace(value) && Md5Regex.IsMatch(value); + + public static bool IsSha256Hash(string? value) => + !string.IsNullOrWhiteSpace(value) && Sha256Regex.IsMatch(value); + + public static string? StringToSha256Hash(string? input) => + string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))); + + public static string? StreamToSha256Hash(Stream stream) => + stream is null ? null : Convert.ToHexString(SHA256.HashData(stream)); + + public static string? BytesToSha256Hash(byte[] bytes) => + bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes)); + + public static Result ToMd5Hash(string input) + { + if (string.IsNullOrEmpty(input)) + return Result.Fail("Input content cannot be null or empty for MD5 processing."); + + byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input)); + return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant()); + } + + public Result HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input) + ? Result.Fail("Input must be a valid hexadecimal string.") + : Result.Ok(hasher.EncodeHex(input)); + + public Result HashEncodeIntId(int id) => id < 0 + ? Result.Fail("Id cannot be negative.") + : Result.Ok(hasher.Encode(id)); + + public Result HashEncodeLongId(long id) => id < 0 + ? Result.Fail("Id cannot be negative.") + : Result.Ok(hasher.EncodeLong(id)); + + public Result DecodeIntIdHash(string hash) + { + if (string.IsNullOrWhiteSpace(hash)) return Result.Fail("Invalid token layout."); + + int[] decoded = hasher.Decode(hash); + + return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail("Invalid or modified Int hash token."); + } + + public Result DecodeLongIdHash(string hash) + { + if (string.IsNullOrWhiteSpace(hash)) return Result.Fail("Invalid token layout."); + + long[] decoded = hasher.DecodeLong(hash); + + return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail("Invalid or modified Long hash token."); + } + + public Result DecodeHexHash(string hex) + { + try + { + string decoded = hasher.DecodeHex(hex); + + return string.IsNullOrEmpty(decoded) + ? Result.Fail("Invalid or corrupted hex hash.") + : Result.Ok(decoded); + } + catch (FormatException fex) + { + return Result.Fail(new Error("Invalid hash structure.").CausedBy(fex)); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} \ No newline at end of file diff --git a/LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs b/LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs deleted file mode 100644 index 5b120df..0000000 --- a/LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace LiteCharms.Features.Leads.Commands; - -public class CreateLeadCommand : IRequest> -{ - public Guid? CustomerId { get; set; } - - public string? Source { get; set; } - - public string? ClickId { get; set; } - - public string? WebClickId { get; set; } - - public string? AppClickId { get; set; } - - public long? CampaignId { get; set; } - - public long? AdGroupId { get; set; } - - public long? AdName { get; set; } - - public long? TargetId { get; set; } - - public long? FeedItemId { get; set; } - - public string? ClickLocation { get; set; } - - public string? AttribusionHash { get; set; } - - private CreateLeadCommand(Guid? customerId, string source, string clickId, string webClickId, string appClickId, long? campaignId, long? adGroupId, long? adName, long? targetId, long? feedItemId, string? clickLocation, string? attribusionHash) - { - CustomerId = customerId; - Source = source; - ClickId = clickId; - WebClickId = webClickId; - AppClickId = appClickId; - CampaignId = campaignId; - AdGroupId = adGroupId; - AdName = adName; - TargetId = targetId; - FeedItemId = feedItemId; - ClickLocation = clickLocation; - AttribusionHash = attribusionHash; - } - - public static CreateLeadCommand Create(Guid? customerId, string source, string clickId, string webClickId, string appClickId, long? campaignId, long? adGroupId, long? adName, long? targetId, long? feedItemId, string? clickLocation, string? attribusionHash) - { - if(string.IsNullOrWhiteSpace(source)) - throw new ArgumentNullException("Lead source is required to create a lead.", nameof(source)); - - if (string.IsNullOrWhiteSpace(clickId) || string.IsNullOrWhiteSpace(appClickId) || string.IsNullOrWhiteSpace(webClickId)) - throw new ArgumentException("ClickId, App ClickId and Web ClickId are required to create a lead."); - - return new(customerId, source, clickId, webClickId, appClickId, campaignId, adGroupId, adName, targetId, feedItemId, clickLocation, attribusionHash); - } -} diff --git a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs b/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs deleted file mode 100644 index bd99665..0000000 --- a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs +++ /dev/null @@ -1,47 +0,0 @@ -using LiteCharms.Features.Utilities.Commands; -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Leads.Commands.Handlers; - -public class CreateLeadCommandHandler(IDbContextFactory contextFactory, ISender mediator) : IRequestHandler> -{ - public async ValueTask> Handle(CreateLeadCommand request, CancellationToken cancellationToken) - { - try - { - var hashCommand = ComputeHashCommand.Create($"{request.ClickId}{request.AppClickId}{request.WebClickId}"); - var hashResult = await mediator.Send(hashCommand, cancellationToken); - - if(hashResult.IsFailed) - return Result.Fail(new Error($"Failed to compute hash for lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}") - .CausedBy(hashResult.Errors)); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var newLead = context.Leads.Add(new Entities.Lead - { - WebClickId = request.WebClickId, - AppClickId = request.AppClickId, - Source = request.Source, - ClickId = request.ClickId, - AdGroupId = request.AdGroupId, - AdName = request.AdName, - CampaignId = request.CampaignId, - ClickLocation = request.ClickLocation, - CustomerId = request.CustomerId, - FeedItemId = request.FeedItemId, - Status = Models.LeadStatus.New, - TargetId = request.TargetId, - AttributionHash = hashResult.Value - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(newLead.Entity.Id) - : Result.Fail(new Error($"Failed to create lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs b/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs deleted file mode 100644 index 7187005..0000000 --- a/LiteCharms.Features/Leads/Commands/Handlers/UpdateLeadCommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Leads.Commands.Handlers; - -public class UpdateLeadCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateLeadCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var lead = await context.Leads.FirstOrDefaultAsync(l => l.Id == request.LeadId, cancellationToken); - - if (lead is null) - return Result.Fail(new Error($"Lead with ID {request.LeadId} not found.")); - - lead.Status = request.Status; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error($"Failed to update the lead {request.LeadId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Leads/Commands/UpdateLeadCommand.cs b/LiteCharms.Features/Leads/Commands/UpdateLeadCommand.cs deleted file mode 100644 index a201b70..0000000 --- a/LiteCharms.Features/Leads/Commands/UpdateLeadCommand.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Leads.Commands; - -public class UpdateLeadCommand : IRequest -{ - public Guid LeadId { get; set; } - - public LeadStatus Status { get; set; } - - private UpdateLeadCommand(Guid leadId, LeadStatus status) - { - LeadId = leadId; - Status = status; - } - - public static UpdateLeadCommand Create(Guid leadId, LeadStatus status) - { - if (leadId == Guid.Empty) - throw new ArgumentException("Lead ID cannot be empty.", nameof(leadId)); - - if (!Enum.IsDefined(typeof(LeadStatus), status)) - throw new ArgumentException("Invalid lead status.", nameof(status)); - - return new(leadId, status); - } -} diff --git a/LiteCharms.Features/Leads/Queries/GetCustomerLeadsQuery.cs b/LiteCharms.Features/Leads/Queries/GetCustomerLeadsQuery.cs deleted file mode 100644 index ddede3c..0000000 --- a/LiteCharms.Features/Leads/Queries/GetCustomerLeadsQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Leads.Queries; - -public class GetCustomerLeadsQuery : IRequest> -{ - public Guid CustomerId { get; } - - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - private GetCustomerLeadsQuery(Guid customerId, DateOnly from, DateOnly to) - { - CustomerId = customerId; - From = from; - To = to; - } - - public static GetCustomerLeadsQuery Create(Guid customerId, DateOnly from, DateOnly to) - { - if(customerId == Guid.Empty) - throw new ArgumentException("Customer ID cannot be empty.", nameof(customerId)); - - if(from > to) - throw new ArgumentException("The 'From' date cannot be later than the 'To' date."); - - return new(customerId, from, to); - } -} diff --git a/LiteCharms.Features/Leads/Queries/GetLeadsQuery.cs b/LiteCharms.Features/Leads/Queries/GetLeadsQuery.cs deleted file mode 100644 index 3278bf7..0000000 --- a/LiteCharms.Features/Leads/Queries/GetLeadsQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Leads.Queries; - -public class GetLeadsQuery : IRequest> -{ - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - public int MaxRecords { get; set; } - - private GetLeadsQuery(DateOnly from, DateOnly to, int maxRecords = 1000) - { - From = from; - To = to; - MaxRecords = maxRecords; - } - - public static GetLeadsQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000) - { - if (from > to) - throw new ArgumentException("From date cannot be greater than To date."); - - if(maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer."); - - return new(from, to, maxRecords); - } -} diff --git a/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs b/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs deleted file mode 100644 index 630b9c5..0000000 --- a/LiteCharms.Features/Leads/Queries/Handlers/GetCustomerLeadsQueryHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Leads.Queries.Handlers; - -public class GetCustomerLeadsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerLeadsQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var leads = await context.Leads.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(lead => lead.CustomerId == request.CustomerId) - .Where(lead => lead.CreatedAt.Date >= fromDate && lead.CreatedAt.Date <= toDate) - .ToArrayAsync(cancellationToken); - - return leads?.Length > 0 - ? Result.Ok(leads.Select(l => l.ToModel()).ToArray()) - : Result.Fail(new Error($"No customer {request.CustomerId} leads found for the specified date range {request.From} to {request.To}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs b/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs deleted file mode 100644 index 2496116..0000000 --- a/LiteCharms.Features/Leads/Queries/Handlers/GetLeadsQueryHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Leads.Queries.Handlers; - -public class GetLeadsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetLeadsQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var leads = await context.Leads.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(l => l.CreatedAt.Date >= fromDate && l.CreatedAt.Date <= toDate) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return leads?.Length > 0 - ? Result.Ok(leads.Select(l => l.ToModel()).ToArray()) - : Result.Fail(new Error($"No leads found for the specified date range {request.From} to {request.To}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/LiteCharms.Features.csproj b/LiteCharms.Features/LiteCharms.Features.csproj index 9a2a0ac..60259b7 100644 --- a/LiteCharms.Features/LiteCharms.Features.csproj +++ b/LiteCharms.Features/LiteCharms.Features.csproj @@ -21,6 +21,7 @@ LICENSE utility;dotnet icon.png + 8a78916e-c86b-4f4b-9f4e-d8e7769b5d23 @@ -28,10 +29,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + - - + + @@ -47,18 +180,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + - - - - - + diff --git a/LiteCharms.Features/Mediator/LoggingPipelineBehavior.cs b/LiteCharms.Features/Mediator/LoggingPipelineBehavior.cs new file mode 100644 index 0000000..2bf0410 --- /dev/null +++ b/LiteCharms.Features/Mediator/LoggingPipelineBehavior.cs @@ -0,0 +1,29 @@ +namespace LiteCharms.Features.Mediator; + +public sealed class LoggingPipelineBehavior(ILogger> logger) : + IPipelineBehavior + where TRequest : IRequest + where TResponse : ResultBase, new() +{ + public async ValueTask Handle(TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + TResponse? response = await next(message, cancellationToken); + + if (response is null) + logger.LogCritical("{Request} {TypeName} was returned as null", typeof(TRequest).Name, typeof(TRequest).Name); + + if(response?.IsFailed == true || response?.Errors?.Any() == true) + { + foreach (var error in response.Errors) + { + if (!string.IsNullOrWhiteSpace(error.Message)) + logger.LogWarning("{Request} {Error}", typeof(TRequest).Name, error.Message); + + if (error?.Reasons?.Count > 0) + error.Reasons.ForEach(r => logger.LogError("{Request} {Reason}", typeof(TRequest).Name, r.ToString())); + } + } + + return response; + } +} diff --git a/LiteCharms.Features/Mediator/MediatorTelemetry.cs b/LiteCharms.Features/Mediator/MediatorTelemetry.cs new file mode 100644 index 0000000..00493c1 --- /dev/null +++ b/LiteCharms.Features/Mediator/MediatorTelemetry.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.Mediator; + +public static class MediatorTelemetry +{ + public const string ServiceName = "LiteCharms.Mediator"; + + public static readonly ActivitySource Source = new(ServiceName); + public static readonly Meter Meter = new(ServiceName); + + public static readonly Counter RequestCounter = Meter.CreateCounter("mediator_requests_total"); + public static readonly Histogram RequestDuration = Meter.CreateHistogram("mediator_request_duration_ms"); +} diff --git a/LiteCharms.Features/Mediator/TelemetryPipelineBehavior.cs b/LiteCharms.Features/Mediator/TelemetryPipelineBehavior.cs new file mode 100644 index 0000000..0a9efbd --- /dev/null +++ b/LiteCharms.Features/Mediator/TelemetryPipelineBehavior.cs @@ -0,0 +1,66 @@ +namespace LiteCharms.Features.Mediator; + +public sealed class TelemetryPipelineBehavior : + IPipelineBehavior + where TRequest : IRequest + where TResponse : ResultBase, new() +{ + public async ValueTask Handle(TRequest message, MessageHandlerDelegate next, CancellationToken cancellationToken) + { + var requestName = typeof(TRequest).Name; + + using var activity = MediatorTelemetry.Source.StartActivity(requestName); + + activity?.SetTag("mediator.request_type", typeof(TRequest).FullName); + + var stopWatch = Stopwatch.StartNew(); + var status = "Success"; + + try + { + TResponse? response = await next(message, cancellationToken); + + if (response is null) + { + status = "NullResponse"; + activity?.SetStatus(ActivityStatusCode.Error, "Response was null"); + + return response; + } + + if (response.IsFailed) + { + status = "Failed"; + activity?.SetStatus(ActivityStatusCode.Error, "Request failed"); + + var firstError = response.Errors.FirstOrDefault()?.Message ?? "Unknown Error"; + activity?.SetTag("error.message", firstError); + + foreach (var error in response.Errors) + activity?.AddEvent(new ActivityEvent("Result Error", tags: new() { { "message", error.Message } })); + } + else + activity?.SetStatus(ActivityStatusCode.Ok); + + return response; + } + catch (Exception ex) + { + status = "Exception"; + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + + activity?.AddException(ex); + + throw; + } + finally + { + stopWatch.Stop(); + + var tags = new TagList { { "request", requestName }, { "status", status } }; + + MediatorTelemetry.RequestCounter.Add(1, tags); + MediatorTelemetry.RequestDuration.Record(stopWatch.Elapsed.TotalMilliseconds, tags); + } + } +} \ No newline at end of file diff --git a/LiteCharms.Features/Models/DateRange.cs b/LiteCharms.Features/Models/DateRange.cs new file mode 100644 index 0000000..a5616b4 --- /dev/null +++ b/LiteCharms.Features/Models/DateRange.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.Models; + +public sealed class DateRange +{ + public DateOnly From { get; set; } + + public DateOnly To { get; set; } + + public int MaxRecords { get; set; } +} diff --git a/LiteCharms.Features/Models/PageReference.cs b/LiteCharms.Features/Models/PageReference.cs new file mode 100644 index 0000000..12d53cb --- /dev/null +++ b/LiteCharms.Features/Models/PageReference.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.Models; + +public sealed class PageReference +{ + public string? Tag { get; set; } + + public string? Description { get; set; } + + public string? Url { get; set; } +} diff --git a/LiteCharms.Features/Models/ProductFilter.cs b/LiteCharms.Features/Models/ProductFilter.cs new file mode 100644 index 0000000..78f6d90 --- /dev/null +++ b/LiteCharms.Features/Models/ProductFilter.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Features.Models; + +public sealed class ProductFilter +{ + public string? Name { get; set; } + + public string? Title { get; set; } + + public string? Manufacturer { get; set; } + + public string? SerialNumber { get; set; } + + public decimal MinPrice { get; set; } + + public decimal MaxPrice { get; set; } +} diff --git a/LiteCharms.Features/Models/ProductMetadata.cs b/LiteCharms.Features/Models/ProductMetadata.cs new file mode 100644 index 0000000..d059f36 --- /dev/null +++ b/LiteCharms.Features/Models/ProductMetadata.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.Models; + +public sealed class ProductMetadata +{ + public string? Manufacturer { get; set; } + + public string? ManufactureDate { get; set; } + + public string? CopyrightInfo { get; set; } + + public string? SerialNumber { get; set; } +} diff --git a/LiteCharms.Features/Models/SocialMedia.cs b/LiteCharms.Features/Models/SocialMedia.cs new file mode 100644 index 0000000..b4f10c2 --- /dev/null +++ b/LiteCharms.Features/Models/SocialMedia.cs @@ -0,0 +1,13 @@ + +namespace LiteCharms.Features.Models; + +public sealed class SocialMedia +{ + public SocialMediaTypes Type { get; set; } + + public string? Name { get; set; } + + public string? ImageUrl { get; set; } + + public string? Url { get; set; } +} diff --git a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs b/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs deleted file mode 100644 index cfa40b1..0000000 --- a/LiteCharms.Features/Notifications/Commands/CreateNotificationCommand.cs +++ /dev/null @@ -1,63 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Notifications.Commands; - -public class CreateNotificationCommand : IRequest> -{ - public NotificationDirection Direction { get; set; } - - public string? Author { get; set; } - - public string? Title { get; set; } - - public string? Description { get; set; } - - public string? Platform { get; set; } - - public string? PlatformAddress { get; set; } - - public string? CorrelationId { get; set; } - - public string? CorrelationIdType { 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) - { - Direction = direction; - Author = author; - Title = title; - Description = description; - Platform = platform; - PlatformAddress = platformAddress; - CorrelationId = correlationId; - CorrelationIdType = correlationIdType; - IsInternal = isInternal; - } - - public static CreateNotificationCommand Create(NotificationDirection direction, string author, string title, string description, string platform, string platformAddress, string correlationId, string correlationIdType, bool isInternal) - { - if (string.IsNullOrWhiteSpace(author)) - throw new ArgumentException("Author cannot be null or whitespace.", nameof(author)); - - if (string.IsNullOrWhiteSpace(title)) - throw new ArgumentException("Title cannot be null or whitespace.", nameof(title)); - - if (string.IsNullOrWhiteSpace(description)) - throw new ArgumentException("Description cannot be null or whitespace.", nameof(description)); - - if (string.IsNullOrWhiteSpace(platform)) - throw new ArgumentException("Platform cannot be null or whitespace.", nameof(platform)); - - if (string.IsNullOrWhiteSpace(platformAddress)) - throw new ArgumentException("PlatformAddress cannot be null or whitespace.", nameof(platformAddress)); - - if (string.IsNullOrWhiteSpace(correlationId)) - throw new ArgumentException("CorrelationId cannot be null or whitespace.", nameof(correlationId)); - - if (string.IsNullOrWhiteSpace(correlationIdType)) - throw new ArgumentException("CorrelationIdType cannot be null or whitespace.", nameof(correlationIdType)); - - return new(direction, author, title, description, platform, platformAddress, correlationId, correlationIdType, isInternal); - } -} diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs b/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs deleted file mode 100644 index e57569b..0000000 --- a/LiteCharms.Features/Notifications/Commands/Handlers/CreateNotificationCommandHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Notifications.Commands.Handlers; - -public class CreateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(CreateNotificationCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var newNotification = context.Notifications.Add(new Entities.Notification - { - Direction = request.Direction, - Author = request.Author, - Title = request.Title, - Description = request.Description, - Platform = request.Platform, - PlatformAddress = request.PlatformAddress, - CorrelationId = request.CorrelationId, - CorrelationIdType = request.CorrelationIdType, - IsInternal = request.IsInternal, - }); - - return newNotification is not null - ? Result.Ok(newNotification.Entity.Id) - : Result.Fail(new Error("Failed to create notification")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs b/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs deleted file mode 100644 index 2adb588..0000000 --- a/LiteCharms.Features/Notifications/Commands/Handlers/UpdateNotificationCommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Notifications.Commands.Handlers; - -public class UpdateNotificationCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateNotificationCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var notification = await context.Notifications.FirstOrDefaultAsync(n => n.Id == request.NotificationId, cancellationToken); - - if(notification is null) - return Result.Fail(new Error($"Notification with id {request.NotificationId} not found.")); - - notification.Processed = request.Processed; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error($"Failed to update notification with id {request.NotificationId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs b/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs deleted file mode 100644 index d5961f2..0000000 --- a/LiteCharms.Features/Notifications/Commands/UpdateNotificationCommand.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace LiteCharms.Features.Notifications.Commands; - -public class UpdateNotificationCommand : IRequest -{ - public Guid NotificationId { get; set; } - - public bool Processed { get; set; } - - private UpdateNotificationCommand(Guid notificationId, bool processed) - { - NotificationId = notificationId; - Processed = processed; - } - - public static UpdateNotificationCommand Create(Guid notificationId, bool processed) - { - if(notificationId == Guid.Empty) - throw new ArgumentException("Notification ID cannot be empty.", nameof(notificationId)); - - return new(notificationId, processed); - } -} diff --git a/LiteCharms.Features/Notifications/Queries/GetNotificationQuery.cs b/LiteCharms.Features/Notifications/Queries/GetNotificationQuery.cs deleted file mode 100644 index f41aea5..0000000 --- a/LiteCharms.Features/Notifications/Queries/GetNotificationQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Notifications.Queries; - -public class GetNotificationQuery : IRequest> -{ - public Guid NotificationId { get; set; } - - private GetNotificationQuery(Guid notificationId) => NotificationId = notificationId; - - public static GetNotificationQuery Create(Guid notificationId) - { - if (notificationId == Guid.Empty) - throw new ArgumentException("Notification ID is required.", nameof(notificationId)); - - return new(notificationId); - } -} diff --git a/LiteCharms.Features/Notifications/Queries/GetNotificationsQuery.cs b/LiteCharms.Features/Notifications/Queries/GetNotificationsQuery.cs deleted file mode 100644 index 6eef589..0000000 --- a/LiteCharms.Features/Notifications/Queries/GetNotificationsQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Notifications.Queries; - -public class GetNotificationsQuery : IRequest> -{ - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - public int MaxRecords { get; set; } - - private GetNotificationsQuery(DateOnly from, DateOnly to, int maxRecords = 1000) - { - From = from; - To = to; - MaxRecords = maxRecords; - } - - public static GetNotificationsQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000) - { - if (from > to) - throw new ArgumentException("From date cannot be greater than To date."); - - if(maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer.", nameof(maxRecords)); - - return new(from, to, maxRecords); - } -} diff --git a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs deleted file mode 100644 index 133e3bf..0000000 --- a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Notifications.Queries.Handlers; - -public class GetNotificationQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetNotificationQuery request, CancellationToken cancellationToken) - { - try - { - await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var notification = await context.Notifications.FindAsync(new object[] { request.NotificationId }, cancellationToken); - - return notification is not null - ? Result.Ok(notification.ToModel()) - : Result.Fail(new Error($"Notification with id {request.NotificationId} not found")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs b/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs deleted file mode 100644 index f6ac872..0000000 --- a/LiteCharms.Features/Notifications/Queries/Handlers/GetNotificationsQueryHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Notifications.Queries.Handlers; - -public class GetNotificationsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetNotificationsQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var notifications = await context.Notifications.AsNoTracking() - .Where(n => n.CreatedAt >= fromDate && n.CreatedAt <= toDate) - .OrderByDescending(n => n.CreatedAt) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return notifications?.Length > 0 - ? Result.Ok(notifications.Select(n => n.ToModel()).ToArray()) - : Result.Fail(new Error($"No notifications found for the specified date range {request.From} to {request.To}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs b/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs deleted file mode 100644 index e18258e..0000000 --- a/LiteCharms.Features/Orders/Commands/CreateOrderCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace LiteCharms.Features.Orders.Commands; - -public class CreateOrderCommand : IRequest> -{ - public Guid CustomerId { get; set; } - - public Guid ShoppingCartId { get; set; } - - public Guid? QuoteId { get; set; } - - private CreateOrderCommand(Guid customerId, Guid shoppingCartId, Guid? quoteId = null) - { - CustomerId = customerId; - ShoppingCartId = shoppingCartId; - QuoteId = quoteId; - } - - public static CreateOrderCommand Create(Guid customerId, Guid shoppingCartId, Guid? quoteId = null) - { - if (customerId == Guid.Empty) - throw new ArgumentException("CustomerId is required.", nameof(customerId)); - - if (shoppingCartId == Guid.Empty) - throw new ArgumentException("ShoppingCartId is required.", nameof(shoppingCartId)); - - return new(customerId, shoppingCartId, quoteId); - } -} diff --git a/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs b/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs deleted file mode 100644 index 1200afb..0000000 --- a/LiteCharms.Features/Orders/Commands/Handlers/CreateOrderCommandHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Orders.Commands.Handlers; - -public class CreateOrderCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(CreateOrderCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if(!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer {request.CustomerId} does not exist.")); - - if(!await context.ShoppingCarts.AnyAsync(sc => sc.Id == request.ShoppingCartId, cancellationToken)) - return Result.Fail(new Error($"Shopping cart {request.ShoppingCartId} does not exist.")); - - if(request.QuoteId.HasValue && !await context.Quotes.AnyAsync(q => q.Id == request.QuoteId.Value, cancellationToken)) - return Result.Fail(new Error($"Quote {request.QuoteId.Value} does not exist.")); - - var newOrder = context.Orders.Add(new Entities.Order - { - CustomerId = request.CustomerId, - ShoppingCartId = request.ShoppingCartId, - QuoteId = request.QuoteId, - CreatedAt = DateTime.UtcNow - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(newOrder.Entity.Id) - : Result.Fail(new Error($"Failed to create customer {request.CustomerId} order using shopping cart {request.ShoppingCartId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs b/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs deleted file mode 100644 index 2524b79..0000000 --- a/LiteCharms.Features/Orders/Commands/Handlers/UpdateOrderStatusCommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Orders.Commands.Handlers; - -public class UpdateOrderStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateOrderStatusCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken); - - if (order is null) - return Result.Fail(new Error($"Order {request.OrderId} not found")); - - order.Status = request.Status; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error($"Failed to update order {request.OrderId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Orders/Commands/UpdateOrderStatusCommand.cs b/LiteCharms.Features/Orders/Commands/UpdateOrderStatusCommand.cs deleted file mode 100644 index 3e6d1c6..0000000 --- a/LiteCharms.Features/Orders/Commands/UpdateOrderStatusCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Orders.Commands; - -public class UpdateOrderStatusCommand : IRequest -{ - public Guid OrderId { get; set; } - - public OrderStatus Status { get; set; } - - public string? Note { get; set; } - - private UpdateOrderStatusCommand(Guid orderId, OrderStatus status, string? note) - { - OrderId = orderId; - Status = status; - Note = note; - } - - public static UpdateOrderStatusCommand Create(Guid orderId, OrderStatus status, string? note) - { - if (orderId == Guid.Empty) - throw new ArgumentException("OrderId is required.", nameof(orderId)); - - if (!Enum.IsDefined(typeof(OrderStatus), status)) - throw new ArgumentException("Invalid order status.", nameof(status)); - - return new(orderId, status, note); - } -} diff --git a/LiteCharms.Features/Orders/Queries/GetCustomerOrdersQuery.cs b/LiteCharms.Features/Orders/Queries/GetCustomerOrdersQuery.cs deleted file mode 100644 index 7d11f91..0000000 --- a/LiteCharms.Features/Orders/Queries/GetCustomerOrdersQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Orders.Queries; - -public class GetCustomerOrdersQuery : IRequest> -{ - public Guid CustomerId { get; } - - private GetCustomerOrdersQuery(Guid customerId) => CustomerId = customerId; - - public static GetCustomerOrdersQuery Create(Guid customerId) - { - if (customerId == Guid.Empty) - throw new ArgumentException("CustomerId is required.", nameof(customerId)); - - return new(customerId); - } -} diff --git a/LiteCharms.Features/Orders/Queries/GetOrderRefundQuery.cs b/LiteCharms.Features/Orders/Queries/GetOrderRefundQuery.cs deleted file mode 100644 index 887e03b..0000000 --- a/LiteCharms.Features/Orders/Queries/GetOrderRefundQuery.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Orders.Queries; - -public class GetOrderRefundQuery : IRequest> -{ - public Guid OrderId { get; set; } - - public Guid OrderRefundId { get; set; } - - private GetOrderRefundQuery(Guid orderId, Guid orderRefundId) - { - OrderId = orderId; - OrderRefundId = orderRefundId; - } - - public static GetOrderRefundQuery Create(Guid orderId, Guid orderRefundId) - { - if (orderId == Guid.Empty) - throw new ArgumentException("OrderId is required.", nameof(orderId)); - - if (orderRefundId == Guid.Empty) - throw new ArgumentException("OrderRefundId is required.", nameof(orderRefundId)); - - return new(orderId, orderRefundId); - } -} diff --git a/LiteCharms.Features/Orders/Queries/GetOrdersQuery.cs b/LiteCharms.Features/Orders/Queries/GetOrdersQuery.cs deleted file mode 100644 index c0c2c3b..0000000 --- a/LiteCharms.Features/Orders/Queries/GetOrdersQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Orders.Queries; - -public class GetOrdersQuery : IRequest> -{ - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - public int MaxRecords { get; set; } - - private GetOrdersQuery(DateOnly from, DateOnly to, int maxRecords = 1000) - { - From = from; - To = to; - MaxRecords = maxRecords; - } - - public static GetOrdersQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000) - { - if (from > to) - throw new ArgumentException("From date cannot be greater than To date."); - - if(maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer."); - - return new(from, to, maxRecords); - } -} \ No newline at end of file diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs b/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs deleted file mode 100644 index 6d683ce..0000000 --- a/LiteCharms.Features/Orders/Queries/Handlers/GetCustomerOrdersQueryHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Orders.Queries.Handlers; - -public class GetCustomerOrdersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerOrdersQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if(!await context.Customers.AsNoTracking().AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer with Id {request.CustomerId} does not exist.")); - - var orders = await context.Orders.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(o => o.CustomerId == request.CustomerId) - .ToArrayAsync(cancellationToken); - - return orders?.Length > 0 - ? Result.Ok(orders.Select(o => o.ToModel()).ToArray()) - : Result.Fail(new Error($"No orders found for customer with Id {request.CustomerId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs b/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs deleted file mode 100644 index 26bc1f2..0000000 --- a/LiteCharms.Features/Orders/Queries/Handlers/GetOrderRefundQueryHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Orders.Queries.Handlers; - -public class GetOrderRefundQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetOrderRefundQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var refund = await context.OrderRefunds.AsNoTracking() - .FirstOrDefaultAsync(r => r.OrderId == request.OrderId && r.Id == request.OrderRefundId, cancellationToken); - - return refund is not null - ? Result.Ok(refund.ToModel()) - : Result.Fail(new Error($"Refund {request.OrderRefundId} not found for the given OrderId: {request.OrderId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs b/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs deleted file mode 100644 index 29b25a3..0000000 --- a/LiteCharms.Features/Orders/Queries/Handlers/GetOrdersQueryHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Orders.Queries.Handlers; - -public class GetOrdersQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetOrdersQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var orders = await context.Orders - .OrderByDescending(o => o.CreatedAt) - .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return orders?.Length > 0 - ? Result.Ok(orders.Select(o => o.ToModel()).ToArray()) - : Result.Fail(new Error($"No orders found for the specified date range {request.From} - {request.To}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Products/Queries/GetProductPriceQuery.cs b/LiteCharms.Features/Products/Queries/GetProductPriceQuery.cs deleted file mode 100644 index 347d759..0000000 --- a/LiteCharms.Features/Products/Queries/GetProductPriceQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Products.Queries; - -public class GetProductPriceQuery : IRequest> -{ - public Guid ProductId { get; set; } - - private GetProductPriceQuery(Guid productId) => ProductId = productId; - - public static GetProductPriceQuery Create(Guid productId) - { - if (productId == Guid.Empty) - throw new ArgumentException("ProductId is required.", nameof(productId)); - - return new(productId); - } -} diff --git a/LiteCharms.Features/Products/Queries/GetProductPricesQuery.cs b/LiteCharms.Features/Products/Queries/GetProductPricesQuery.cs deleted file mode 100644 index 6e9cf66..0000000 --- a/LiteCharms.Features/Products/Queries/GetProductPricesQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Products.Queries; - -public class GetProductPricesQuery : IRequest> -{ - public int MaxRecords { get; set; } - - private GetProductPricesQuery(int maxRecords = 1000) => MaxRecords = maxRecords; - - public static GetProductPricesQuery Create(int maxRecords = 1000) - { - if (maxRecords <= 0) - throw new ArgumentOutOfRangeException(nameof(maxRecords), "MaxRecords must be greater than zero."); - - return new(maxRecords); - } -} diff --git a/LiteCharms.Features/Products/Queries/GetProductQuery.cs b/LiteCharms.Features/Products/Queries/GetProductQuery.cs deleted file mode 100644 index 2f5b2a8..0000000 --- a/LiteCharms.Features/Products/Queries/GetProductQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Products.Queries; - -public class GetProductQuery : IRequest> -{ - public Guid ProductId { get; set; } - - private GetProductQuery(Guid productId) => ProductId = productId; - - public static GetProductQuery Create(Guid productId) - { - if(productId == Guid.Empty) - throw new ArgumentException("Product ID is required.", nameof(productId)); - - return new(productId); - } -} diff --git a/LiteCharms.Features/Products/Queries/GetProductsQuery.cs b/LiteCharms.Features/Products/Queries/GetProductsQuery.cs deleted file mode 100644 index 1c7082d..0000000 --- a/LiteCharms.Features/Products/Queries/GetProductsQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Products.Queries; - -public class GetProductsQuery : IRequest> -{ - public int MaxRecords { get; set; } - - private GetProductsQuery(int maxRecords = 1000) => MaxRecords = maxRecords; - - public static GetProductsQuery Create(int maxRecords = 1000) - { - if (maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer."); - - return new(maxRecords); - } -} diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs deleted file mode 100644 index 12bcc9f..0000000 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductPriceQueryHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Products.Queries.Handlers; - -public class GetProductPriceQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetProductPriceQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if(!await context.Products.AnyAsync(p => p.Id == request.ProductId, cancellationToken)) - return Result.Fail(new Error($"Product {request.ProductId} not found.")); - - var productPrice = await context.ProductPrices.AsNoTracking() - .Where(pp => pp.ProductId == request.ProductId && pp.Active) - .OrderByDescending(pp => pp.CreatedAt) - .FirstOrDefaultAsync(cancellationToken); - - return productPrice is not null - ? Result.Ok(productPrice.ToModel()) - : Result.Fail(new Error($"Product price {request.ProductId} not found.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs deleted file mode 100644 index 0f1f5af..0000000 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductPricesQueryHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Products.Queries.Handlers; - -public class GetProductPricesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetProductPricesQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var products = await context.ProductPrices.AsNoTracking() - .OrderByDescending(o => o.Id) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return Result.Ok(products.Select(p => p.ToModel()).ToArray()); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs deleted file mode 100644 index cca604f..0000000 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Products.Queries.Handlers; - -public class GetProductQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetProductQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var product = await context.Products.FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken); - - return product is not null - ? Result.Ok(product.ToModel()) - : Result.Fail(new Error($"Product with ID {request.ProductId} not found.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs b/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs deleted file mode 100644 index 1b99b16..0000000 --- a/LiteCharms.Features/Products/Queries/Handlers/GetProductsQueryHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Products.Queries.Handlers; - -public class GetProductsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetProductsQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var products = await context.Products.AsNoTracking() - .OrderByDescending(o => o.Id) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return Result.Ok(products.Select(p => p.ToModel()).ToArray()); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs b/LiteCharms.Features/Quartz/JobOrchestrator.cs similarity index 53% rename from LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs rename to LiteCharms.Features/Quartz/JobOrchestrator.cs index c03f3c6..e8cb77a 100644 --- a/LiteCharms.Infrastructure/Quartz/JobOrchestrator.cs +++ b/LiteCharms.Features/Quartz/JobOrchestrator.cs @@ -1,17 +1,16 @@ -using LiteCharms.Abstractions; -using static LiteCharms.Abstractions.Timezones; +using LiteCharms.Features.Abstractions; -namespace LiteCharms.Infrastructure.Quartz; +namespace LiteCharms.Features.Quartz; -public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator +public sealed class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestrator { - public async Task SendAsync(TNotification notification, CancellationToken cancellationToken = default) + public async ValueTask SendAsync(TNotification notification, CancellationToken cancellationToken = default) where TNotification : IEvent { var chainedJobGroup = "onetime-jobs"; var scheduler = await schedulerFactory.GetScheduler(cancellationToken); - var jobKey = new JobKey($"{notification.Name.ToLower()}-{notification.CorrelationId.ToLower()}", chainedJobGroup); + var jobKey = new JobKey($"{notification.Name.ToLower(CultureInfo.InvariantCulture)}-{notification.CorrelationId.ToLower(CultureInfo.InvariantCulture)}", chainedJobGroup); var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup); var job = JobBuilder.Create>() @@ -19,6 +18,7 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra .WithDescription($"Correlation ID: {notification.CorrelationId}") .UsingJobData(new JobDataMap { ["Payload"] = JsonSerializer.Serialize(notification) }) .DisallowConcurrentExecution() + .RequestRecovery() .Build(); var trigger = global::Quartz.TriggerBuilder.Create() @@ -29,13 +29,13 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra await scheduler.ScheduleJob(job, new List { trigger }.AsReadOnly(), replace: true, cancellationToken); } - public async Task ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) + public async ValueTask ScheduleAsync(TNotification notification, string cronExpression, CancellationToken cancellationToken = default) where TNotification : IEvent { var chainedJobGroup = "scheduled-jobs"; var scheduler = await schedulerFactory.GetScheduler(cancellationToken); - var jobKey = new JobKey($"{notification.Name.ToLower()}-{notification.CorrelationId.ToLower()}", chainedJobGroup); + var jobKey = new JobKey($"{notification.Name.ToLower(CultureInfo.InvariantCulture)}", chainedJobGroup); var triggerKey = new TriggerKey($"{jobKey.Name}-trigger", chainedJobGroup); var job = JobBuilder.Create>() @@ -46,14 +46,14 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra .StoreDurably() .Build(); - var now = SouthAfricanTimeZone.UtcNow(); + var now = DateTime.UtcNow; var trigger = global::Quartz.TriggerBuilder.Create() .WithIdentity(triggerKey) .WithDescription($"Scheduled via Main Job at {now:g}") - .WithCronSchedule(cronExpression, cron => cron.InTimeZone(SouthAfricanTimeZone) - .WithMisfireHandlingInstructionFireAndProceed()) - .StartAt(now) + .WithCronSchedule(cronExpression, cron => cron + .WithMisfireHandlingInstructionIgnoreMisfires()) + .StartAt((DateTimeOffset)now) .Build(); await scheduler.AddJob(job, replace: true, cancellationToken); @@ -63,4 +63,25 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra else await scheduler.ScheduleJob(job, new List { trigger }.AsReadOnly(), replace: true, cancellationToken); } + + public async ValueTask InterruptAsync(string eventName, string? correlationId = null, CancellationToken cancellationToken = default) + { + var scheduler = await schedulerFactory.GetScheduler(cancellationToken); + + var jobKeyName = string.Empty; + var jobGroup = string.Empty; + + if (!string.IsNullOrWhiteSpace(correlationId)) + { + jobKeyName = $"{eventName.ToLower(CultureInfo.InvariantCulture)}-{correlationId.ToLower(CultureInfo.InvariantCulture)}"; + jobGroup = "onetime-jobs"; + } + else + { + jobKeyName = eventName.ToLower(CultureInfo.InvariantCulture); + jobGroup = "scheduled-jobs"; + } + + return await scheduler.Interrupt(JobKey.Create(jobKeyName, jobGroup), cancellationToken); + } } diff --git a/LiteCharms.Features/Quartz/MediatorJob.cs b/LiteCharms.Features/Quartz/MediatorJob.cs new file mode 100644 index 0000000..108b55c --- /dev/null +++ b/LiteCharms.Features/Quartz/MediatorJob.cs @@ -0,0 +1,51 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.Mediator; + +namespace LiteCharms.Features.Quartz; + +[DisallowConcurrentExecution] +public sealed class MediatorJob(IMediator mediator) : IJob where TNotification : IEvent +{ + public async Task Execute(IJobExecutionContext context) + { + if (context.Recovering) + Trace.WriteLine($"CRITICAL RECOVERY: Resurrecting job '{typeof(TNotification).Name}' after a previous cluster node crashed mid-execution."); + + var data = context.MergedJobDataMap["Payload"] as string; + + if (string.IsNullOrWhiteSpace(data)) + { + Trace.WriteLine("Job Payload missing, job ended"); + + return; + } + + var notification = JsonSerializer.Deserialize(data); + + if (notification is null) + { + Trace.WriteLine("Notification could not be Json converted from data string, job ended"); + + return; + } + + using var activity = MediatorTelemetry.Source.StartActivity(typeof(TNotification).Name); + + activity?.SetTag("event.correlation_id", notification.CorrelationId); + + try + { + await mediator.Publish(notification, context.CancellationToken); + + Trace.WriteLine("Job published successfully"); + } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + Trace.WriteLine($"Job '{typeof(TNotification).Name}' was gracefully interrupted by the cluster control plane."); + + activity?.SetStatus(ActivityStatusCode.Ok); + + return; + } + } +} diff --git a/LiteCharms.Infrastructure/Quartz/RetryJobListener.cs b/LiteCharms.Features/Quartz/RetryJobListener.cs similarity index 78% rename from LiteCharms.Infrastructure/Quartz/RetryJobListener.cs rename to LiteCharms.Features/Quartz/RetryJobListener.cs index afe84e7..1de4161 100644 --- a/LiteCharms.Infrastructure/Quartz/RetryJobListener.cs +++ b/LiteCharms.Features/Quartz/RetryJobListener.cs @@ -1,6 +1,6 @@ -namespace LiteCharms.Infrastructure.Quartz; +namespace LiteCharms.Features.Quartz; -public class RetryJobListener : IJobListener +public sealed class RetryJobListener : IJobListener { public string Name => "RetryJobListener"; @@ -12,6 +12,9 @@ public class RetryJobListener : IJobListener public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default) { + if (context.CancellationToken.IsCancellationRequested) + return; + if (jobException is not null && context.RefireCount < RetryCount) jobException.RefireImmediately = true; } diff --git a/LiteCharms.Features/Quotes/Commands/AssignQuoteToOrderCommand.cs b/LiteCharms.Features/Quotes/Commands/AssignQuoteToOrderCommand.cs deleted file mode 100644 index 498aa4a..0000000 --- a/LiteCharms.Features/Quotes/Commands/AssignQuoteToOrderCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.Quotes.Commands; - -public class AssignQuoteToOrderCommand : IRequest -{ - public Guid OrderId { get; set; } - - public Guid QuoteId { get; set; } - - private AssignQuoteToOrderCommand(Guid orderId, Guid quoteId) - { - OrderId = orderId; - QuoteId = quoteId; - } - - public static AssignQuoteToOrderCommand Create(Guid orderId, Guid quoteId) - { - if(orderId == Guid.Empty) - throw new ArgumentException("Order ID is required.", nameof(orderId)); - - if(quoteId == Guid.Empty) - throw new ArgumentException("Quote ID is required.", nameof(quoteId)); - - return new AssignQuoteToOrderCommand(orderId, quoteId); - } -} diff --git a/LiteCharms.Features/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs b/LiteCharms.Features/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs deleted file mode 100644 index 61c0902..0000000 --- a/LiteCharms.Features/Quotes/Commands/AssignQuoteToShoppingCartCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.Quotes.Commands; - -public class AssignQuoteToShoppingCartCommand : IRequest -{ - public Guid QuoteId { get; set; } - - public Guid ShoppingCartId { get; set; } - - private AssignQuoteToShoppingCartCommand(Guid quoteId, Guid shoppingCartId) - { - QuoteId = quoteId; - ShoppingCartId = shoppingCartId; - } - - public static AssignQuoteToShoppingCartCommand Create(Guid quoteId, Guid shoppingCartId) - { - if(quoteId == Guid.Empty) - throw new ArgumentException("QuoteId cannot be empty.", nameof(quoteId)); - - if (shoppingCartId == Guid.Empty) - throw new ArgumentException("ShoppingCartId cannot be empty.", nameof(shoppingCartId)); - - return new(quoteId, shoppingCartId); - } -} diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs b/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs deleted file mode 100644 index 4178e6b..0000000 --- a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToOrderCommandHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Quotes.Commands.Handlers; - -public class AssignQuoteToOrderCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(AssignQuoteToOrderCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken); - - if (order is null) - return Result.Fail(new Error($"Order with id {request.OrderId} not found")); - - if(!await context.Quotes.AnyAsync(q => q.Id == request.OrderId, cancellationToken)) - return Result.Fail(new Error($"Quote with id {request.QuoteId} not found")); - - if(order.QuoteId == request.QuoteId) - return Result.Fail(new Error($"Quote with id {request.QuoteId} is already assigned to order with id {request.OrderId}")); - - order.QuoteId = request.QuoteId; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error($"Failed to assign quote with id {request.QuoteId} to order with id {request.OrderId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs b/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs deleted file mode 100644 index 8638079..0000000 --- a/LiteCharms.Features/Quotes/Commands/Handlers/AssignQuoteToShoppingCartCommandHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Quotes.Commands.Handlers; - -public class AssignQuoteToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(AssignQuoteToShoppingCartCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var shoppingCart = await context.Orders.FirstOrDefaultAsync(o => o.Id == request.ShoppingCartId, cancellationToken); - - if (shoppingCart is null) - return Result.Fail(new Error($"ShoppingCart with id {request.ShoppingCartId} not found")); - - if(!await context.Quotes.AnyAsync(q => q.Id == request.QuoteId, cancellationToken)) - return Result.Fail(new Error($"Quote with id {request.QuoteId} not found")); - - shoppingCart.QuoteId = request.QuoteId; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error("Failed to assign quote to shopping cart")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs b/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs deleted file mode 100644 index 70c5191..0000000 --- a/LiteCharms.Features/Quotes/Commands/Handlers/UpdateQuoteStatusCommandHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Quotes.Commands.Handlers; - -public class UpdateQuoteStatusCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateQuoteStatusCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var quote = await context.Quotes.FirstOrDefaultAsync(q => q.Id == request.QuoteId, cancellationToken); - - if (quote is null) - return Result.Fail(new Error("Quote not found.")); - - quote.Status = request.Status; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail(new Error("Failed to update quote status.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Quotes/Commands/UpdateQuoteStatusCommand.cs b/LiteCharms.Features/Quotes/Commands/UpdateQuoteStatusCommand.cs deleted file mode 100644 index 901bc46..0000000 --- a/LiteCharms.Features/Quotes/Commands/UpdateQuoteStatusCommand.cs +++ /dev/null @@ -1,24 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Quotes.Commands; - -public class UpdateQuoteStatusCommand : IRequest -{ - public Guid QuoteId { get; set; } - - public QuoteStatus Status { get; set; } - - private UpdateQuoteStatusCommand(Guid quoteId, QuoteStatus status) - { - QuoteId = quoteId; - Status = status; - } - - public static UpdateQuoteStatusCommand Create(Guid quoteId, QuoteStatus status) - { - if(quoteId == Guid.Empty) - throw new ArgumentException("Quote ID cannot be empty.", nameof(quoteId)); - - return new(quoteId, status); - } -} diff --git a/LiteCharms.Features/Quotes/Queries/GetCustomerQuotesQuery.cs b/LiteCharms.Features/Quotes/Queries/GetCustomerQuotesQuery.cs deleted file mode 100644 index d531b3a..0000000 --- a/LiteCharms.Features/Quotes/Queries/GetCustomerQuotesQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Quotes.Queries; - -public class GetCustomerQuotesQuery : IRequest> -{ - public Guid CustomerId { get; set; } - - private GetCustomerQuotesQuery(Guid customerId) => CustomerId = customerId; - - public static GetCustomerQuotesQuery Create(Guid customerId) - { - if (customerId == Guid.Empty) - throw new ArgumentException("CustomerId is required."); - - return new(customerId); - } -} \ No newline at end of file diff --git a/LiteCharms.Features/Quotes/Queries/GetQuoteQuery.cs b/LiteCharms.Features/Quotes/Queries/GetQuoteQuery.cs deleted file mode 100644 index 0f566cf..0000000 --- a/LiteCharms.Features/Quotes/Queries/GetQuoteQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Quotes.Queries; - -public class GetQuoteQuery : IRequest> -{ - public Guid QuoteId { get; set; } - - private GetQuoteQuery(Guid quoteId) => QuoteId = quoteId; - - public static GetQuoteQuery Create(Guid quoteId) - { - if(quoteId == Guid.Empty) - throw new ArgumentException("Quote ID is required.", nameof(quoteId)); - - return new(quoteId); - } -} diff --git a/LiteCharms.Features/Quotes/Queries/GetQuotesQuery.cs b/LiteCharms.Features/Quotes/Queries/GetQuotesQuery.cs deleted file mode 100644 index 20a8d4b..0000000 --- a/LiteCharms.Features/Quotes/Queries/GetQuotesQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Quotes.Queries; - -public class GetQuotesQuery : IRequest> -{ - public DateOnly From { get; set; } - - public DateOnly To { get; set; } - - public int MaxRecords { get; set; } - - private GetQuotesQuery(DateOnly from, DateOnly to, int maxRecords = 1000) - { - From = from; - To = to; - MaxRecords = maxRecords; - } - - public static GetQuotesQuery Create(DateOnly from, DateOnly to, int maxRecords = 1000) - { - if (from > to) - throw new ArgumentException("From date cannot be greater than To date."); - - if (maxRecords <= 0) - throw new ArgumentException("MaxRecords must be a positive integer."); - - return new(from, to, maxRecords); - } -} diff --git a/LiteCharms.Features/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs b/LiteCharms.Features/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs deleted file mode 100644 index 3243924..0000000 --- a/LiteCharms.Features/Quotes/Queries/Handlers/GetCustomerQuotesQueryHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Quotes.Queries; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Quotes.Queries.Handlers; - -public class GetCustomerQuotesQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerQuotesQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer with Id {request.CustomerId} does not exist.")); - - var quotes = await context.Quotes.AsNoTracking() - .Where(q => q.CustomerId == request.CustomerId).ToArrayAsync(cancellationToken); - - return quotes?.Length > 0 - ? Result.Ok(quotes.Select(q => q.ToModel()).ToArray()) - : Result.Fail(new Error($"No quotes found for customer with Id {request.CustomerId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs b/LiteCharms.Features/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs deleted file mode 100644 index d8343cd..0000000 --- a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuoteQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Quotes.Queries.Handlers; - -public class GetQuoteQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetQuoteQuery request, CancellationToken cancellationToken) - { - try - { - await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var quote = await context.Quotes.AsNoTracking().FirstOrDefaultAsync(q => q.Id == request.QuoteId, cancellationToken); - - return quote is not null - ? Result.Ok(quote.ToModel()) - : Result.Fail(new Error($"Quote with ID {request.QuoteId} not found.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuotesHandler.cs b/LiteCharms.Features/Quotes/Queries/Handlers/GetQuotesHandler.cs deleted file mode 100644 index adfae5e..0000000 --- a/LiteCharms.Features/Quotes/Queries/Handlers/GetQuotesHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Quotes.Queries.Handlers; - -public class GetQuotesHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetQuotesQuery request, CancellationToken cancellationToken) - { - try - { - var fromDate = request.From.ToDateTime(TimeOnly.MinValue); - var toDate = request.To.ToDateTime(TimeOnly.MaxValue); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var quotes = await context.Quotes.AsNoTracking() - .OrderByDescending(o => o.CreatedAt) - .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate) - .Take(request.MaxRecords) - .ToArrayAsync(cancellationToken); - - return quotes?.Length > 0 - ? Result.Ok(quotes.Select(o => o.ToModel()).ToArray()) - : Result.Fail(new Error($"No quotes found for the specified date range {request.From} - {request.To}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs b/LiteCharms.Features/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs deleted file mode 100644 index 0b52018..0000000 --- a/LiteCharms.Features/Refunds/Commands/Handlers/RefundCustomerCommandHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Refunds.Commands.Handlers; - -public class RefundCustomerCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(RefundCustomerCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if(!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) - return Result.Fail(new Error($"Order with Id: {request.OrderId} does not exist")); - - if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer with Id: {request.CustomerId} does not exist")); - - if(!await context.Orders.AnyAsync(o => o.Id == request.OrderId && o.CustomerId == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Order with Id: {request.OrderId} does not belong to Customer with Id: {request.CustomerId}")); - - var refund = context.OrderRefunds.Add(new Entities.OrderRefund - { - OrderId = request.OrderId, - Reason = request.Reason, - Amount = request.Amount - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(refund.Entity.Id) - : Result.Fail(new Error($"Failed to create refund for OrderId: {request.OrderId}")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs b/LiteCharms.Features/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs deleted file mode 100644 index de1bf04..0000000 --- a/LiteCharms.Features/Refunds/Commands/Handlers/UpdateOrderRefundCommandHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.Refunds.Commands.Handlers; - -public class UpdateOrderRefundCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateOrderRefundCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var refund = await context.OrderRefunds.FirstOrDefaultAsync(r => r.Id == request.OrderRefundId, cancellationToken); - - if (refund is null) - return Result.Fail($"Order refund not found with id {request.OrderRefundId}"); - - refund.Reason = request.Reason; - refund.Amount = request.Amount; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to update order refund {request.OrderRefundId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Refunds/Commands/RefundCustomerCommand.cs b/LiteCharms.Features/Refunds/Commands/RefundCustomerCommand.cs deleted file mode 100644 index ccfda6e..0000000 --- a/LiteCharms.Features/Refunds/Commands/RefundCustomerCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace LiteCharms.Features.Refunds.Commands; - -public class RefundCustomerCommand : IRequest> -{ - public Guid OrderId { get; set; } - - public Guid CustomerId { get; set; } - - public string? Reason { get; set; } - - public decimal Amount { get; set; } - - private RefundCustomerCommand(Guid orderId, Guid customerId, string? reason, decimal amount) - { - OrderId = orderId; - CustomerId = customerId; - Reason = reason; - Amount = amount; - CustomerId = customerId; - } - - public static RefundCustomerCommand Create(Guid orderId, Guid customerId, string? reason, decimal amount) - { - if (orderId == Guid.Empty) - throw new ArgumentException("OrderId is required", nameof(orderId)); - - if (customerId == Guid.Empty) - throw new ArgumentException("CustomerId is required", nameof(customerId)); - - if (amount <= 0) - throw new ArgumentException("Amount must be greater than zero", nameof(amount)); - - if (string.IsNullOrWhiteSpace(reason)) - throw new ArgumentException("Reason is required", nameof(reason)); - - return new(orderId, customerId, reason, amount); - } -} diff --git a/LiteCharms.Features/Refunds/Commands/UpdateOrderRefundCommand.cs b/LiteCharms.Features/Refunds/Commands/UpdateOrderRefundCommand.cs deleted file mode 100644 index bd9ab59..0000000 --- a/LiteCharms.Features/Refunds/Commands/UpdateOrderRefundCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace LiteCharms.Features.Refunds.Commands; - -public class UpdateOrderRefundCommand : IRequest -{ - public Guid OrderRefundId { get; set; } - - public string? Reason { get; set; } - - public decimal Amount { get; set; } - - private UpdateOrderRefundCommand(Guid orderRefundId, string? reason, decimal amount) - { - OrderRefundId = orderRefundId; - Reason = reason; - Amount = amount; - } - - public static UpdateOrderRefundCommand Create(Guid orderRefundId, string? reason, decimal amount) - { - if (orderRefundId == Guid.Empty) - throw new ArgumentException("Order refund id is required.", nameof(orderRefundId)); - - if (string.IsNullOrWhiteSpace(reason)) - throw new ArgumentException("Refund update reason is required"); - - return new(orderRefundId, reason, amount); - } -} diff --git a/LiteCharms.Features/Refunds/Queries/GetCustomerRefundsQuery.cs b/LiteCharms.Features/Refunds/Queries/GetCustomerRefundsQuery.cs deleted file mode 100644 index 24d2cc9..0000000 --- a/LiteCharms.Features/Refunds/Queries/GetCustomerRefundsQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Refunds.Queries; - -public class GetCustomerRefundsQuery : IRequest> -{ - public Guid CustomerId { get; set; } - - private GetCustomerRefundsQuery(Guid customerId) => CustomerId = customerId; - - public static GetCustomerRefundsQuery Create(Guid customerId) - { - if (customerId == Guid.Empty) - throw new ArgumentException("CustomerId is required.", nameof(customerId)); - - return new(customerId); - } -} diff --git a/LiteCharms.Features/Refunds/Queries/GetRefundQuery.cs b/LiteCharms.Features/Refunds/Queries/GetRefundQuery.cs deleted file mode 100644 index 9f4d375..0000000 --- a/LiteCharms.Features/Refunds/Queries/GetRefundQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.Refunds.Queries; - -public class GetRefundQuery : IRequest> -{ - public Guid OrderRefundId { get; set; } - - private GetRefundQuery(Guid orderRefundId) => OrderRefundId = orderRefundId; - - public static GetRefundQuery Create(Guid orderRefundId) - { - if(orderRefundId == Guid.Empty) - throw new ArgumentException("Customer ID is required.", nameof(orderRefundId)); - - return new(orderRefundId); - } -} diff --git a/LiteCharms.Features/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs b/LiteCharms.Features/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs deleted file mode 100644 index 34b9f21..0000000 --- a/LiteCharms.Features/Refunds/Queries/Handlers/GetCustomerRefundsQueryHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.Refunds.Queries; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Refunds.Queries.Handlers; - -public class GetCustomerRefundsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerRefundsQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if(!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer with Id {request.CustomerId} does not exist.")); - - var refunds = await context.OrderRefunds.AsNoTracking().AsSplitQuery() - .OrderByDescending(o => o.CreatedAt) - .Where(r => r.Order!.CustomerId == request.CustomerId).ToArrayAsync(cancellationToken); - - return refunds?.Length > 0 - ? Result.Ok(refunds.Select(r => r.ToModel()).ToArray()) - : Result.Fail(new Error($"No refunds found for customer with Id {request.CustomerId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Refunds/Queries/Handlers/GetRefundQueryHandler.cs b/LiteCharms.Features/Refunds/Queries/Handlers/GetRefundQueryHandler.cs deleted file mode 100644 index 29bd3e5..0000000 --- a/LiteCharms.Features/Refunds/Queries/Handlers/GetRefundQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.Refunds.Queries.Handlers; - -public class GetRefundQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetRefundQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var refund = await context.OrderRefunds.AsNoTracking().FirstOrDefaultAsync(r => r.Id == request.OrderRefundId, cancellationToken); - - return refund is not null - ? Result.Ok(refund.ToModel()) - : Result.Fail($"Order refund could not be found with id {request.OrderRefundId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/S3/Abstractions/IS3Service.cs b/LiteCharms.Features/S3/Abstractions/IS3Service.cs new file mode 100644 index 0000000..4c0cdb5 --- /dev/null +++ b/LiteCharms.Features/S3/Abstractions/IS3Service.cs @@ -0,0 +1,7 @@ +namespace LiteCharms.Features.S3.Abstractions; + +public interface IS3Service +{ + Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default); + Task DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default); +} diff --git a/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs new file mode 100644 index 0000000..3fcb5d8 --- /dev/null +++ b/LiteCharms.Features/S3/Abstractions/S3ServiceBase.cs @@ -0,0 +1,83 @@ +using LiteCharms.Features.Hasher; + +namespace LiteCharms.Features.S3.Abstractions; + +public abstract class S3ServiceBase(IAmazonS3 amazonS3) +{ + protected readonly IAmazonS3 Client = amazonS3; + + protected abstract string BucketName { get; } + protected abstract string CdnBaseUrl { get; } + + public virtual async Task> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) + { + try + { + if (string.IsNullOrWhiteSpace(BucketName)) + return Result.Fail("Bucket name is not configured."); + + if (string.IsNullOrWhiteSpace(CdnBaseUrl)) + return Result.Fail("CDN base URL is not configured."); + + using var stream = new MemoryStream(); + + await fileStream.CopyToAsync(stream, cancellationToken); + await fileStream.DisposeAsync(); + + stream.Seek(0, SeekOrigin.Begin); + + var fileHash = HashService.StreamToSha256Hash(stream); + + if(string.IsNullOrWhiteSpace(fileHash)) + return Result.Fail("Failed to compute file hash."); + + var fileKey = $"{fileHash.ToLower(CultureInfo.InvariantCulture)}{Path.GetExtension(fileName)}"; + + var putRequest = new PutObjectRequest + { + BucketName = BucketName, + Key = fileKey, + InputStream = stream, + ContentType = contentType, + UseChunkEncoding = false, + }; + + stream.Seek(0, SeekOrigin.Begin); + + var response = await Client.PutObjectAsync(putRequest, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.OK + ? Result.Fail($"Failed to upload {fileName} to S3.") + : Result.Ok($"{CdnBaseUrl}/{fileKey}"); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex)); + } + } + + public virtual async Task DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) + { + try + { + if (string.IsNullOrWhiteSpace(BucketName)) + return Result.Fail("Bucket name is not configured."); + + var deleteRequest = new DeleteObjectRequest + { + BucketName = BucketName, + Key = fileKey + }; + + var response = await Client.DeleteObjectAsync(deleteRequest, cancellationToken); + + return response.HttpStatusCode != System.Net.HttpStatusCode.NoContent + ? Result.Fail($"Failed to delete {fileKey} from S3.") + : Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error($"Error deleting {fileKey} from S3: {ex.Message}").CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs b/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs new file mode 100644 index 0000000..5f4ddac --- /dev/null +++ b/LiteCharms.Features/S3/BookshopInvoicesS3Service.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.S3.Abstractions; +using static LiteCharms.Features.S3.Constants; + +namespace LiteCharms.Features.S3; + +public sealed class BookshopInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopInvoicesBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + protected override string BucketName => configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:BucketName").Value ?? ""; + protected override string CdnBaseUrl => configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:CdnBaseUrl").Value ?? ""; +} diff --git a/LiteCharms.Features/S3/BookshopQuotesS3Service.cs b/LiteCharms.Features/S3/BookshopQuotesS3Service.cs new file mode 100644 index 0000000..2362d66 --- /dev/null +++ b/LiteCharms.Features/S3/BookshopQuotesS3Service.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.S3.Abstractions; +using static LiteCharms.Features.S3.Constants; + +namespace LiteCharms.Features.S3; + +public sealed class BookshopQuotesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopQuotesBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + protected override string BucketName => configuration.GetSection($"{BookshopQuotesS3SettingsSection}:BucketName").Value ?? ""; + protected override string CdnBaseUrl => configuration.GetSection($"{BookshopQuotesS3SettingsSection}:CdnBaseUrl").Value ?? ""; +} diff --git a/LiteCharms.Features/S3/BookshopS3Service.cs b/LiteCharms.Features/S3/BookshopS3Service.cs new file mode 100644 index 0000000..024b829 --- /dev/null +++ b/LiteCharms.Features/S3/BookshopS3Service.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.S3.Abstractions; +using static LiteCharms.Features.S3.Constants; + +namespace LiteCharms.Features.S3; + +public sealed class BookshopS3Service(IConfiguration configuration, [FromKeyedServices(BookshopBucketName)] IAmazonS3 amazonS3) : + S3ServiceBase(amazonS3), IS3Service +{ + protected override string BucketName => configuration.GetSection($"{BookshopS3SettingsSection}:BucketName").Value ?? ""; + protected override string CdnBaseUrl => configuration.GetSection($"{BookshopS3SettingsSection}:CdnBaseUrl").Value ?? ""; +} diff --git a/LiteCharms.Features/S3/Configuration/S3Settings.cs b/LiteCharms.Features/S3/Configuration/S3Settings.cs new file mode 100644 index 0000000..72984fa --- /dev/null +++ b/LiteCharms.Features/S3/Configuration/S3Settings.cs @@ -0,0 +1,16 @@ +namespace LiteCharms.Features.S3.Configuration; + +public sealed class S3Settings +{ + public string? ServiceUrl { get; set; } + + public string? AccessKey { get; set; } + + public string? SecretKey { get; set; } + + public string? BucketName { get; set; } + + public string? Region { get; set; } + + public string? CdnBaseUrl { get; set; } +} diff --git a/LiteCharms.Features/S3/Constants.cs b/LiteCharms.Features/S3/Constants.cs new file mode 100644 index 0000000..3da4501 --- /dev/null +++ b/LiteCharms.Features/S3/Constants.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.S3; + +public static class Constants +{ + public const string BookshopS3SettingsSection = "BookshopS3Settings"; + public const string BookshopInvoicesS3SettingsSection = "BookshopInvoicesS3Settings"; + public const string BookshopQuotesS3SettingsSection = "BookshopQuotesS3Settings"; + + public const string BookshopBucketName = "bookshop"; + public const string BookshopInvoicesBucketName = "bookshop.invoices"; + public const string BookshopQuotesBucketName = "bookshop.quotes"; +} diff --git a/LiteCharms.Abstractions/EventBusQueueBase.cs b/LiteCharms.Features/ServiceBus/Abstractions/EventBusQueueBase.cs similarity index 73% rename from LiteCharms.Abstractions/EventBusQueueBase.cs rename to LiteCharms.Features/ServiceBus/Abstractions/EventBusQueueBase.cs index 29f7265..31391af 100644 --- a/LiteCharms.Abstractions/EventBusQueueBase.cs +++ b/LiteCharms.Features/ServiceBus/Abstractions/EventBusQueueBase.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Abstractions; +using LiteCharms.Features.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Abstractions; public abstract class EventBusQueueBase { diff --git a/LiteCharms.Abstractions/IEventBus.cs b/LiteCharms.Features/ServiceBus/Abstractions/IEventBus.cs similarity index 64% rename from LiteCharms.Abstractions/IEventBus.cs rename to LiteCharms.Features/ServiceBus/Abstractions/IEventBus.cs index f40d372..4f3ebb0 100644 --- a/LiteCharms.Abstractions/IEventBus.cs +++ b/LiteCharms.Features/ServiceBus/Abstractions/IEventBus.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Abstractions; +using LiteCharms.Features.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Abstractions; public interface IEventBus { diff --git a/LiteCharms.Abstractions/IEventBusQueue.cs b/LiteCharms.Features/ServiceBus/Abstractions/IEventBusQueue.cs similarity index 56% rename from LiteCharms.Abstractions/IEventBusQueue.cs rename to LiteCharms.Features/ServiceBus/Abstractions/IEventBusQueue.cs index 98750b4..1877555 100644 --- a/LiteCharms.Abstractions/IEventBusQueue.cs +++ b/LiteCharms.Features/ServiceBus/Abstractions/IEventBusQueue.cs @@ -1,4 +1,6 @@ -namespace LiteCharms.Abstractions; +using LiteCharms.Features.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Abstractions; public interface IEventBusQueue { diff --git a/LiteCharms.Abstractions/Constants.cs b/LiteCharms.Features/ServiceBus/Constants.cs similarity index 86% rename from LiteCharms.Abstractions/Constants.cs rename to LiteCharms.Features/ServiceBus/Constants.cs index 6f533d2..ff8d4fe 100644 --- a/LiteCharms.Abstractions/Constants.cs +++ b/LiteCharms.Features/ServiceBus/Constants.cs @@ -1,4 +1,4 @@ -namespace LiteCharms.Abstractions; +namespace LiteCharms.Features.ServiceBus; public static class Constants { diff --git a/LiteCharms.Infrastructure/ServiceBus/EmailServiceBus.cs b/LiteCharms.Features/ServiceBus/EmailServiceBus.cs similarity index 59% rename from LiteCharms.Infrastructure/ServiceBus/EmailServiceBus.cs rename to LiteCharms.Features/ServiceBus/EmailServiceBus.cs index 5205283..f83ed8e 100644 --- a/LiteCharms.Infrastructure/ServiceBus/EmailServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/EmailServiceBus.cs @@ -1,9 +1,10 @@ -using LiteCharms.Abstractions; -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.ServiceBus.Abstractions; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Infrastructure.ServiceBus; +namespace LiteCharms.Features.ServiceBus; -public class EmailServiceBus(EmailQueue messages) : IEventBus +public sealed class EmailServiceBus(EmailQueue messages) : IEventBus { public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) where TEvent : class, IEvent diff --git a/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs new file mode 100644 index 0000000..5a74145 --- /dev/null +++ b/LiteCharms.Features/ServiceBus/Exchanges/EmailExchange.cs @@ -0,0 +1,33 @@ +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.ServiceBus.Queues; + +namespace LiteCharms.Features.ServiceBus.Exchanges; + +public sealed class EmailExchange(EmailQueue messages, ILogger logger, IPublisher mediator) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await foreach(IEvent? message in messages.Incoming.ReadAllAsync(stoppingToken)) + { + try + { + switch (message.Name) + { + case "SendShopEmailEnquiryEvent": + await mediator.Publish(message, stoppingToken); + break; + case "ProcessEmailNotificationsEvent": + await mediator.Publish(message, stoppingToken); + break; + default: + logger.LogWarning("Unsupported email event {Event}", message.Name); + break; + } + } + catch (Exception ex) + { + logger.LogError(ex, ex.Message); + } + } + } +} diff --git a/LiteCharms.Infrastructure/ServiceBus/Exchanges/GeneralExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs similarity index 50% rename from LiteCharms.Infrastructure/ServiceBus/Exchanges/GeneralExchange.cs rename to LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs index f21d566..32d84f1 100644 --- a/LiteCharms.Infrastructure/ServiceBus/Exchanges/GeneralExchange.cs +++ b/LiteCharms.Features/ServiceBus/Exchanges/GeneralExchange.cs @@ -1,8 +1,8 @@ -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Infrastructure.ServiceBus.Exchanges; +namespace LiteCharms.Features.ServiceBus.Exchanges; -public class GeneralExchange(GeneralQueue messages) : BackgroundService +public sealed class GeneralExchange(GeneralQueue messages) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/LiteCharms.Infrastructure/ServiceBus/Exchanges/SalesExchange.cs b/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs similarity index 51% rename from LiteCharms.Infrastructure/ServiceBus/Exchanges/SalesExchange.cs rename to LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs index 6288734..3705993 100644 --- a/LiteCharms.Infrastructure/ServiceBus/Exchanges/SalesExchange.cs +++ b/LiteCharms.Features/ServiceBus/Exchanges/SalesExchange.cs @@ -1,8 +1,8 @@ -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Infrastructure.ServiceBus.Exchanges; +namespace LiteCharms.Features.ServiceBus.Exchanges; -public class SalesExchange(SalesQueue messages) : BackgroundService +public sealed class SalesExchange(SalesQueue messages) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/LiteCharms.Infrastructure/ServiceBus/GeneralServiceBus.cs b/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs similarity index 63% rename from LiteCharms.Infrastructure/ServiceBus/GeneralServiceBus.cs rename to LiteCharms.Features/ServiceBus/GeneralServiceBus.cs index 28d6088..0694c5c 100644 --- a/LiteCharms.Infrastructure/ServiceBus/GeneralServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/GeneralServiceBus.cs @@ -1,9 +1,10 @@ -using LiteCharms.Abstractions; -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.ServiceBus.Abstractions; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Infrastructure.ServiceBus; +namespace LiteCharms.Features.ServiceBus; -public class GeneralServiceBus(GeneralQueue messages) : IEventBus +public sealed class GeneralServiceBus(GeneralQueue messages) : IEventBus { public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) where TEvent : class, IEvent diff --git a/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs b/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs new file mode 100644 index 0000000..700a1f1 --- /dev/null +++ b/LiteCharms.Features/ServiceBus/Queues/EmailQueue.cs @@ -0,0 +1,5 @@ +using LiteCharms.Features.ServiceBus.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Queues; + +public sealed class EmailQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs b/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs new file mode 100644 index 0000000..ef155ec --- /dev/null +++ b/LiteCharms.Features/ServiceBus/Queues/GeneralQueue.cs @@ -0,0 +1,5 @@ +using LiteCharms.Features.ServiceBus.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Queues; + +public sealed class GeneralQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs b/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs new file mode 100644 index 0000000..bb76de8 --- /dev/null +++ b/LiteCharms.Features/ServiceBus/Queues/SalesQueue.cs @@ -0,0 +1,5 @@ +using LiteCharms.Features.ServiceBus.Abstractions; + +namespace LiteCharms.Features.ServiceBus.Queues; + +public sealed class SalesQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Infrastructure/ServiceBus/SalesServiceBus.cs b/LiteCharms.Features/ServiceBus/SalesServiceBus.cs similarity index 63% rename from LiteCharms.Infrastructure/ServiceBus/SalesServiceBus.cs rename to LiteCharms.Features/ServiceBus/SalesServiceBus.cs index bb3412c..d30050c 100644 --- a/LiteCharms.Infrastructure/ServiceBus/SalesServiceBus.cs +++ b/LiteCharms.Features/ServiceBus/SalesServiceBus.cs @@ -1,9 +1,10 @@ -using LiteCharms.Abstractions; -using LiteCharms.Infrastructure.ServiceBus.Queues; +using LiteCharms.Features.Abstractions; +using LiteCharms.Features.ServiceBus.Abstractions; +using LiteCharms.Features.ServiceBus.Queues; -namespace LiteCharms.Infrastructure.ServiceBus; +namespace LiteCharms.Features.ServiceBus; -public class SalesServiceBus(SalesQueue messages) : IEventBus +public sealed class SalesServiceBus(SalesQueue messages) : IEventBus { public async Task PublishAsync(TEvent notification, CancellationToken cancellationToken = default) where TEvent : class, IEvent diff --git a/LiteCharms.Features/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs b/LiteCharms.Features/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs deleted file mode 100644 index 009fccd..0000000 --- a/LiteCharms.Features/ShoppingCarts/Commands/AddItemToShoppingCartCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class AddItemToShoppingCartCommand : IRequest -{ - public Guid ShoppingCartId { get; set; } - - public Guid ProductPriceId { get; set; } - - public int Quantity { get; set; } - - private AddItemToShoppingCartCommand(Guid shoppingCartId, Guid productPriceId, int quantity = 1) - { - ShoppingCartId = shoppingCartId; - ProductPriceId = productPriceId; - Quantity = quantity; - } - - public static AddItemToShoppingCartCommand Create(Guid shoppingCartId, Guid productPriceId, int quantity = 1) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); - - if (productPriceId == Guid.Empty) - throw new ArgumentException($"Product item required", nameof(productPriceId)); - - if(quantity <= 0) throw new ArgumentException($"Quantity must be at least 1", nameof(quantity)); - - return new(shoppingCartId, productPriceId, quantity); - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/CreateShoppingCartCommand.cs b/LiteCharms.Features/ShoppingCarts/Commands/CreateShoppingCartCommand.cs deleted file mode 100644 index fdc7b0e..0000000 --- a/LiteCharms.Features/ShoppingCarts/Commands/CreateShoppingCartCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class CreateShoppingCartCommand : IRequest> -{ - public Guid? CustomerId { get; set; } - - public Guid? OrderId { get; set; } - - public Guid? QuoteId { get; set; } - - private CreateShoppingCartCommand(Guid customerId, Guid? orderId = null, Guid? quoteId = null) - { - CustomerId = customerId; - OrderId = orderId; - QuoteId = quoteId; - } - - public static CreateShoppingCartCommand Create(Guid customerId, Guid? orderId = null, Guid? quoteId = null) - { - if (customerId == Guid.Empty) - throw new ArgumentException($"Customer ID is required", nameof(customerId)); - - return new(customerId, orderId, quoteId); - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs b/LiteCharms.Features/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs deleted file mode 100644 index f535ca4..0000000 --- a/LiteCharms.Features/ShoppingCarts/Commands/EmptyShoppingCartCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class EmptyShoppingCartCommand : IRequest -{ - public Guid ShoppingCartId { get; set; } - - private EmptyShoppingCartCommand(Guid shoppingCartId) => ShoppingCartId = shoppingCartId; - - public static EmptyShoppingCartCommand Create(Guid shoppingCartId) - { - if(shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart ID is required.", nameof(shoppingCartId)); - - return new(shoppingCartId); - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs deleted file mode 100644 index 3490517..0000000 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/AddItemToShoppingCartCommandHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class AddItemToShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(AddItemToShoppingCartCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ProductPrices.AnyAsync(c => c.Id == request.ProductPriceId, cancellationToken)) - return Result.Fail($"Product item could not be found with id {request.ProductPriceId}"); - - var cart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == request.ShoppingCartId, cancellationToken); - - if (cart is null) - return Result.Fail($"Shopping cart could not be found with id {request.ShoppingCartId}"); - - if (cart.ShoppingCartItems?.Any(i => i.ProductPriceId == request.ProductPriceId) == true) - return Result.Fail($"Item already in shopping cart with id {request.ShoppingCartId}"); - - context.ShoppingCartItems.Add(new Entities.ShoppingCartItem - { - ShoppingCartId = request.ShoppingCartId, - ProductPriceId = request.ProductPriceId, - Quantity = request.Quantity - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to add cart item with id {request.ProductPriceId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs deleted file mode 100644 index c6d17b0..0000000 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/CreateShoppingCartCommandHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class CreateShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(CreateShoppingCartCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail($"Customer could not be found with id {request.CustomerId}"); - - var cart = context.ShoppingCarts.Add(new Entities.ShoppingCart - { - CustomerId = request.CustomerId, - OrderId = request.OrderId, - QuoteId = request.QuoteId - }); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok(cart.Entity.Id) - : Result.Fail($"Failed to create shopping cart for customer id {request.CustomerId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs deleted file mode 100644 index 2787a8c..0000000 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/EmptyShoppingCartCommandHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class EmptyShoppingCartCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(EmptyShoppingCartCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ShoppingCarts.AnyAsync(c => c.Id == request.ShoppingCartId, cancellationToken)) - return Result.Fail($"Shopping could not be found with id {request.ShoppingCartId}"); - - if (await context.ShoppingCartItems.CountAsync(i => i.ShoppingCartId == request.ShoppingCartId, cancellationToken) == 0) - return Result.Ok(); - - var cartItems = await context.ShoppingCartItems.Where(i => i.ShoppingCartId == request.ShoppingCartId).ToListAsync(cancellationToken); - - context.RemoveRange(cartItems); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Could not empty cart with id {request.ShoppingCartId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs deleted file mode 100644 index 1d7944d..0000000 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/RemoveShoppingCartItemCommandHandler.cs +++ /dev/null @@ -1,36 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class RemoveShoppingCartItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(RemoveShoppingCartItemCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ProductPrices.AnyAsync(c => c.Id == request.ShoppingCartItemId, cancellationToken)) - return Result.Fail($"Product item could not be found with id {request.ShoppingCartItemId}"); - - var cart = await context.ShoppingCarts.FirstOrDefaultAsync(c => c.Id == request.ShoppingCartId, cancellationToken); - - if (cart is null) - return Result.Fail($"Shopping cart item could not be found with id {request.ShoppingCartId}"); - - var item = await context.ShoppingCartItems.FirstOrDefaultAsync(i => i.Id == request.ShoppingCartItemId, cancellationToken); - - if (item is null) return Result.Ok(); - - context.ShoppingCartItems.Remove(item); - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to remove shopping cart item with id {request.ShoppingCartItemId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs b/LiteCharms.Features/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs deleted file mode 100644 index b1880ec..0000000 --- a/LiteCharms.Features/ShoppingCarts/Commands/Handlers/UpdateShoppingCartItemCommandHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LiteCharms.Infrastructure.Database; - -namespace LiteCharms.Features.ShoppingCarts.Commands.Handlers; - -public class UpdateShoppingCartItemCommandHandler(IDbContextFactory contextFactory) : IRequestHandler -{ - public async ValueTask Handle(UpdateShoppingCartItemCommand request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ShoppingCarts.AnyAsync(c => c.Id == request.ShoppingCartId, cancellationToken)) - return Result.Fail($"Shopping could not be found with id {request.ShoppingCartId}"); - - var item = await context.ShoppingCartItems.FirstOrDefaultAsync(i => i.ShoppingCartId == request.ShoppingCartId, cancellationToken); - - if(item is null) - return Result.Fail($"Shopping cart item could not be found with id {request.ShoppingCartItemId}"); - - item.Quantity = request.Quantity; - - return await context.SaveChangesAsync(cancellationToken) > 0 - ? Result.Ok() - : Result.Fail($"Failed to update cart item quntity"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs b/LiteCharms.Features/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs deleted file mode 100644 index 26706d8..0000000 --- a/LiteCharms.Features/ShoppingCarts/Commands/RemoveShoppingCartItemCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class RemoveShoppingCartItemCommand : IRequest -{ - public Guid ShoppingCartId { get; set; } - - public Guid ShoppingCartItemId { get; set; } - - private RemoveShoppingCartItemCommand(Guid shoppingCartId, Guid shoppingCartItemId) - { - ShoppingCartId = shoppingCartId; - ShoppingCartItemId = shoppingCartItemId; - } - - public static RemoveShoppingCartItemCommand Create(Guid shoppingCartId, Guid shoppingCartItemId) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); - - if (shoppingCartItemId == Guid.Empty) - throw new ArgumentException($"Shopping cart item required", nameof(shoppingCartItemId)); - - return new(shoppingCartId, shoppingCartItemId); - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs b/LiteCharms.Features/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs deleted file mode 100644 index d7cebe0..0000000 --- a/LiteCharms.Features/ShoppingCarts/Commands/UpdateShoppingCartItemCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace LiteCharms.Features.ShoppingCarts.Commands; - -public class UpdateShoppingCartItemCommand : IRequest -{ - public Guid ShoppingCartId { get; set; } - - public Guid ShoppingCartItemId { get; set; } - - public int Quantity { get; set; } - - private UpdateShoppingCartItemCommand(Guid shoppingCartId, Guid shoppingCartItemId, int quantity = 1) - { - ShoppingCartId = shoppingCartId; - ShoppingCartItemId = shoppingCartItemId; - Quantity = quantity; - } - - public static UpdateShoppingCartItemCommand Create(Guid shoppingCartId, Guid shoppingCartItemId, int quantity = 1) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart ID is required", nameof(shoppingCartId)); - - if (shoppingCartItemId == Guid.Empty) - throw new ArgumentException($"Shopping cart item is required", nameof(shoppingCartItemId)); - - if (quantity <= 0) throw new ArgumentException($"Quantity must be at least 1", nameof(quantity)); - - return new(shoppingCartId, shoppingCartItemId, quantity); - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs b/LiteCharms.Features/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs deleted file mode 100644 index 0d64287..0000000 --- a/LiteCharms.Features/ShoppingCarts/Queries/GetCustomerShoppingCartsQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries; - -public class GetCustomerShoppingCartsQuery : IRequest> -{ - public Guid CustomerId { get; set; } - - private GetCustomerShoppingCartsQuery(Guid customerId) => CustomerId = customerId; - - public static GetCustomerShoppingCartsQuery Create(Guid customerId) - { - if(customerId == Guid.Empty) - throw new ArgumentException("Customer ID is required.", nameof(customerId)); - - return new(customerId); - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs b/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs deleted file mode 100644 index 197d475..0000000 --- a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartItemsQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries; - -public class GetShoppingCartItemsQuery : IRequest> -{ - public Guid ShoppingCartId { get; set; } - - private GetShoppingCartItemsQuery(Guid shoppingCartId) => ShoppingCartId = shoppingCartId; - - public static GetShoppingCartItemsQuery Create(Guid shoppingCartId) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException("Shopping cart id is required", nameof(shoppingCartId)); - - return new(shoppingCartId); - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartQuery.cs b/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartQuery.cs deleted file mode 100644 index 07dfc5a..0000000 --- a/LiteCharms.Features/ShoppingCarts/Queries/GetShoppingCartQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using LiteCharms.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries; - -public class GetShoppingCartQuery : IRequest> -{ - public Guid ShoppingCartId { get; set; } - - private GetShoppingCartQuery(Guid shoppingCartId) => ShoppingCartId = shoppingCartId; - - public static GetShoppingCartQuery Create(Guid shoppingCartId) - { - if (shoppingCartId == Guid.Empty) - throw new ArgumentException($"Shopping cart id is required", nameof(shoppingCartId)); - - return new(shoppingCartId); - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs deleted file mode 100644 index d4a6885..0000000 --- a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetCustomerShoppingCartsQueryHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Features.ShoppingCarts.Queries; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; - -public class GetCustomerShoppingCartsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetCustomerShoppingCartsQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken)) - return Result.Fail(new Error($"Customer with Id {request.CustomerId} does not exist.")); - - var shoppingCarts = await context.ShoppingCarts.Where(sc => sc.CustomerId == request.CustomerId).ToArrayAsync(cancellationToken); - - return shoppingCarts?.Length > 0 - ? Result.Ok(shoppingCarts.Select(c => c.ToModel()).ToArray()) - : Result.Fail(new Error($"No shopping carts found for customer with Id {request.CustomerId}.")); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs deleted file mode 100644 index e523c25..0000000 --- a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartItemsQueryHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; - -public class GetShoppingCartItemsQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetShoppingCartItemsQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - if (!await context.ShoppingCarts.AnyAsync(i => i.Id == request.ShoppingCartId, cancellationToken)) - return Result.Fail($"Shopping cart could not be found with id {request.ShoppingCartId}"); - - var items = await context.ShoppingCartItems.AsNoTracking() - .Where(i => i.ShoppingCartId == request.ShoppingCartId).ToArrayAsync(cancellationToken); - - return items?.Length > 0 - ? Result.Ok(items.Select(i => i.ToModel()).ToArray()) - : Result.Fail($"Failed to retrieve shopping cart items with id {request.ShoppingCartId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs b/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs deleted file mode 100644 index d58ffc3..0000000 --- a/LiteCharms.Features/ShoppingCarts/Queries/Handlers/GetShoppingCartQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Extensions; -using LiteCharms.Infrastructure.Database; -using LiteCharms.Models; - -namespace LiteCharms.Features.ShoppingCarts.Queries.Handlers; - -public class GetShoppingCartQueryHandler(IDbContextFactory contextFactory) : IRequestHandler> -{ - public async ValueTask> Handle(GetShoppingCartQuery request, CancellationToken cancellationToken) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var cart = await context.ShoppingCarts.AsNoTracking().FirstOrDefaultAsync(c => c.Id == request.ShoppingCartId, cancellationToken); - - return cart is not null - ? Result.Ok(cart.ToModel()) - : Result.Fail($"Failed to find shopping cart with id {request.ShoppingCartId}"); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Utilities/Commands/ComputeHashCommand.cs b/LiteCharms.Features/Utilities/Commands/ComputeHashCommand.cs deleted file mode 100644 index ef235fc..0000000 --- a/LiteCharms.Features/Utilities/Commands/ComputeHashCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace LiteCharms.Features.Utilities.Commands; - -public class ComputeHashCommand : IRequest> -{ - public string? Input { get; set; } - - private ComputeHashCommand(string input) => Input = input; - - public static ComputeHashCommand Create(string input) - { - if(string.IsNullOrWhiteSpace(input)) - throw new ArgumentException("Input is required", nameof(input)); - - return new(input); - } -} diff --git a/LiteCharms.Features/Utilities/Commands/Handlers/ComputeHashCommandHandler.cs b/LiteCharms.Features/Utilities/Commands/Handlers/ComputeHashCommandHandler.cs deleted file mode 100644 index d4ef5b4..0000000 --- a/LiteCharms.Features/Utilities/Commands/Handlers/ComputeHashCommandHandler.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace LiteCharms.Features.Utilities.Commands.Handlers; - -public class ComputeHashCommandHandler : IRequestHandler> -{ - public async ValueTask> Handle(ComputeHashCommand request, CancellationToken cancellationToken) - { - try - { - var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(request.Input!)); - - return Result.Ok(Convert.ToHexString(bytes)); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Utilities/Commands/Handlers/SendEmailCommandHandler.cs b/LiteCharms.Features/Utilities/Commands/Handlers/SendEmailCommandHandler.cs deleted file mode 100644 index e3fcdf7..0000000 --- a/LiteCharms.Features/Utilities/Commands/Handlers/SendEmailCommandHandler.cs +++ /dev/null @@ -1,60 +0,0 @@ -using LiteCharms.Models.Configuraton.Email; - -namespace LiteCharms.Features.Utilities.Commands.Handlers; - -public class SendEmailCommandHandler(IOptions smtpOptions) : IRequestHandler -{ - public async ValueTask Handle(SendEmailCommand request, CancellationToken cancellationToken) - { - try - { - var settings = smtpOptions.Value; - - if(settings == null) - return Result.Fail(new Error("SMTP settings are not configured.")); - - if(settings.Credentials == null) - return Result.Fail(new Error("SMTP credentials are not configured.")); - - if(string.IsNullOrWhiteSpace(settings?.Credentials.Username) || string.IsNullOrWhiteSpace(settings.Credentials.Password)) - return Result.Fail(new Error("SMTP credentials are incomplete.")); - - if(string.IsNullOrWhiteSpace(settings.Host) || settings.Port == 0) - return Result.Fail(new Error("SMTP host and port must be configured.")); - - var message = new MimeMessage(); - message.From.Add(new MailboxAddress(request.SenderName, request.From!)); - message.To.Add(new MailboxAddress(request.RecipientName, request.To!)); - message.Subject = request.Subject!; - - var bodyBuilder = new BodyBuilder(); - - if(request.Attachment?.Length > 0 && !string.IsNullOrEmpty(request.AttachmentFileName)) - bodyBuilder.Attachments.Add(request.AttachmentFileName!, request.Attachment!, cancellationToken); - - if (!request.IsHtml) bodyBuilder.TextBody = request.Message; - if (request.IsHtml) bodyBuilder.HtmlBody = request.Message; - - message.Body = bodyBuilder.ToMessageBody(); - - using var client = new SmtpClient(); - - await client.ConnectAsync(settings.Host!, settings.Port, settings.UseSsl, cancellationToken); - await client.AuthenticateAsync(settings.Credentials!.Username!, settings.Credentials.Password!, cancellationToken); - - var response = await client.SendAsync(message, cancellationToken); - - bool emailSent = response.Contains("OK", StringComparison.InvariantCultureIgnoreCase); - - await client.DisconnectAsync(true, cancellationToken); - - return emailSent - ? Result.Ok() - : Result.Fail(new Error("Failed to send email. SMTP response: " + response)); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } -} diff --git a/LiteCharms.Features/Utilities/Commands/SendEmailCommand.cs b/LiteCharms.Features/Utilities/Commands/SendEmailCommand.cs deleted file mode 100644 index 4393edd..0000000 --- a/LiteCharms.Features/Utilities/Commands/SendEmailCommand.cs +++ /dev/null @@ -1,79 +0,0 @@ -namespace LiteCharms.Features.Utilities.Commands; - -public class SendEmailCommand : IRequest -{ - public string? From { get; set; } - - public string? SenderName { get; set; } - - public string? To { get; set; } - - public string? RecipientName { get; set; } - - public string? Subject { get; set; } - - public string? Message { get; set; } - - public bool IsHtml { get; set; } - - public Stream? Attachment { get; set; } - - public string? AttachmentFileName { get; set; } - - private SendEmailCommand(string from, string senderName, string to, string recipientName, string subject, string message, bool isHtml = false, Stream? attachment = null, string? attachmentFileName = null) - { - From = from; - To = to; - Subject = subject; - Message = message; - IsHtml = isHtml; - Attachment = attachment; - AttachmentFileName = attachmentFileName; - SenderName = senderName; - RecipientName = recipientName; - } - - public static SendEmailCommand Create(string from, string senderName, string to, string recipientName, string subject, string message, bool isHtml = false, Stream? attachment = null, string? attachmentFileName = null) - { - if (string.IsNullOrWhiteSpace(from)) - throw new ArgumentException("From address is required."); - - if (string.IsNullOrWhiteSpace(senderName)) - throw new ArgumentException("Sender name is required."); - - if (!string.IsNullOrWhiteSpace(senderName) && senderName?.Length > 255) - throw new ArgumentException("Sender name cannot exceed 255 characters."); - - if (string.IsNullOrWhiteSpace(to)) - throw new ArgumentException("To address is required."); - - if (string.IsNullOrWhiteSpace(recipientName)) - throw new ArgumentException("Recipient name is required."); - - if (!string.IsNullOrWhiteSpace(recipientName) && recipientName?.Length > 255) - throw new ArgumentException("Recipient name cannot exceed 255 characters."); - - if (string.IsNullOrWhiteSpace(subject)) - throw new ArgumentException("Subject is required."); - - if (!string.IsNullOrWhiteSpace(subject) && subject?.Length > 2048) - throw new ArgumentException("Subject cannot exceed 2048 characters."); - - if (string.IsNullOrWhiteSpace(message)) - throw new ArgumentException("Message is required."); - - if (message.Length > 10485760) - throw new ArgumentException("Message cannot exceed 10 MB."); - - if (attachment != null && string.IsNullOrWhiteSpace(attachmentFileName)) - throw new ArgumentException("Attachment file name must be provided when an attachment is included."); - - if (attachment is not null && attachment.Length > 10485760) - throw new ArgumentException("Attachment cannot exceed 10 MB."); - - if (!string.IsNullOrWhiteSpace(attachmentFileName) && attachmentFileName.Length > 255) - throw new ArgumentException("Attachment file name cannot exceed 255 characters."); - - return new(from, senderName!, to, recipientName!, subject!, message, isHtml, attachment, attachmentFileName); - } -} diff --git a/LiteCharms.Infrastructure/Database/LeadGeneratorDbContext.cs b/LiteCharms.Infrastructure/Database/LeadGeneratorDbContext.cs deleted file mode 100644 index b3e7195..0000000 --- a/LiteCharms.Infrastructure/Database/LeadGeneratorDbContext.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteCharms.Entities; - -namespace LiteCharms.Infrastructure.Database; - -public class LeadGeneratorDbContext(DbContextOptions options) : DbContext(options) -{ - public DbSet Customers { get; set; } - - public DbSet Leads { get; set; } - - public DbSet Orders { get; set; } - - public DbSet OrderRefunds { get; set; } - - public DbSet Products { get; set; } - - public DbSet ProductPrices { get; set; } - - public DbSet Notifications { get; set; } - - public DbSet Quotes { get; set; } - - public DbSet ShoppingCarts { get; set; } - - public DbSet ShoppingCartItems { get; set; } -} diff --git a/LiteCharms.Infrastructure/Database/LeadGeneratorDbContextFactory.cs b/LiteCharms.Infrastructure/Database/LeadGeneratorDbContextFactory.cs deleted file mode 100644 index 0a18c5b..0000000 --- a/LiteCharms.Infrastructure/Database/LeadGeneratorDbContextFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace LiteCharms.Infrastructure.Database; - -public class LeadGeneratorDbContextFactory : IDesignTimeDbContextFactory -{ - public LeadGeneratorDbContext CreateDbContext(string[] args) - { - var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddUserSecrets(typeof(LeadGeneratorDbContext).Assembly) - .AddJsonFile("appsettings.json") - .AddEnvironmentVariables() - .Build(); - - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgresLeadGenerator")); - - return new LeadGeneratorDbContext(optionsBuilder.Options); - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.Designer.cs deleted file mode 100644 index 129f9fd..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.Designer.cs +++ /dev/null @@ -1,272 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(LeadGeneratorDbContext))] - [Migration("20260502231708_Init")] - partial class Init - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttribusionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("GoogleClickId") - .HasColumnType("text"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("LeadId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("ProductPrice", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.cs b/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.cs deleted file mode 100644 index 60c879e..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260502231708_Init.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class Init : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Customer", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - Company = table.Column(type: "text", nullable: true), - Name = table.Column(type: "text", nullable: false), - LastName = table.Column(type: "text", nullable: false), - Tax = table.Column(type: "text", nullable: true), - Email = table.Column(type: "text", nullable: false), - Discord = table.Column(type: "text", nullable: true), - Slack = table.Column(type: "text", nullable: true), - LinkedIn = table.Column(type: "text", nullable: true), - Whatsapp = table.Column(type: "text", nullable: true), - Website = table.Column(type: "text", nullable: true), - Phone = table.Column(type: "text", nullable: true), - Address = table.Column(type: "text", nullable: true), - City = table.Column(type: "text", nullable: true), - Region = table.Column(type: "text", nullable: true), - Country = table.Column(type: "text", nullable: true), - PostalCode = table.Column(type: "text", nullable: true), - Active = table.Column(type: "boolean", nullable: false, defaultValue: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Customer", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Lead", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - GoogleClickId = table.Column(type: "text", nullable: true), - WebClickId = table.Column(type: "text", nullable: true), - AppClickId = table.Column(type: "text", nullable: true), - CampaignId = table.Column(type: "bigint", nullable: true), - AdGroupId = table.Column(type: "bigint", nullable: true), - AdName = table.Column(type: "bigint", nullable: true), - TargetId = table.Column(type: "bigint", nullable: true), - FeedItemId = table.Column(type: "bigint", nullable: true), - ClickLocation = table.Column(type: "text", nullable: true), - AttribusionHash = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Lead", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Order", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - LeadId = table.Column(type: "uuid", nullable: false), - ProductId = table.Column(type: "uuid", nullable: false), - ProductPriceId = table.Column(type: "uuid", nullable: false), - Status = table.Column(type: "integer", nullable: false), - Notes = table.Column(type: "jsonb", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Order", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "OrderRefund", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - OrderId = table.Column(type: "uuid", nullable: false), - Reason = table.Column(type: "text", nullable: false), - Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_OrderRefund", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Product", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), - Active = table.Column(type: "boolean", nullable: false, defaultValue: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Product", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ProductPrice", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - ProductId = table.Column(type: "uuid", nullable: false), - Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - Discount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), - Active = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ProductPrice", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Customer"); - - migrationBuilder.DropTable( - name: "Lead"); - - migrationBuilder.DropTable( - name: "Order"); - - migrationBuilder.DropTable( - name: "OrderRefund"); - - migrationBuilder.DropTable( - name: "Product"); - - migrationBuilder.DropTable( - name: "ProductPrice"); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.Designer.cs deleted file mode 100644 index 4d9b3f9..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.Designer.cs +++ /dev/null @@ -1,357 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(LeadGeneratorDbContext))] - [Migration("20260503002123_DefinedEntityRelationships")] - partial class DefinedEntityRelationships - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttribusionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("GoogleClickId") - .HasColumnType("text"); - - b.Property("LeadId") - .HasColumnType("uuid"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LeadGenerator.Database.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.HasOne("LeadGenerator.Database.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LeadGenerator.Database.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.HasOne("LeadGenerator.Database.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.cs deleted file mode 100644 index 059f789..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503002123_DefinedEntityRelationships.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class DefinedEntityRelationships : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LeadId", - table: "Order"); - - migrationBuilder.RenameColumn( - name: "ProductId", - table: "Order", - newName: "CustomerId"); - - migrationBuilder.AddColumn( - name: "RefundId", - table: "Order", - type: "uuid", - nullable: true); - - migrationBuilder.AddColumn( - name: "CustomerId", - table: "Lead", - type: "uuid", - nullable: true); - - migrationBuilder.AddColumn( - name: "LeadId", - table: "Lead", - type: "uuid", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_ProductPrice_ProductId", - table: "ProductPrice", - column: "ProductId"); - - migrationBuilder.CreateIndex( - name: "IX_OrderRefund_OrderId", - table: "OrderRefund", - column: "OrderId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Order_CustomerId", - table: "Order", - column: "CustomerId"); - - migrationBuilder.CreateIndex( - name: "IX_Order_ProductPriceId", - table: "Order", - column: "ProductPriceId"); - - migrationBuilder.CreateIndex( - name: "IX_Lead_CustomerId", - table: "Lead", - column: "CustomerId"); - - migrationBuilder.AddForeignKey( - name: "FK_Lead_Customer_CustomerId", - table: "Lead", - column: "CustomerId", - principalTable: "Customer", - principalColumn: "Id"); - - migrationBuilder.AddForeignKey( - name: "FK_Order_Customer_CustomerId", - table: "Order", - column: "CustomerId", - principalTable: "Customer", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - - migrationBuilder.AddForeignKey( - name: "FK_Order_ProductPrice_ProductPriceId", - table: "Order", - column: "ProductPriceId", - principalTable: "ProductPrice", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - - migrationBuilder.AddForeignKey( - name: "FK_OrderRefund_Order_OrderId", - table: "OrderRefund", - column: "OrderId", - principalTable: "Order", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_ProductPrice_Product_ProductId", - table: "ProductPrice", - column: "ProductId", - principalTable: "Product", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Lead_Customer_CustomerId", - table: "Lead"); - - migrationBuilder.DropForeignKey( - name: "FK_Order_Customer_CustomerId", - table: "Order"); - - migrationBuilder.DropForeignKey( - name: "FK_Order_ProductPrice_ProductPriceId", - table: "Order"); - - migrationBuilder.DropForeignKey( - name: "FK_OrderRefund_Order_OrderId", - table: "OrderRefund"); - - migrationBuilder.DropForeignKey( - name: "FK_ProductPrice_Product_ProductId", - table: "ProductPrice"); - - migrationBuilder.DropIndex( - name: "IX_ProductPrice_ProductId", - table: "ProductPrice"); - - migrationBuilder.DropIndex( - name: "IX_OrderRefund_OrderId", - table: "OrderRefund"); - - migrationBuilder.DropIndex( - name: "IX_Order_CustomerId", - table: "Order"); - - migrationBuilder.DropIndex( - name: "IX_Order_ProductPriceId", - table: "Order"); - - migrationBuilder.DropIndex( - name: "IX_Lead_CustomerId", - table: "Lead"); - - migrationBuilder.DropColumn( - name: "RefundId", - table: "Order"); - - migrationBuilder.DropColumn( - name: "CustomerId", - table: "Lead"); - - migrationBuilder.DropColumn( - name: "LeadId", - table: "Lead"); - - migrationBuilder.RenameColumn( - name: "CustomerId", - table: "Order", - newName: "ProductId"); - - migrationBuilder.AddColumn( - name: "LeadId", - table: "Order", - type: "uuid", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.Designer.cs deleted file mode 100644 index 0fac239..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.Designer.cs +++ /dev/null @@ -1,354 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(LeadGeneratorDbContext))] - [Migration("20260503003624_RemovedLeadIdFromLead")] - partial class RemovedLeadIdFromLead - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttribusionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("GoogleClickId") - .HasColumnType("text"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LeadGenerator.Database.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.HasOne("LeadGenerator.Database.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LeadGenerator.Database.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.HasOne("LeadGenerator.Database.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.cs deleted file mode 100644 index d469ce3..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503003624_RemovedLeadIdFromLead.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class RemovedLeadIdFromLead : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LeadId", - table: "Lead"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LeadId", - table: "Lead", - type: "uuid", - nullable: true); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.Designer.cs deleted file mode 100644 index 036f3c2..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503012708_AddedStatusToLead.Designer.cs +++ /dev/null @@ -1,357 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(LeadGeneratorDbContext))] - [Migration("20260503012708_AddedStatusToLead")] - partial class AddedStatusToLead - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttribusionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("GoogleClickId") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Lead", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.HasOne("LeadGenerator.Database.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LeadGenerator.Database.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.OrderRefund", b => - { - b.HasOne("LeadGenerator.Database.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LeadGenerator.Database.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.ProductPrice", b => - { - b.HasOne("LeadGenerator.Database.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LeadGenerator.Database.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.Designer.cs deleted file mode 100644 index 0a1ad72..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.Designer.cs +++ /dev/null @@ -1,357 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(LeadGeneratorDbContext))] - [Migration("20260503133855_CorrectedAttributionHashColumnOnLead")] - partial class CorrectedAttributionHashColumnOnLead - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttributionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("GoogleClickId") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.HasOne("LiteCharms.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.cs b/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.cs deleted file mode 100644 index 9d1fd30..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260503133855_CorrectedAttributionHashColumnOnLead.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class CorrectedAttributionHashColumnOnLead : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "AttribusionHash", - table: "Lead", - newName: "AttributionHash"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "AttributionHash", - table: "Lead", - newName: "AttribusionHash"); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs deleted file mode 100644 index 6f22981..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs +++ /dev/null @@ -1,360 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(LeadGeneratorDbContext))] - [Migration("20260505120450_GeneralisedLead")] - partial class GeneralisedLead - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttributionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickId") - .HasColumnType("text"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("Source") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.HasOne("LiteCharms.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.cs deleted file mode 100644 index 6176058..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class GeneralisedLead : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "GoogleClickId", - table: "Lead", - newName: "Source"); - - migrationBuilder.AddColumn( - name: "ClickId", - table: "Lead", - type: "text", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ClickId", - table: "Lead"); - - migrationBuilder.RenameColumn( - name: "Source", - table: "Lead", - newName: "GoogleClickId"); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.Designer.cs deleted file mode 100644 index afd8292..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.Designer.cs +++ /dev/null @@ -1,409 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(LeadGeneratorDbContext))] - [Migration("20260505123745_AddedNotifications")] - partial class AddedNotifications - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttributionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickId") - .HasColumnType("text"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("Source") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Notification", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Author") - .IsRequired() - .HasColumnType("text"); - - b.Property("CorrelationId") - .IsRequired() - .HasColumnType("text"); - - b.Property("CorrelationIdType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Direction") - .HasColumnType("integer"); - - b.Property("IsInternal") - .HasColumnType("boolean"); - - b.Property("Platform") - .IsRequired() - .HasColumnType("text"); - - b.Property("PlatformAddress") - .IsRequired() - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Notification", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.HasOne("LiteCharms.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.cs deleted file mode 100644 index c39cf39..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505123745_AddedNotifications.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class AddedNotifications : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Notification", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - Direction = table.Column(type: "integer", nullable: false), - Author = table.Column(type: "text", nullable: false), - Title = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), - Platform = table.Column(type: "text", nullable: false), - PlatformAddress = table.Column(type: "text", nullable: false), - CorrelationId = table.Column(type: "text", nullable: false), - CorrelationIdType = table.Column(type: "text", nullable: false), - IsInternal = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Notification", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Notification"); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.Designer.cs deleted file mode 100644 index ae0228c..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.Designer.cs +++ /dev/null @@ -1,416 +0,0 @@ -// -using System; -using LiteCharms.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - [DbContext(typeof(LeadGeneratorDbContext))] - [Migration("20260505124135_AddedProcessedColumnToNotifications")] - partial class AddedProcessedColumnToNotifications - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Address") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Company") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discord") - .HasColumnType("text"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LinkedIn") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("Region") - .HasColumnType("text"); - - b.Property("Slack") - .HasColumnType("text"); - - b.Property("Tax") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("Website") - .HasColumnType("text"); - - b.Property("Whatsapp") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Customer", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AdGroupId") - .HasColumnType("bigint"); - - b.Property("AdName") - .HasColumnType("bigint"); - - b.Property("AppClickId") - .HasColumnType("text"); - - b.Property("AttributionHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("CampaignId") - .HasColumnType("bigint"); - - b.Property("ClickId") - .HasColumnType("text"); - - b.Property("ClickLocation") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.Property("FeedItemId") - .HasColumnType("bigint"); - - b.Property("Source") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TargetId") - .HasColumnType("bigint"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.Property("WebClickId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.ToTable("Lead", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Notification", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Author") - .IsRequired() - .HasColumnType("text"); - - b.Property("CorrelationId") - .IsRequired() - .HasColumnType("text"); - - b.Property("CorrelationIdType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Direction") - .HasColumnType("integer"); - - b.Property("IsInternal") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Platform") - .IsRequired() - .HasColumnType("text"); - - b.Property("PlatformAddress") - .IsRequired() - .HasColumnType("text"); - - b.Property("Processed") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Notification", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("uuid"); - - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); - - b.Property("ProductPriceId") - .HasColumnType("uuid"); - - b.Property("RefundId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductPriceId"); - - b.ToTable("Order", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Amount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("OrderId") - .HasColumnType("uuid"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("OrderId") - .IsUnique(); - - b.ToTable("OrderRefund", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Product", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone"); - - b.Property("Discount") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("Price") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)"); - - b.Property("ProductId") - .HasColumnType("uuid"); - - b.Property("UpdatedAt") - .ValueGeneratedOnUpdate() - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProductId"); - - b.ToTable("ProductPrice", (string)null); - }); - - modelBuilder.Entity("LiteCharms.Entities.Lead", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Leads") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Customer"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.HasOne("LiteCharms.Entities.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") - .WithMany() - .HasForeignKey("ProductPriceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("ProductPrice"); - }); - - modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => - { - b.HasOne("LiteCharms.Entities.Order", "Order") - .WithOne("Refund") - .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Order"); - }); - - modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => - { - b.HasOne("LiteCharms.Entities.Product", "Product") - .WithMany("ProductPrices") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Customer", b => - { - b.Navigation("Leads"); - - b.Navigation("Orders"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Order", b => - { - b.Navigation("Refund"); - }); - - modelBuilder.Entity("LiteCharms.Entities.Product", b => - { - b.Navigation("ProductPrices"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.cs deleted file mode 100644 index 3439cab..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505124135_AddedProcessedColumnToNotifications.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class AddedProcessedColumnToNotifications : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "IsInternal", - table: "Notification", - type: "boolean", - nullable: false, - defaultValue: true, - oldClrType: typeof(bool), - oldType: "boolean"); - - migrationBuilder.AddColumn( - name: "Processed", - table: "Notification", - type: "boolean", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Processed", - table: "Notification"); - - migrationBuilder.AlterColumn( - name: "IsInternal", - table: "Notification", - type: "boolean", - nullable: false, - oldClrType: typeof(bool), - oldType: "boolean", - oldDefaultValue: true); - } - } -} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.cs deleted file mode 100644 index 8442535..0000000 --- a/LiteCharms.Infrastructure/Database/Migrations/20260505202859_AddedQuoteShoppingCartalteredOrderCustomer.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LiteCharms.Infrastructure.Database.Migrations -{ - /// - public partial class AddedQuoteShoppingCartalteredOrderCustomer : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Order_ProductPrice_ProductPriceId", - table: "Order"); - - migrationBuilder.DropIndex( - name: "IX_Order_ProductPriceId", - table: "Order"); - - migrationBuilder.RenameColumn( - name: "ProductPriceId", - table: "Order", - newName: "ShoppingCartId"); - - migrationBuilder.AddColumn( - name: "QuoteId", - table: "Order", - type: "uuid", - nullable: true); - - migrationBuilder.CreateTable( - name: "ShoppingCart", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - CustomerId = table.Column(type: "uuid", nullable: true), - OrderId = table.Column(type: "uuid", nullable: true), - QuoteId = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ShoppingCart", x => x.Id); - table.ForeignKey( - name: "FK_ShoppingCart_Customer_CustomerId", - column: x => x.CustomerId, - principalTable: "Customer", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "Quote", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - CustomerId1 = table.Column(type: "uuid", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ExpiredAt = table.Column(type: "timestamp with time zone", nullable: true), - CustomerId = table.Column(type: "uuid", nullable: false), - ShoppingCartId = table.Column(type: "uuid", nullable: false), - Status = table.Column(type: "integer", nullable: false), - Reason = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Quote", x => x.Id); - table.ForeignKey( - name: "FK_Quote_Customer_CustomerId", - column: x => x.CustomerId, - principalTable: "Customer", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Quote_Customer_CustomerId1", - column: x => x.CustomerId1, - principalTable: "Customer", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_Quote_ShoppingCart_ShoppingCartId", - column: x => x.ShoppingCartId, - principalTable: "ShoppingCart", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "ShoppingCartItems", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ShoppingCartId = table.Column(type: "uuid", nullable: false), - ProductPriceId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), - Quantity = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ShoppingCartItems", x => x.Id); - table.ForeignKey( - name: "FK_ShoppingCartItems_ProductPrice_ProductPriceId", - column: x => x.ProductPriceId, - principalTable: "ProductPrice", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ShoppingCartItems_ShoppingCart_ShoppingCartId", - column: x => x.ShoppingCartId, - principalTable: "ShoppingCart", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Order_QuoteId", - table: "Order", - column: "QuoteId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Order_ShoppingCartId", - table: "Order", - column: "ShoppingCartId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Quote_CustomerId", - table: "Quote", - column: "CustomerId"); - - migrationBuilder.CreateIndex( - name: "IX_Quote_CustomerId1", - table: "Quote", - column: "CustomerId1"); - - migrationBuilder.CreateIndex( - name: "IX_Quote_ShoppingCartId", - table: "Quote", - column: "ShoppingCartId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingCart_CustomerId", - table: "ShoppingCart", - column: "CustomerId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingCartItems_ProductPriceId", - table: "ShoppingCartItems", - column: "ProductPriceId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingCartItems_ShoppingCartId", - table: "ShoppingCartItems", - column: "ShoppingCartId"); - - migrationBuilder.AddForeignKey( - name: "FK_Order_Quote_QuoteId", - table: "Order", - column: "QuoteId", - principalTable: "Quote", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - - migrationBuilder.AddForeignKey( - name: "FK_Order_ShoppingCart_ShoppingCartId", - table: "Order", - column: "ShoppingCartId", - principalTable: "ShoppingCart", - principalColumn: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Order_Quote_QuoteId", - table: "Order"); - - migrationBuilder.DropForeignKey( - name: "FK_Order_ShoppingCart_ShoppingCartId", - table: "Order"); - - migrationBuilder.DropTable( - name: "Quote"); - - migrationBuilder.DropTable( - name: "ShoppingCartItems"); - - migrationBuilder.DropTable( - name: "ShoppingCart"); - - migrationBuilder.DropIndex( - name: "IX_Order_QuoteId", - table: "Order"); - - migrationBuilder.DropIndex( - name: "IX_Order_ShoppingCartId", - table: "Order"); - - migrationBuilder.DropColumn( - name: "QuoteId", - table: "Order"); - - migrationBuilder.RenameColumn( - name: "ShoppingCartId", - table: "Order", - newName: "ProductPriceId"); - - migrationBuilder.CreateIndex( - name: "IX_Order_ProductPriceId", - table: "Order", - column: "ProductPriceId"); - - migrationBuilder.AddForeignKey( - name: "FK_Order_ProductPrice_ProductPriceId", - table: "Order", - column: "ProductPriceId", - principalTable: "ProductPrice", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - } - } -} diff --git a/LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs b/LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs deleted file mode 100644 index d1a8bd0..0000000 --- a/LiteCharms.Infrastructure/HealthChecks/QuartzHealthCheck.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace LiteCharms.Infrastructure.HealthChecks; - -public class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck -{ - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - try - { - var scheduler = await schedulerFactory.GetScheduler(cancellationToken); - - if (!scheduler.IsStarted) - return HealthCheckResult.Unhealthy("Quartz scheduler is not running"); - - await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken); - - return HealthCheckResult.Healthy("Quartz scheduler is ready"); - } - catch (SchedulerException) - { - return HealthCheckResult.Unhealthy("Quartz scheduler cannot connect to the store"); - } - } -} diff --git a/LiteCharms.Infrastructure/Quartz/MediatorJob.cs b/LiteCharms.Infrastructure/Quartz/MediatorJob.cs deleted file mode 100644 index 8cf9c75..0000000 --- a/LiteCharms.Infrastructure/Quartz/MediatorJob.cs +++ /dev/null @@ -1,21 +0,0 @@ -using LiteCharms.Abstractions; - -namespace LiteCharms.Infrastructure.Quartz; - -[DisallowConcurrentExecution] -public class MediatorJob(IMediator mediator) : IJob where TNotification : IEvent -{ - public async Task Execute(IJobExecutionContext context) - { - var data = context.MergedJobDataMap["Payload"] as string; - - if (string.IsNullOrWhiteSpace(data)) return; - - var notification = JsonSerializer.Deserialize(data); - - if(notification is null) return; - - if(notification is TNotification) - await mediator.Publish(notification, context.CancellationToken); - } -} diff --git a/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs b/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs deleted file mode 100644 index 52928fd..0000000 --- a/LiteCharms.Infrastructure/ServiceBus/Exchanges/EmailExchange.cs +++ /dev/null @@ -1,12 +0,0 @@ -using LiteCharms.Infrastructure.ServiceBus.Queues; - -namespace LiteCharms.Infrastructure.ServiceBus.Exchanges; - -public class EmailExchange(EmailQueue messages) : BackgroundService -{ - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - if(messages.Incoming.CanCount) - await Task.Delay(1000, stoppingToken); - } -} diff --git a/LiteCharms.Infrastructure/ServiceBus/Queues/EmailQueue.cs b/LiteCharms.Infrastructure/ServiceBus/Queues/EmailQueue.cs deleted file mode 100644 index 24b161e..0000000 --- a/LiteCharms.Infrastructure/ServiceBus/Queues/EmailQueue.cs +++ /dev/null @@ -1,5 +0,0 @@ -using LiteCharms.Abstractions; - -namespace LiteCharms.Infrastructure.ServiceBus.Queues; - -public class EmailQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Infrastructure/ServiceBus/Queues/GeneralQueue.cs b/LiteCharms.Infrastructure/ServiceBus/Queues/GeneralQueue.cs deleted file mode 100644 index f7024f6..0000000 --- a/LiteCharms.Infrastructure/ServiceBus/Queues/GeneralQueue.cs +++ /dev/null @@ -1,5 +0,0 @@ -using LiteCharms.Abstractions; - -namespace LiteCharms.Infrastructure.ServiceBus.Queues; - -public class GeneralQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Infrastructure/ServiceBus/Queues/SalesQueue.cs b/LiteCharms.Infrastructure/ServiceBus/Queues/SalesQueue.cs deleted file mode 100644 index 714ac11..0000000 --- a/LiteCharms.Infrastructure/ServiceBus/Queues/SalesQueue.cs +++ /dev/null @@ -1,5 +0,0 @@ -using LiteCharms.Abstractions; - -namespace LiteCharms.Infrastructure.ServiceBus.Queues; - -public class SalesQueue : EventBusQueueBase, IEventBusQueue; diff --git a/LiteCharms.Infrastructure/appsettings.json b/LiteCharms.Infrastructure/appsettings.json deleted file mode 100644 index 0db3279..0000000 --- a/LiteCharms.Infrastructure/appsettings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} diff --git a/LiteCharms.Models/Notification.cs b/LiteCharms.Models/Notification.cs deleted file mode 100644 index 88c4896..0000000 --- a/LiteCharms.Models/Notification.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace LiteCharms.Models; - -public class Notification -{ - public Guid Id { get; set; } - - public DateTimeOffset CreatedAt { get; set; } - - public NotificationDirection Direction { get; set; } - - public string? Author { get; set; } - - public string? Title { get; set; } - - public string? Description { get; set; } - - public string? Platform { get; set; } - - public string? PlatformAddress { get; set; } - - public string? CorrelationId { get; set; } - - public string? CorrelationIdType { get; set; } - - public bool IsInternal { get; set; } - - public bool Processed { get; set; } -} diff --git a/LiteCharms.Models/Order.cs b/LiteCharms.Models/Order.cs deleted file mode 100644 index 70ec1f3..0000000 --- a/LiteCharms.Models/Order.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace LiteCharms.Models; - -public class Order -{ - public Guid Id { get; set; } - - public DateTimeOffset CreatedAt { get; set; } - - public DateTimeOffset? UpdatedAt { get; set; } - - public Guid CustomerId { get; set; } - - public Guid? QuoteId { get; set; } - - public Guid ShoppingCartId { get; set; } - - public Guid? RefundId { get; set; } - - public OrderStatus Status { get; set; } - - public string[]? Notes { get; set; } -} diff --git a/LiteCharms.Models/Product.cs b/LiteCharms.Models/Product.cs deleted file mode 100644 index 406452c..0000000 --- a/LiteCharms.Models/Product.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace LiteCharms.Models; - -public class Product -{ - public Guid Id { get; set; } - - public string? Name { get; set; } - - public string? Description { get; set; } - - public bool Active { get; set; } -} diff --git a/LiteCharms.Models/Quote.cs b/LiteCharms.Models/Quote.cs deleted file mode 100644 index 955d0e7..0000000 --- a/LiteCharms.Models/Quote.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace LiteCharms.Models; - -public class Quote -{ - public Guid Id { get; set; } - - public DateTimeOffset CreatedAt { get; set; } - - public DateTimeOffset? UpdatedAt { get; set; } - - public DateTimeOffset? ExpiredAt { get; set; } - - public Guid CustomerId { get; set; } - - public Guid ShoppingCartId { get; set; } - - public QuoteStatus Status { get; set; } - - public string? Reason { get; set; } -} diff --git a/LiteCharms.Models/ShoppingCart.cs b/LiteCharms.Models/ShoppingCart.cs deleted file mode 100644 index 53e27b9..0000000 --- a/LiteCharms.Models/ShoppingCart.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace LiteCharms.Models; - -public class ShoppingCart -{ - public Guid Id { get; set; } - - public DateTimeOffset CreatedAt { get; set; } - - public DateTimeOffset? UpdatedAt { get; set; } - - public Guid? CustomerId { get; set; } - - public Guid? OrderId { get; set; } - - public Guid? QuoteId { get; set; } -} diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index 91cd1e5..de05e99 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -1,7 +1,18 @@ - - - - - + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index f355470..aa43ab3 100644 --- a/README.md +++ b/README.md @@ -1 +1,23 @@ -# LiteCharmsShared \ No newline at end of file +# LiteCharms.Components + +A suite of shared libraries providing core logic, models, and infrastructure for the LiteCharms ecosystem. + +## 📦 Published Packages +All packages are versioned and pushed to [Nexus](https://nexus.khongisa.co.za) automatically via CI/CD. + +* **Abstractions:** Interfaces and base contracts. +* **Models:** Shared DTOs and Request/Response objects. +* **Entities:** Database schema definitions. +* **Infrastructure:** Persistence and external service clients. +* **Features:** Core business logic modules. +* **Extensions:** Shared helper methods and DI configurations. + +## 🚀 CI/CD Workflow +1. Create a **Pull Request** to `master`. +2. Drone CI builds, tests, and packs the suite as `1.${DRONE_BUILD_NUMBER}.0`. +3. Packages are pushed to the Nexus `nuget-hosted` repository. +4. A formal **Gitea Release** is created with the versioned "Bill of Materials." + +## 🛠 Usage +Add the Nexus NuGet source to your local environment and reference the version listed in the latest Gitea Release: +`dotnet add package LiteCharms.Models -v 1.XX.0` \ No newline at end of file