Compare commits

..
99 Commits
Author SHA1 Message Date
khwezi 50a8a59d92 Merge pull request 'Split Features to create space for more projects' (#39) from midrandshop into master
Reviewed-on: #39
2026-05-24 13:19:55 +02:00
Khwezi Mngoma 70c6e0bfbc Split Features to create space for more projects
continuous-integration/drone/pr Build is passing
2026-05-24 13:19:09 +02:00
khwezi b70d9559b0 Merge pull request 'midrandshop' (#38) from midrandshop into master
Reviewed-on: #38
2026-05-23 11:55:02 +02:00
Khwezi Mngoma 032b9e1818 Updated nuget packages
continuous-integration/drone/pr Build is passing
2026-05-23 11:54:32 +02:00
Khwezi Mngoma 424c1c6f8c Fixed email test 2026-05-23 11:52:47 +02:00
khwezi 81d5e8f07c Merge pull request 'Added MidrandShop feature and spl;it extensions and healthchecks' (#37) from midrandshop into master
Reviewed-on: #37
2026-05-23 11:49:27 +02:00
Khwezi Mngoma 7d5e9a18d8 Added MidrandShop feature and spl;it extensions and healthchecks
continuous-integration/drone/pr Build is failing
2026-05-23 11:48:47 +02:00
khwezi 20a53942b5 Merge pull request 'Added product metadata' (#36) from s3service into master
Reviewed-on: #36
2026-05-20 21:15:01 +02:00
Khwezi Mngoma 3656223b5f Added product metadata
continuous-integration/drone/pr Build is passing
Added dates to product model
Migrated database changes
2026-05-20 21:13:58 +02:00
khwezi 9edb2aa4aa Merge pull request 'Optimised UploadFileAsync()' (#35) from s3service into master
Reviewed-on: #35
2026-05-20 15:33:47 +02:00
Khwezi Mngoma ccf30ac36b Optimised UploadFileAsync()
continuous-integration/drone/pr Build is passing
Implemented and tested DeleteFileAsync()
2026-05-20 15:32:54 +02:00
khwezi 6ed023f2cf Merge pull request 'Refactored the S3 services to properly upload the file' (#34) from s3service into master
Reviewed-on: #34
2026-05-20 08:03:43 +02:00
Khwezi Mngoma d6fdf1b9c8 Refactored the S3 services to properly upload the file
continuous-integration/drone/pr Build is passing
2026-05-20 08:01:44 +02:00
khwezi 2c9f5a846c Merge pull request 'Updated how i use configs' (#33) from s3service into master
Reviewed-on: #33
2026-05-19 14:57:59 +02:00
Khwezi Mngoma 89a343a85f Updated how i use configs
continuous-integration/drone/pr Build is passing
2026-05-19 14:57:14 +02:00
khwezi 41f7c05be3 Merge pull request 'Refactored service to internalise the CDN' (#32) from s3service into master
Reviewed-on: #32
2026-05-19 11:34:51 +02:00
Khwezi Mngoma 52d204e286 Refactored service to internalise the CDN
continuous-integration/drone/pr Build is passing
2026-05-19 11:34:00 +02:00
khwezi 1a03355e84 Merge pull request 'Added S3 support' (#31) from s3service into master
Reviewed-on: #31
2026-05-19 10:24:05 +02:00
Khwezi Mngoma f245bc94e1 Added S3 support
continuous-integration/drone/pr Build is passing
2026-05-19 10:23:36 +02:00
khwezi 7743c3178e Merge pull request 'Simplified notification updating' (#30) from emailjobs into master
Reviewed-on: #30
2026-05-17 16:01:24 +02:00
Khwezi Mngoma da141311ff Simplified notification updating
continuous-integration/drone/pr Build is passing
2026-05-17 16:00:35 +02:00
khwezi ab3d8e6e9a Merge pull request 'Refactored GetNotificationsAsync() date handling' (#29) from emailjobs into master
Reviewed-on: #29
2026-05-17 13:14:01 +02:00
Khwezi Mngoma 97bde73777 Refactored GetNotificationsAsync() date handling
continuous-integration/drone/pr Build is passing
2026-05-17 13:12:58 +02:00
khwezi db4c348288 Merge pull request 'Fixed email sending logic' (#28) from emailjobs into master
Reviewed-on: #28
2026-05-16 00:29:01 +02:00
Khwezi Mngoma a65e926a53 Fixed email sending logic
continuous-integration/drone/pr Build is passing
2026-05-16 00:28:31 +02:00
khwezi 6683234642 Merge pull request 'Refactored batch drop logic' (#27) from emailjobs into master
Reviewed-on: #27
2026-05-16 00:05:51 +02:00
Khwezi Mngoma 1471d9e597 Refactored batch drop logic
continuous-integration/drone/pr Build is passing
2026-05-16 00:04:58 +02:00
khwezi 6ddbb9479a Merge pull request 'Added an empty constructor to ProcessEmailNotificationEvent' (#26) from emailjobs into master
Reviewed-on: #26
2026-05-15 23:53:09 +02:00
Khwezi Mngoma e978aa17f8 Added an empty constructor to ProcessEmailNotificationEvent
continuous-integration/drone/pr Build is passing
2026-05-15 23:52:38 +02:00
khwezi 6c7349a0f8 Merge pull request 'Added additional logging and traces' (#25) from emailjobs into master
Reviewed-on: #25
2026-05-15 23:21:52 +02:00
Khwezi Mngoma a31f75c5ef Added additional logging and traces
continuous-integration/drone/pr Build is passing
2026-05-15 23:21:31 +02:00
khwezi e97fd6cd3f Merge pull request 'Added debug logging' (#24) from emailjobs into master
Reviewed-on: #24
2026-05-15 23:09:21 +02:00
Khwezi Mngoma 7f4246ac63 Added debug logging
continuous-integration/drone/pr Build is passing
2026-05-15 23:08:15 +02:00
khwezi 184c7c252a Merge pull request 'Set misfireThreshold to 2min and eased Cluster node checkin limit' (#23) from emailjobs into master
Reviewed-on: #23
2026-05-15 22:29:08 +02:00
Khwezi Mngoma dfc62c8fe1 Set misfireThreshold to 2min and eased Cluster node checkin limit
continuous-integration/drone/pr Build is passing
2026-05-15 22:28:18 +02:00
khwezi bfe8c458d6 Merge pull request 'Optimised quartz' (#22) from emailjobs into master
Reviewed-on: #22
2026-05-15 09:52:06 +02:00
Khwezi Mngoma 0f91f102e5 Optimised quartz
continuous-integration/drone/pr Build is passing
2026-05-15 09:51:26 +02:00
khwezi e6e0475db1 Merge pull request 'emailjobs' (#21) from emailjobs into master
Reviewed-on: #21
2026-05-15 08:39:36 +02:00
Khwezi Mngoma be9c83c8a3 Removed Abstractions listing from Git Tag
continuous-integration/drone/pr Build is passing
2026-05-15 08:39:10 +02:00
Khwezi Mngoma 65687d231e Ensured UTC is used 2026-05-15 08:37:58 +02:00
khwezi 5090c60797 Merge pull request 'Fixed Lead->Customer Relationship' (#20) from emailjobs into master
Reviewed-on: #20
2026-05-15 07:55:56 +02:00
Khwezi Mngoma 4523ef6151 Fixed Lead->Customer Relationship
continuous-integration/drone/pr Build is passing
2026-05-15 07:54:35 +02:00
khwezi 9432252e15 Merge pull request 'Added khongisa host entry on pipeline commands' (#19) from emailjobs into master
Reviewed-on: #19
2026-05-14 02:54:00 +02:00
Khwezi Mngoma 36b3656886 Added khongisa host entry on pipeline commands
continuous-integration/drone/pr Build is passing
2026-05-14 02:53:36 +02:00
khwezi 47111a1a3a Merge pull request 'emailjobs' (#18) from emailjobs into master
Reviewed-on: #18
2026-05-14 02:49:34 +02:00
Khwezi Mngoma f606b8fd3c Removed other packages from pipeline
continuous-integration/drone/pr Build is failing
2026-05-14 02:48:46 +02:00
Khwezi Mngoma 2610275bef Stable run on Notification creation 2026-05-14 02:46:07 +02:00
Khwezi Mngoma 134d8429c0 Completed refactor 2026-05-14 01:33:21 +02:00
Khwezi Mngoma 42001998d6 Fixed package references and namespaces
Refactored mappers
2026-05-13 20:15:21 +02:00
Khwezi Mngoma a42c51d7b2 Retructured solution 2026-05-13 20:06:24 +02:00
khwezi 6eb3d50375 Merge pull request 'Updated job scheduler' (#17) from emailjobs into master
Reviewed-on: #17
2026-05-10 17:33:23 +02:00
Khwezi Mngoma 26075cd9a7 Updated job scheduler
continuous-integration/drone/pr Build is passing
2026-05-10 17:32:09 +02:00
khwezi 4deb732804 Merge pull request 'emailjobs' (#16) from emailjobs into master
Reviewed-on: #16
2026-05-10 16:51:24 +02:00
Khwezi Mngoma cecd9f90e9 Implemented service bus handling of emails and notification processing
continuous-integration/drone/pr Build is passing
2026-05-10 16:50:36 +02:00
Khwezi Mngoma 73ba41beaf Added outgoing email notification processing event 2026-05-10 16:07:53 +02:00
Khwezi Mngoma e8e9a85c57 Migrated database changes after refactoring the Notification model 2026-05-10 15:27:26 +02:00
Khwezi Mngoma 394429677e Added package management 2026-05-10 14:18:56 +02:00
khwezi 20d9387d0b Merge pull request 'Migrated database changes' (#15) from develop into master
Reviewed-on: #15
2026-05-10 11:17:54 +02:00
Khwezi Mngoma 32d1019eb5 Migrated database changes
continuous-integration/drone/pr Build is passing
2026-05-10 11:16:52 +02:00
khwezi 9f6d0ccaa0 Merge pull request 'Populated README' (#14) from develop into master
Reviewed-on: #14
2026-05-10 09:48:35 +02:00
Khwezi Mngoma 275befc258 Populated README
continuous-integration/drone/pr Build is passing
2026-05-10 09:48:12 +02:00
khwezi 1acbc4d213 Merge pull request 'fixed git repo naming' (#13) from develop into master
Reviewed-on: #13
2026-05-10 09:22:37 +02:00
Khwezi Mngoma 193496bbf9 fixed git repo naming
continuous-integration/drone/pr Build is passing
2026-05-10 09:22:09 +02:00
khwezi 8c99668fac Merge pull request 'Added tag and release step after publish' (#12) from develop into master
Reviewed-on: #12
2026-05-10 09:19:33 +02:00
Khwezi Mngoma a015801423 Added tag and release step after publish
continuous-integration/drone/pr Build is failing
2026-05-10 09:18:54 +02:00
khwezi ad44f46204 Merge pull request 'Refactored Quartz instance id to AUTO, removed constant' (#11) from develop into master
Reviewed-on: #11
2026-05-10 08:21:08 +02:00
Khwezi Mngoma 9d64b5e76d Refactored Quartz instance id to AUTO, removed constant
continuous-integration/drone/pr Build is passing
2026-05-10 08:20:18 +02:00
khwezi 49d999c1e3 Merge pull request 'Refactored postgres extension' (#10) from develop into master
Reviewed-on: #10
2026-05-09 17:45:00 +02:00
Khwezi Mngoma fd6b8cd965 Refactored postgres extension
continuous-integration/drone/pr Build is passing
2026-05-09 17:44:16 +02:00
khwezi 9ed4777a18 Merge pull request 'Refactored database references' (#9) from develop into master
Reviewed-on: #9
2026-05-09 17:01:02 +02:00
Khwezi Mngoma ff34326a53 Refactored database references
continuous-integration/drone/pr Build is passing
2026-05-09 16:58:34 +02:00
khwezi 0cf44f68cc Merge pull request 'Added scheduler constants' (#8) from develop into master
Reviewed-on: #8
2026-05-09 15:27:50 +02:00
Khwezi Mngoma 6db6228bc9 Added scheduler constants
continuous-integration/drone/pr Build is passing
2026-05-09 15:27:07 +02:00
khwezi 41ed5a4288 Merge pull request 'Fixed quartz host config table prefix' (#7) from develop into master
Reviewed-on: #7
2026-05-09 13:30:33 +02:00
Khwezi Mngoma 61118c6223 Fixed quartz host config table prefix
continuous-integration/drone/pr Build is passing
2026-05-09 13:29:35 +02:00
khwezi bbcba5e06c Merge pull request 'Fixed quartz table name prefix' (#6) from develop into master
Reviewed-on: #6
2026-05-09 13:28:29 +02:00
Khwezi Mngoma bd852b30c3 Fixed quartz table name prefix
continuous-integration/drone/pr Build is passing
2026-05-09 13:27:54 +02:00
khwezi 502cc326dd Merge pull request 'Updated pipeline to use major version with minor always 0' (#5) from develop into master
Reviewed-on: #5
2026-05-09 12:02:19 +02:00
Khwezi Mngoma 37e2116aaf Updated pipeline to use major version with minor always 0
continuous-integration/drone/pr Build is passing
2026-05-09 12:01:52 +02:00
khwezi 4675d4c5fc Merge pull request 'Updated pipeline to use major versions only' (#4) from develop into master
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is failing
Reviewed-on: #4
2026-05-09 11:56:20 +02:00
Khwezi Mngoma 60980292a2 Updated pipeline to use major versions only
continuous-integration/drone/pr Build is failing
2026-05-09 11:55:43 +02:00
khwezi f80bb2fff9 Merge pull request 'Changed target branch from nain to master' (#3) from dronepipeline into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: #3
2026-05-09 11:37:39 +02:00
Khwezi Mngoma df1e31cc86 Changed target branch from nain to master
continuous-integration/drone/pr Build is passing
2026-05-09 11:37:11 +02:00
khwezi 6767906b0d Merge pull request 'Added pipeline separator' (#2) from dronepipeline into master
Reviewed-on: #2
2026-05-09 11:25:34 +02:00
Khwezi Mngoma 8cb1943ac8 Added pipeline separator 2026-05-09 11:25:08 +02:00
khwezi a344af4498 Merge pull request 'Added .drone.yml pipeline' (#1) from dronepipeline into master
Reviewed-on: #1
2026-05-09 11:12:38 +02:00
Khwezi Mngoma c95efd1faf Added .drone.yml pipeline 2026-05-09 11:11:29 +02:00
Khwezi Mngoma 3065d8e27a Refactored service bus lifetiemes to singleton 2026-05-09 09:03:12 +02:00
Khwezi Mngoma d998aac7c4 Rfactored service bus service lifetime 2026-05-09 08:37:45 +02:00
Khwezi Mngoma 33633502e2 Added quartz scheduler client 2026-05-07 16:24:10 +02:00
Khwezi Mngoma fe1d78342b Updated nuget packaged to v1.0.17 2026-05-07 16:14:52 +02:00
Khwezi Mngoma acc0d44c7c Added abstractions
Internal service bus support enabled
Added quartz support
Reconfigured extensions
2026-05-07 16:08:47 +02:00
Khwezi Mngoma c6ce9c85df Updated email functionality 2026-05-06 11:14:32 +02:00
Khwezi Mngoma 6ca87413d6 Pushed new nuget packages 2026-05-06 10:55:55 +02:00
Khwezi Mngoma 4321b03735 Implemented shopping carts functionality elements 2026-05-06 10:48:02 +02:00
Khwezi Mngoma 83f51c6a23 Added notifications
Added shopping cart and items
Added quotes
Refactored relatinoships
Migrated changes
Refactored cqrs commands and queries
Refactored mappings
2026-05-05 23:59:31 +02:00
Khwezi Mngoma 4b822c63b2 Added support for notifications 2026-05-05 14:36:46 +02:00
Khwezi Mngoma f5c9557f59 Generalised lead entries 2026-05-05 14:06:38 +02:00
Khwezi Mngoma 33112dcf79 Stable email sender 2026-05-04 23:29:29 +02:00
211 changed files with 10994 additions and 4245 deletions
+53
View File
@@ -0,0 +1,53 @@
---
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
- 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\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
\"draft\": false,
\"prerelease\": false
}"
trigger:
event:
- pull_request
+248
View File
@@ -0,0 +1,248 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Code Actions ####
# Type members
dotnet_hide_advanced_members = false
dotnet_member_insertion_location = with_other_members_of_the_same_kind
dotnet_property_generation_behavior = prefer_throwing_properties
# Symbol search
dotnet_search_reference_assemblies = true
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Expression-level preferences
dotnet_prefer_system_hash_code = true
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_non_hidden_explicit_cast_in_source = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Field preferences
dotnet_style_readonly_field = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
# New line preferences
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = true
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = false
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = false
# Expression-bodied members
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = true
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Modifier preferences
csharp_prefer_static_anonymous_function = true
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
# Code-block preferences
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
csharp_prefer_system_threading_lock = true
csharp_style_namespace_declarations = block_scoped
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_primary_constructors = true
csharp_style_prefer_simple_property_accessors = true
csharp_style_prefer_top_level_statements = true
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_implicitly_typed_lambda_expression = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_unbound_generic_type_in_nameof = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace
# New line preferences
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
@@ -1,28 +0,0 @@
namespace LiteCharms.Entities.Configuration;
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> 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.RefundId).IsRequired(false);
builder.Property(f => f.ProductPriceId).IsRequired();
builder.Property(f => f.Status).HasConversion<int>().IsRequired();
builder.Property(f => f.Notes).HasColumnType("jsonb").IsRequired(false);
builder.HasOne(f => f.ProductPrice)
.WithMany()
.HasForeignKey(f => f.ProductPriceId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Customer)
.WithMany(f => f.Orders)
.HasForeignKey(f => f.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -1,14 +0,0 @@
namespace LiteCharms.Entities.Configuration;
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> 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);
}
}
-11
View File
@@ -1,11 +0,0 @@
using LiteCharms.Entities.Configuration;
namespace LiteCharms.Entities;
[EntityTypeConfiguration<CustomerConfiguration, Customer>]
public class Customer : Models.Customer
{
public virtual ICollection<Lead>? Leads { get; set; }
public virtual ICollection<Order>? Orders { get; set; }
}
@@ -1,45 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\LiteCharms.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<!-- Nuget Package Details -->
<PropertyGroup>
<PackageId>LiteCharms.Entities</PackageId>
<Version>1.0.2</Version>
<Authors>Khwezi Mngoma</Authors>
<Company>Lite Charms (PTY) Ltd</Company>
<Description>Shared entities for Lite Charms applications.</Description>
<PackageProjectUrl>https://gitea.khongisa.co.za/litecharms/components</PackageProjectUrl>
<RepositoryUrl>https://gitea.khongisa.co.za/litecharms/components.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageTags>utility;dotnet</PackageTags>
<PackageIcon>icon.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<None Include="..\LICENSE" Pack="true" PackagePath="\"/>
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<!-- Database -->
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
<!-- Global Usings -->
<Using Include="Microsoft.EntityFrameworkCore" />
<Using Include="Microsoft.EntityFrameworkCore.Metadata.Builders" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiteCharms.Models\LiteCharms.Models.csproj" />
</ItemGroup>
</Project>
-13
View File
@@ -1,13 +0,0 @@
using LiteCharms.Entities.Configuration;
namespace LiteCharms.Entities;
[EntityTypeConfiguration<OrderConfiguration, Order>]
public class Order : Models.Order
{
public virtual OrderRefund? Refund { get; set; }
public virtual Customer? Customer { get; set; }
public virtual ProductPrice? ProductPrice { get; set; }
}
@@ -1,94 +0,0 @@
using LiteCharms.Models;
namespace LiteCharms.Extensions;
public static class EntityModeMappers
{
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,
GoogleClickId = entity.GoogleClickId,
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,
ProductPriceId = entity.ProductPriceId,
Notes = entity.Notes,
RefundId = entity.RefundId
};
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
};
}
@@ -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<Fixture>()
.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<IMediator>();
}
public void Dispose() { }
}
@@ -0,0 +1,57 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<UserSecretsId>fa06c1a6-2ba8-4c47-b19b-11e0f78e7b92</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<!-- Global Usings -->
<ItemGroup>
<Using Include="Mediator" />
<Using Include="Xunit.Abstractions" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Microsoft.Extensions.Configuration" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiteCharms.Features.TechShop\LiteCharms.Features.TechShop.csproj" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -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<Fixture>
{
private readonly NotificationService notificationService = fixture.Services.GetRequiredService<NotificationService>();
[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);
}
}
@@ -0,0 +1,19 @@
using LiteCharms.Features.TechShop.Products;
namespace LiteCharms.Features.TechShop.Tests;
public class ProductsFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
[Fact]
public async Task GetProductsAsync_ReturnsProducts()
{
var productService = fixture.Services.GetRequiredService<ProductService>();
var result = await productService.GetProductsAsync();
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
output.WriteLine($"Retrieved {result.Value.Length} products.");
}
}
@@ -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": "*"
}
@@ -0,0 +1,7 @@
namespace LiteCharms.Features.TechShop.CartPackages.Entities;
[EntityTypeConfiguration<PackageConfirguration, Package>]
public class Package : Models.Package
{
public virtual ICollection<PackageItem>? PackageItems { get; set; }
}
@@ -0,0 +1,18 @@
namespace LiteCharms.Features.TechShop.CartPackages.Entities;
public class PackageConfirguration : IEntityTypeConfiguration<Package>
{
public void Configure(EntityTypeBuilder<Package> builder)
{
builder.ToTable(nameof(Package));
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().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);
}
}
@@ -0,0 +1,11 @@
using LiteCharms.Features.TechShop.Products.Entities;
namespace LiteCharms.Features.TechShop.CartPackages.Entities;
[EntityTypeConfiguration<PackageItemConfiguration, PackageItem>]
public class PackageItem : Models.PackageItem
{
public virtual Package? Package { get; set; }
public virtual ProductPrice? ProductPrice { get; set; }
}
@@ -0,0 +1,27 @@
namespace LiteCharms.Features.TechShop.CartPackages.Entities;
public class PackageItemConfiguration : IEntityTypeConfiguration<PackageItem>
{
public void Configure(EntityTypeBuilder<PackageItem> builder)
{
builder.ToTable(nameof(PackageItem));
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().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);
}
}
@@ -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; }
}
@@ -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; }
}
@@ -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<ShopDbContext> contextFactory)
{
public async ValueTask<Result<Guid>> 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<Guid>($"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<Guid>($"Failed to add new package item by ID {productPriceId}");
}
catch (Exception ex)
{
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Guid>> 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<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> 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<Result> 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<Result<Package>> 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<Package>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<PackageItem[]>> 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<PackageItem[]>($"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<PackageItem[]>($"Could not find package items by package ID {packageId}");
}
catch (Exception ex)
{
return Result.Fail<PackageItem[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Package[]>> 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<Package[]>(new Error($"No packages found for the specified date range {range.From} - {range.To}."));
}
catch (Exception ex)
{
return Result.Fail<Package[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Guid>> 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<Result<Guid>> 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));
}
}
}
@@ -0,0 +1,133 @@
using LiteCharms.Features.Extensions;
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<ShopDbContext> contextFactory)
{
public async ValueTask<Result<Guid>> 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<Guid>(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<Guid>(new Error($"Failed to create customer {customerEmail}"));
}
catch (Exception ex)
{
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Customer>> 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>($"Customer not found with id {customerId}");
}
catch (Exception ex)
{
return Result.Fail<Customer>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Customer[]>> 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<Customer[]>(new Error("No customers found in the specified date range."));
}
catch (Exception ex)
{
return Result.Fail<Customer[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> 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));
}
}
}
@@ -0,0 +1,18 @@
using LiteCharms.Features.TechShop.Leads.Entities;
using LiteCharms.Features.TechShop.Orders.Entities;
using LiteCharms.Features.TechShop.Quotes.Entities;
using LiteCharms.Features.TechShop.ShoppingCarts.Entities;
namespace LiteCharms.Features.TechShop.Customers.Entities;
[EntityTypeConfiguration<CustomerConfiguration, Customer>]
public class Customer : Models.Customer
{
public virtual ICollection<Lead>? Leads { get; set; }
public virtual ICollection<Order>? Orders { get; set; }
public virtual ICollection<Quote>? Quotes { get; set; }
public virtual ICollection<ShoppingCart>? ShoppingCarts { get; set; }
}
@@ -1,14 +1,14 @@
namespace LiteCharms.Entities.Configuration;
namespace LiteCharms.Features.TechShop.Customers.Entities;
public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> 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<Customer>
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);
}
}
@@ -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; }
@@ -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; }
}
+62
View File
@@ -0,0 +1,62 @@
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
{
Draft = 0,
Sent = 1,
Accepted = 2,
Rejected = 3,
Expired = 4
}
public enum OrderStatus : int
{
Pending = 0,
Completed = 1,
Cancelled = 2,
Failed = 3,
Refunded = 4,
Error = 5
}
public enum LeadStatus : int
{
New = 0,
Contacted = 1,
Qualified = 2,
Unqualified = 3,
Converted = 4,
Lost = 5
}
public enum NotificationDirection : int
{
Incoming = 0,
Outgoing = 1,
Neutral = 2
}
@@ -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<ShopQuartzHealthCheck>("ShopQuartz");
return services;
}
public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<PostgresShopHealthCheck>(ShopDbConfigName);
return services;
}
}
@@ -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
};
}
@@ -0,0 +1,16 @@
using LiteCharms.Features.TechShop.Postgres;
namespace LiteCharms.Features.TechShop.Extensions;
public static class Postgres
{
public const string ShopDbConfigName = "PostgresShop";
public static IServiceCollection AddTechShopDatabase(this IServiceCollection services, IConfiguration configuration)
{
services.AddPooledDbContextFactory<ShopDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName)));
return services;
}
}
@@ -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<PackageService>()
.AddSingleton<LeadService>()
.AddSingleton<NotificationService>()
.AddSingleton<OrderService>()
.AddSingleton<ProductService>()
.AddSingleton<QuoteService>()
.AddSingleton<ShoppingCartService>()
.AddSingleton<CustomerService>();
return services;
}
}
@@ -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(ShopDbConfigName)!;
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
@@ -16,11 +18,11 @@ public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck
await command.ExecuteScalarAsync(cancellationToken);
return HealthCheckResult.Healthy("PostgreSQL is responsive.");
return HealthCheckResult.Healthy($"{ShopDbConfigName} is responsive.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("PostgreSQL is unreachable.", ex);
return HealthCheckResult.Unhealthy($"{ShopDbConfigName} is unreachable.", ex);
}
}
}
@@ -0,0 +1,28 @@
using static LiteCharms.Features.Extensions.Quartz;
namespace LiteCharms.Features.TechShop.HealthChecks;
public class ShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var scheduler = await schedulerFactory.GetScheduler(ShopSchedulerName, cancellationToken);
if(scheduler == null)
return HealthCheckResult.Unhealthy($"Scheduler with name '{ShopSchedulerName}' not found.");
if (!scheduler.IsStarted)
return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler is not running");
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
return HealthCheckResult.Healthy($"{ShopSchedulerName} Quartz scheduler is ready");
}
catch (SchedulerException)
{
return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler cannot connect to the store");
}
}
}
@@ -1,6 +1,6 @@
using LiteCharms.Entities.Configuration;
using LiteCharms.Features.TechShop.Customers.Entities;
namespace LiteCharms.Entities;
namespace LiteCharms.Features.TechShop.Leads.Entities;
[EntityTypeConfiguration<LeadConfiguration, Lead>]
public class Lead : Models.Lead
@@ -1,16 +1,17 @@
namespace LiteCharms.Entities.Configuration;
namespace LiteCharms.Features.TechShop.Leads.Entities;
public class LeadConfiguration : IEntityTypeConfiguration<Lead>
{
public void Configure(EntityTypeBuilder<Lead> 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.GoogleClickId);
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);
builder.Property(f => f.AppClickId);
builder.Property(f => f.CampaignId);
@@ -21,5 +22,10 @@ public class LeadConfiguration : IEntityTypeConfiguration<Lead>
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);
}
}
@@ -0,0 +1,118 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models;
using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.TechShop.Leads.Models;
using LiteCharms.Features.TechShop.Postgres;
using static LiteCharms.Features.Extensions.Hash;
namespace LiteCharms.Features.TechShop.Leads;
public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
{
public async ValueTask<Result<Guid>> 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 = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(newLead.Entity.Id)
: Result.Fail<Guid>(new Error($"Failed to create lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}"));
}
catch (Exception ex)
{
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Lead[]>> 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<Lead[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Lead[]>> 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<Result> 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));
}
}
}
@@ -1,16 +1,18 @@
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; }
public string? GoogleClickId { get; set; }
public string? Source { get; set; }
public string? ClickId { get; set; }
public string? WebClickId { get; set; }
@@ -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; }
}
@@ -6,15 +6,16 @@
<Nullable>enable</Nullable>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\LiteCharms.snk</AssemblyOriginatorKeyFile>
<UserSecretsId>fbd8f4a2-0420-44e2-baff-4678d9e7eee1</UserSecretsId>
</PropertyGroup>
<!-- Nuget Package Details -->
<PropertyGroup>
<PackageId>LiteCharms.Extensions</PackageId>
<Version>1.0.7</Version>
<PackageId>LiteCharms.Features.TechShop</PackageId>
<Version>1.0.20</Version>
<Authors>Khwezi Mngoma</Authors>
<Company>Lite Charms (PTY) Ltd</Company>
<Description>Extension components for Lite Charms applications.</Description>
<Description>TechShop feature components for Lite Charms applications.</Description>
<PackageProjectUrl>https://gitea.khongisa.co.za/litecharms/components</PackageProjectUrl>
<RepositoryUrl>https://gitea.khongisa.co.za/litecharms/components.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
@@ -28,12 +29,30 @@
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<!-- Shared Usings -->
<!-- Quartz Scheduler-->
<ItemGroup>
<Using Include="Microsoft.AspNetCore.Builder" />
<PackageReference Include="OpenTelemetry" Version="1.15.3" />
<PackageReference Include="Quartz" Version="3.18.1" />
<PackageReference Include="Quartz.Plugins" Version="3.18.1" />
<PackageReference Include="Quartz.Plugins.TimeZoneConverter" Version="3.18.1" />
<PackageReference Include="Quartz.Serialization.SystemTextJson" Version="3.18.1" />
<PackageReference Include="Quartz.AspNetCore" Version="3.18.1" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.18.1" />
<!-- Global Usings -->
<Using Include="Quartz" />
</ItemGroup>
<!-- Configuration -->
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.8" />
<!-- Global Usings -->
<Using Include="Microsoft.Extensions.Configuration" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Microsoft.Extensions.Logging" />
</ItemGroup>
<!-- Health Checks -->
@@ -43,8 +62,9 @@
<PackageReference Include="AspNetCore.HealthChecks.UI.Core" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Data" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.8" />
<!-- Global Usings -->
<Using Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
@@ -58,6 +78,7 @@
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
<!-- Global Usings -->
<Using Include="OpenTelemetry.Resources" />
@@ -69,13 +90,13 @@
<!-- Database -->
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7">
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -88,10 +109,58 @@
<Using Include="Microsoft.EntityFrameworkCore.Metadata.Builders" />
</ItemGroup>
<!-- Email -->
<ItemGroup>
<ProjectReference Include="..\LiteCharms.Entities\LiteCharms.Entities.csproj" />
<ProjectReference Include="..\LiteCharms.Infrastructure\LiteCharms.Infrastructure.csproj" />
<ProjectReference Include="..\LiteCharms.Models\LiteCharms.Models.csproj" />
<PackageReference Include="MailKit" Version="4.16.0" />
<PackageReference Include="MimeKit" Version="4.16.0" />
<!-- Global Usings-->
<Using Include="MimeKit" />
<Using Include="MailKit.Net.Smtp" />
</ItemGroup>
<!-- CQRS -->
<ItemGroup>
<PackageReference Include="FluentResults" Version="4.0.0" />
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
<!-- Global Usings -->
<Using Include="FluentResults" />
<Using Include="Mediator" />
</ItemGroup>
<!-- Amazon S3 SDK -->
<ItemGroup>
<PackageReference Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.4.1" />
<PackageReference Include="AWSSDK.S3" Version="4.0.23.4" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
<!-- global Usings -->
<Using Include="Amazon.S3" />
<Using Include="Amazon.S3.Model" />
<Using Include="Amazon.Runtime" />
</ItemGroup>
<!-- Shared Usings -->
<ItemGroup>
<Using Include="Microsoft.AspNetCore.Builder" />
<Using Include="Microsoft.Extensions.Hosting" />
<Using Include="System.Text" />
<Using Include="System.Text.Json" />
<Using Include="System.Threading.Channels" />
<Using Include="System.Collections.ObjectModel" />
<Using Include="System.Diagnostics" />
<Using Include="System.Diagnostics.Metrics" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="System.Security.Cryptography" />
<Using Include="Microsoft.Extensions.Options" />
<Using Include="Microsoft.Extensions.Logging" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,4 @@
namespace LiteCharms.Features.TechShop.Notifications.Entities;
[EntityTypeConfiguration<NotificationConfiguration, Notification>]
public class Notification : Models.Notification;
@@ -0,0 +1,28 @@
namespace LiteCharms.Features.TechShop.Notifications.Entities;
public class NotificationConfiguration : IEntityTypeConfiguration<Notification>
{
public void Configure(EntityTypeBuilder<Notification> 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<int>();
builder.Property(f => f.Platform).IsRequired().HasConversion<int>();
builder.Property(f => f.Priority).IsRequired().HasConversion<int>();
builder.Property(f => f.CorrelationIdType).IsRequired().HasConversion<int>();
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);
}
}
@@ -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<ShopDbContext> contextFactory, ILogger<ProcessEmailNotificationsEvent> logger,
EmailService emailService) : INotificationHandler<ProcessEmailNotificationsEvent>
{
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<string>(1000);
errors.AddRange(sendResult.Errors.Select(e => e.Message));
if (sendResult.Reasons?.Count > 0)
errors.AddRange(sendResult.Reasons.Select(e => e.Message));
notification.HasError = true;
notification.Errors = [.. errors];
}
notification.Processed = true;
notification.UpdatedAt = DateTime.UtcNow;
}
await context.SaveChangesAsync(cancellationToken);
}
catch (Exception ex)
{
logger.LogError(ex, ex.Message);
}
finally
{
await emailService.DisconnectAsync(cancellationToken);
}
}
private async Task<Result> 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
}
};
}
@@ -0,0 +1,25 @@
using static LiteCharms.Features.Extensions.Email;
namespace LiteCharms.Features.TechShop.Notifications.Events.Handlers;
public class SendShopEmailEnquiryEventHandler(NotificationService notificationService) :
INotificationHandler<SendShopEmailEnquiryEvent>
{
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);
}
@@ -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);
}
@@ -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);
}
}
@@ -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; }
}
@@ -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; }
}
@@ -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<ShopDbContext> contextFactory)
{
public async ValueTask<Result<Guid>> 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<Result<Notification>> 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<Notification>(new Error($"Notification with id {notificationId} not found"));
}
catch (Exception ex)
{
return Result.Fail<Notification>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Notification[]>> 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<Result> 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));
}
}
}
@@ -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<OrderConfiguration, Order>]
public class Order : Models.Order
{
public virtual ICollection<OrderRefund>? Refunds { get; set; }
public virtual Customer? Customer { get; set; }
public virtual Quote? Quote { get; set; }
public virtual ShoppingCart? ShoppingCart { get; set; }
}
@@ -0,0 +1,25 @@
namespace LiteCharms.Features.TechShop.Orders.Entities;
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> 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<int>().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);
}
}
@@ -1,9 +1,8 @@
using LiteCharms.Entities.Configuration;
namespace LiteCharms.Entities;
namespace LiteCharms.Features.TechShop.Orders.Entities;
[EntityTypeConfiguration<OrderRefundConfiguration, OrderRefund>]
public class OrderRefund : Models.OrderRefund
{
public virtual Order? Order { get; set; }
}
@@ -1,19 +1,21 @@
namespace LiteCharms.Entities.Configuration;
namespace LiteCharms.Features.TechShop.Orders.Entities;
public class OrderRefundConfiguration : IEntityTypeConfiguration<OrderRefund>
{
public void Configure(EntityTypeBuilder<OrderRefund> 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<OrderRefund>(o => o.OrderId);
builder.HasOne(r => r.Order)
.WithMany(o => o.Refunds)
.HasForeignKey(r => r.OrderId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -0,0 +1,24 @@
using LiteCharms.Features.TechShop;
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; }
}
@@ -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; }
@@ -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; }
}
@@ -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<ShopDbContext> contextFactory)
{
public async ValueTask<Result<Guid>> 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<Guid>(new Error($"Customer {request.CustomerId} does not exist."));
if (!await context.ShoppingCarts.AnyAsync(sc => sc.Id == request.ShoppingCartId, cancellationToken))
return Result.Fail<Guid>(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<Guid>(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<Guid>(new Error($"Failed to create customer {request.CustomerId} order using shopping cart {request.ShoppingCartId}."));
}
catch (Exception ex)
{
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<OrderRefund[]>> 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<OrderRefund[]>(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<OrderRefund[]>(new Error($"No refunds found for customer with Id {customerId}."));
}
catch (Exception ex)
{
return Result.Fail<OrderRefund[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Order[]>> 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<Order[]>(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<Order[]>(new Error($"No orders found for customer with Id {customerId}."));
}
catch (Exception ex)
{
return Result.Fail<Order[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<OrderRefund>> 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<OrderRefund>(new Error($"Refund {orderRefundId} not found for the given OrderId: {orderId}"));
}
catch (Exception ex)
{
return Result.Fail<OrderRefund>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<OrderRefund>> 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<OrderRefund>($"Order refund could not be found with id {orderRefundId}");
}
catch (Exception ex)
{
return Result.Fail<OrderRefund>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<OrderRefund[]>> 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<OrderRefund[]>($"Order refunds could not be found with order id {orderId}");
}
catch (Exception ex)
{
return Result.Fail<OrderRefund[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Order[]>> 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<Order[]>(new Error($"No orders found for the specified date range {range.From} - {range.To}."));
}
catch (Exception ex)
{
return Result.Fail<Order[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Guid>> 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<Guid>(new Error($"Order with Id: {request.OrderId} does not exist"));
if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken))
return Result.Fail<Guid>(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<Guid>(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<Guid>(new Error($"Failed to create refund for OrderId: {request.OrderId}"));
}
catch (Exception ex)
{
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> 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<Result> 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));
}
}
}
@@ -0,0 +1,771 @@
// <auto-generated />
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("20260512065421_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LiteCharms.Entities.Customer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customer", (string)null);
});
modelBuilder.Entity("LiteCharms.Entities.Lead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<long?>("AdGroupId")
.HasColumnType("bigint");
b.Property<long?>("AdName")
.HasColumnType("bigint");
b.Property<string>("AppClickId")
.HasColumnType("text");
b.Property<string>("AttributionHash")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("CampaignId")
.HasColumnType("bigint");
b.Property<string>("ClickId")
.HasColumnType("text");
b.Property<string>("ClickLocation")
.HasColumnType("text");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid?>("CustomerId")
.HasColumnType("uuid");
b.Property<long?>("FeedItemId")
.HasColumnType("bigint");
b.Property<string>("Source")
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("WebClickId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Lead", (string)null);
});
modelBuilder.Entity("LiteCharms.Entities.Notification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("CorrelationId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CorrelationIdType")
.HasColumnType("integer");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<int>("Direction")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Errors")
.HasColumnType("jsonb");
b.Property<bool>("HasError")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsHtml")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsInternal")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Platform")
.HasColumnType("integer");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<bool>("Processed")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("Recipient")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RecipientAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Sender")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderName")
.HasColumnType("text");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Notification", (string)null);
});
modelBuilder.Entity("LiteCharms.Entities.Order", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.PrimitiveCollection<string>("Requirements")
.HasColumnType("jsonb");
b.Property<int>("Status")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Terms")
.HasColumnType("jsonb");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Order", (string)null);
});
modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("OrderRefund", (string)null);
});
modelBuilder.Entity("LiteCharms.Entities.Package", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Package", (string)null);
});
modelBuilder.Entity("LiteCharms.Entities.PackageItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("Thumbnails")
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("Product", (string)null);
});
modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<decimal>("Discount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductId");
b.ToTable("ProductPrice", (string)null);
});
modelBuilder.Entity("LiteCharms.Entities.Quote", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("ExpiredAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.HasColumnType("text");
b.Property<Guid?>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.HasIndex("OrderId")
.IsUnique();
b.HasIndex("ShoppingCartId")
.IsUnique();
b.ToTable("Quote", (string)null);
});
modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.HasIndex("OrderId")
.IsUnique();
b.ToTable("ShoppingCart", (string)null);
});
modelBuilder.Entity("LiteCharms.Entities.ShoppingCartItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.Property<int>("Quantity")
.HasColumnType("integer");
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductPriceId");
b.HasIndex("ShoppingCartId");
b.ToTable("ShoppingCartItems");
});
modelBuilder.Entity("LiteCharms.Entities.ShoppingCartPackage", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("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");
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Entities.Order", b =>
{
b.HasOne("LiteCharms.Entities.Customer", "Customer")
.WithMany("Orders")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b =>
{
b.HasOne("LiteCharms.Entities.Order", "Order")
.WithMany("Refunds")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("LiteCharms.Entities.PackageItem", b =>
{
b.HasOne("LiteCharms.Entities.Package", "Package")
.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")
.WithMany("ProductPrices")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Product");
});
modelBuilder.Entity("LiteCharms.Entities.Quote", b =>
{
b.HasOne("LiteCharms.Entities.Customer", "Customer")
.WithMany("Quotes")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
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");
b.Navigation("Customer");
b.Navigation("Order");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b =>
{
b.HasOne("LiteCharms.Entities.Customer", "Customer")
.WithMany("ShoppingCarts")
.HasForeignKey("CustomerId")
.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 =>
{
b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice")
.WithMany()
.HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Entities.ShoppingCart", "ShoppingCart")
.WithMany("ShoppingCartItems")
.HasForeignKey("ShoppingCartId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ProductPrice");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Entities.ShoppingCartPackage", b =>
{
b.HasOne("LiteCharms.Entities.Package", "Package")
.WithMany()
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.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");
b.Navigation("Orders");
b.Navigation("Quotes");
b.Navigation("ShoppingCarts");
});
modelBuilder.Entity("LiteCharms.Entities.Order", b =>
{
b.Navigation("Quote");
b.Navigation("Refunds");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Entities.Package", b =>
{
b.Navigation("PackageItems");
});
modelBuilder.Entity("LiteCharms.Entities.Product", b =>
{
b.Navigation("ProductPrices");
});
modelBuilder.Entity("LiteCharms.Entities.ShoppingCart", b =>
{
b.Navigation("Quote");
b.Navigation("ShoppingCartItems");
b.Navigation("ShoppingCartPackages");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,474 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Customer",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
Company = table.Column<string>(type: "text", nullable: true),
Name = table.Column<string>(type: "text", nullable: false),
LastName = table.Column<string>(type: "text", nullable: false),
Tax = table.Column<string>(type: "text", nullable: true),
Email = table.Column<string>(type: "text", nullable: false),
Discord = table.Column<string>(type: "text", nullable: true),
Slack = table.Column<string>(type: "text", nullable: true),
LinkedIn = table.Column<string>(type: "text", nullable: true),
Whatsapp = table.Column<string>(type: "text", nullable: true),
Website = table.Column<string>(type: "text", nullable: true),
Phone = table.Column<string>(type: "text", nullable: true),
Address = table.Column<string>(type: "text", nullable: true),
City = table.Column<string>(type: "text", nullable: true),
Region = table.Column<string>(type: "text", nullable: true),
Country = table.Column<string>(type: "text", nullable: true),
PostalCode = table.Column<string>(type: "text", nullable: true),
Active = table.Column<bool>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
Direction = table.Column<int>(type: "integer", nullable: false),
Platform = table.Column<int>(type: "integer", nullable: false),
Priority = table.Column<int>(type: "integer", nullable: false),
CorrelationIdType = table.Column<int>(type: "integer", nullable: false),
Sender = table.Column<string>(type: "text", nullable: false),
SenderName = table.Column<string>(type: "text", nullable: true),
Subject = table.Column<string>(type: "text", nullable: false),
Message = table.Column<string>(type: "text", nullable: false),
Recipient = table.Column<string>(type: "text", nullable: false),
RecipientAddress = table.Column<string>(type: "text", nullable: false),
CorrelationId = table.Column<string>(type: "text", nullable: false),
IsHtml = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
IsInternal = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
Processed = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
HasError = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
Errors = table.Column<string>(type: "jsonb", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Notification", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Package",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
Name = table.Column<string>(type: "text", nullable: false),
Summary = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Description = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
ImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
Active = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Package", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Product",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Summary = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Description = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
ImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
Thumbnails = table.Column<string>(type: "jsonb", nullable: true),
Active = table.Column<bool>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
CustomerId = table.Column<Guid>(type: "uuid", nullable: true),
Source = table.Column<string>(type: "text", nullable: true),
ClickId = table.Column<string>(type: "text", nullable: true),
WebClickId = table.Column<string>(type: "text", nullable: true),
AppClickId = table.Column<string>(type: "text", nullable: true),
CampaignId = table.Column<long>(type: "bigint", nullable: true),
AdGroupId = table.Column<long>(type: "bigint", nullable: true),
AdName = table.Column<long>(type: "bigint", nullable: true),
TargetId = table.Column<long>(type: "bigint", nullable: true),
FeedItemId = table.Column<long>(type: "bigint", nullable: true),
ClickLocation = table.Column<string>(type: "text", nullable: true),
AttributionHash = table.Column<string>(type: "text", nullable: false),
Status = table.Column<int>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
Requirements = table.Column<string>(type: "jsonb", nullable: true),
Notes = table.Column<string>(type: "jsonb", nullable: true),
Terms = table.Column<string>(type: "jsonb", nullable: true),
InvoiceUrl = table.Column<string>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
ProductId = table.Column<Guid>(type: "uuid", nullable: false),
Price = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Discount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Active = table.Column<bool>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
OrderId = table.Column<Guid>(type: "uuid", nullable: false),
Reason = table.Column<string>(type: "text", nullable: false),
Amount = table.Column<decimal>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
OrderId = table.Column<Guid>(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<Guid>(type: "uuid", nullable: false),
PackageId = table.Column<Guid>(type: "uuid", nullable: false),
ProductPriceId = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
Active = table.Column<bool>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
ExpiredAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
OrderId = table.Column<Guid>(type: "uuid", nullable: true),
ShoppingCartId = table.Column<Guid>(type: "uuid", nullable: true),
Status = table.Column<int>(type: "integer", nullable: false),
InvoiceUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
Reason = table.Column<string>(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<Guid>(type: "uuid", nullable: false),
ShoppingCartId = table.Column<Guid>(type: "uuid", nullable: false),
ProductPriceId = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
Quantity = table.Column<int>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
ShoppingCartId = table.Column<Guid>(type: "uuid", nullable: false),
PackageId = table.Column<Guid>(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");
}
/// <inheritdoc />
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");
}
}
}
@@ -0,0 +1,871 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customers", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Models.Customer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<string>("LastName")
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customer");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<long?>("AdGroupId")
.HasColumnType("bigint");
b.Property<long?>("AdName")
.HasColumnType("bigint");
b.Property<string>("AppClickId")
.HasColumnType("text");
b.Property<string>("AttributionHash")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("CampaignId")
.HasColumnType("bigint");
b.Property<string>("ClickId")
.HasColumnType("text");
b.Property<string>("ClickLocation")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid?>("CustomerId")
.HasColumnType("uuid");
b.Property<Guid?>("CustomerId1")
.HasColumnType("uuid");
b.Property<long?>("FeedItemId")
.HasColumnType("bigint");
b.Property<string>("Source")
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("CorrelationId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CorrelationIdType")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<int>("Direction")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Errors")
.HasColumnType("jsonb");
b.Property<bool>("HasError")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsHtml")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsInternal")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Platform")
.HasColumnType("integer");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<bool>("Processed")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("RecipientAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RecipientName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderName")
.HasColumnType("text");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.PrimitiveCollection<string>("Requirements")
.HasColumnType("jsonb");
b.Property<int>("Status")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Terms")
.HasColumnType("jsonb");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("Thumbnails")
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("Products", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<decimal>("Discount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<DateTime?>("ExpiredAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.HasColumnType("text");
b.Property<Guid?>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.Property<Guid?>("ProductPriceId1")
.HasColumnType("uuid");
b.Property<int>("Quantity")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1);
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<Guid?>("ShoppingCartId1")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("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
}
}
}
@@ -0,0 +1,936 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
/// <inheritdoc />
public partial class UsedStringTableNames : Migration
{
/// <inheritdoc />
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<DateTime>(
name: "UpdatedAt",
table: "ShoppingCartItems",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTimeOffset),
oldType: "timestamp with time zone");
migrationBuilder.AlterColumn<int>(
name: "Quantity",
table: "ShoppingCartItems",
type: "integer",
nullable: false,
defaultValue: 1,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<DateTime>(
name: "CreatedAt",
table: "ShoppingCartItems",
type: "timestamp with time zone",
nullable: false,
defaultValueSql: "now()",
oldClrType: typeof(DateTimeOffset),
oldType: "timestamp with time zone");
migrationBuilder.AddColumn<Guid>(
name: "ProductPriceId1",
table: "ShoppingCartItems",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "ShoppingCartId1",
table: "ShoppingCartItems",
type: "uuid",
nullable: true);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Customer",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "LastName",
table: "Customer",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Email",
table: "Customer",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<DateTime>(
name: "CreatedAt",
table: "Customer",
type: "timestamp with time zone",
nullable: false,
oldClrType: typeof(DateTimeOffset),
oldType: "timestamp with time zone",
oldDefaultValueSql: "now()");
migrationBuilder.AlterColumn<bool>(
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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Company = table.Column<string>(type: "text", nullable: true),
Name = table.Column<string>(type: "text", nullable: false),
LastName = table.Column<string>(type: "text", nullable: false),
Tax = table.Column<string>(type: "text", nullable: true),
Email = table.Column<string>(type: "text", nullable: false),
Discord = table.Column<string>(type: "text", nullable: true),
Slack = table.Column<string>(type: "text", nullable: true),
LinkedIn = table.Column<string>(type: "text", nullable: true),
Whatsapp = table.Column<string>(type: "text", nullable: true),
Website = table.Column<string>(type: "text", nullable: true),
Phone = table.Column<string>(type: "text", nullable: true),
Address = table.Column<string>(type: "text", nullable: true),
City = table.Column<string>(type: "text", nullable: true),
Region = table.Column<string>(type: "text", nullable: true),
Country = table.Column<string>(type: "text", nullable: true),
PostalCode = table.Column<string>(type: "text", nullable: true),
Active = table.Column<bool>(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<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Summary = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Description = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
ImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
Thumbnails = table.Column<string>(type: "jsonb", nullable: true),
Active = table.Column<bool>(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<Guid>(type: "uuid", nullable: false),
CustomerId1 = table.Column<Guid>(type: "uuid", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CustomerId = table.Column<Guid>(type: "uuid", nullable: true),
Source = table.Column<string>(type: "text", nullable: true),
ClickId = table.Column<string>(type: "text", nullable: true),
WebClickId = table.Column<string>(type: "text", nullable: true),
AppClickId = table.Column<string>(type: "text", nullable: true),
CampaignId = table.Column<long>(type: "bigint", nullable: true),
AdGroupId = table.Column<long>(type: "bigint", nullable: true),
AdName = table.Column<long>(type: "bigint", nullable: true),
TargetId = table.Column<long>(type: "bigint", nullable: true),
FeedItemId = table.Column<long>(type: "bigint", nullable: true),
ClickLocation = table.Column<string>(type: "text", nullable: true),
AttributionHash = table.Column<string>(type: "text", nullable: false),
Status = table.Column<int>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
Requirements = table.Column<string>(type: "jsonb", nullable: true),
Notes = table.Column<string>(type: "jsonb", nullable: true),
Terms = table.Column<string>(type: "jsonb", nullable: true),
InvoiceUrl = table.Column<string>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ProductId = table.Column<Guid>(type: "uuid", nullable: false),
Price = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Discount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Active = table.Column<bool>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
OrderId = table.Column<Guid>(type: "uuid", nullable: false),
Reason = table.Column<string>(type: "text", nullable: false),
Amount = table.Column<decimal>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
OrderId = table.Column<Guid>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ExpiredAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
OrderId = table.Column<Guid>(type: "uuid", nullable: true),
ShoppingCartId = table.Column<Guid>(type: "uuid", nullable: true),
Status = table.Column<int>(type: "integer", nullable: false),
InvoiceUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
Reason = table.Column<string>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
ShoppingCartId = table.Column<Guid>(type: "uuid", nullable: false),
PackageId = table.Column<Guid>(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");
}
/// <inheritdoc />
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<DateTimeOffset>(
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<int>(
name: "Quantity",
table: "ShoppingCartItems",
type: "integer",
nullable: false,
oldClrType: typeof(int),
oldType: "integer",
oldDefaultValue: 1);
migrationBuilder.AlterColumn<DateTimeOffset>(
name: "CreatedAt",
table: "ShoppingCartItems",
type: "timestamp with time zone",
nullable: false,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldDefaultValueSql: "now()");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Customer",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "LastName",
table: "Customer",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Email",
table: "Customer",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<DateTimeOffset>(
name: "CreatedAt",
table: "Customer",
type: "timestamp with time zone",
nullable: false,
defaultValueSql: "now()",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone");
migrationBuilder.AlterColumn<bool>(
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<Guid>(type: "uuid", nullable: false),
CustomerId = table.Column<Guid>(type: "uuid", nullable: true),
AdGroupId = table.Column<long>(type: "bigint", nullable: true),
AdName = table.Column<long>(type: "bigint", nullable: true),
AppClickId = table.Column<string>(type: "text", nullable: true),
AttributionHash = table.Column<string>(type: "text", nullable: false),
CampaignId = table.Column<long>(type: "bigint", nullable: true),
ClickId = table.Column<string>(type: "text", nullable: true),
ClickLocation = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
FeedItemId = table.Column<long>(type: "bigint", nullable: true),
Source = table.Column<string>(type: "text", nullable: true),
Status = table.Column<int>(type: "integer", nullable: false),
TargetId = table.Column<long>(type: "bigint", nullable: true),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
WebClickId = table.Column<string>(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<Guid>(type: "uuid", nullable: false),
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
InvoiceUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
Notes = table.Column<string>(type: "jsonb", nullable: true),
Requirements = table.Column<string>(type: "jsonb", nullable: true),
Status = table.Column<int>(type: "integer", nullable: false),
Terms = table.Column<string>(type: "jsonb", nullable: true),
UpdatedAt = table.Column<DateTimeOffset>(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<Guid>(type: "uuid", nullable: false),
Active = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
Description = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
ImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
Name = table.Column<string>(type: "text", nullable: false),
Summary = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Thumbnails = table.Column<string>(type: "jsonb", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Product", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OrderRefund",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
OrderId = table.Column<Guid>(type: "uuid", nullable: false),
Amount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
Reason = table.Column<string>(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<Guid>(type: "uuid", nullable: false),
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
OrderId = table.Column<Guid>(type: "uuid", nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTimeOffset>(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<Guid>(type: "uuid", nullable: false),
ProductId = table.Column<Guid>(type: "uuid", nullable: false),
Active = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
Discount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Price = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(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<Guid>(type: "uuid", nullable: false),
CustomerId = table.Column<Guid>(type: "uuid", nullable: false),
OrderId = table.Column<Guid>(type: "uuid", nullable: true),
ShoppingCartId = table.Column<Guid>(type: "uuid", nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
ExpiredAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
InvoiceUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
Reason = table.Column<string>(type: "text", nullable: true),
Status = table.Column<int>(type: "integer", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(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<Guid>(type: "uuid", nullable: false),
PackageId = table.Column<Guid>(type: "uuid", nullable: false),
ShoppingCartId = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(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);
}
}
}
@@ -0,0 +1,776 @@
// <auto-generated />
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("20260515055221_FixedLeadCustomerRelationship")]
partial class FixedLeadCustomerRelationship
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customers", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<long?>("AdGroupId")
.HasColumnType("bigint");
b.Property<long?>("AdName")
.HasColumnType("bigint");
b.Property<string>("AppClickId")
.HasColumnType("text");
b.Property<string>("AttributionHash")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("CampaignId")
.HasColumnType("bigint");
b.Property<string>("ClickId")
.HasColumnType("text");
b.Property<string>("ClickLocation")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid?>("CustomerId")
.HasColumnType("uuid");
b.Property<long?>("FeedItemId")
.HasColumnType("bigint");
b.Property<string>("Source")
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("CorrelationId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CorrelationIdType")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<int>("Direction")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Errors")
.HasColumnType("jsonb");
b.Property<bool>("HasError")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsHtml")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsInternal")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Platform")
.HasColumnType("integer");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<bool>("Processed")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("RecipientAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RecipientName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderName")
.HasColumnType("text");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.PrimitiveCollection<string>("Requirements")
.HasColumnType("jsonb");
b.Property<int>("Status")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Terms")
.HasColumnType("jsonb");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("Thumbnails")
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("Products", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<decimal>("Discount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<DateTime?>("ExpiredAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.HasColumnType("text");
b.Property<Guid?>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.Property<int>("Quantity")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1);
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("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
}
}
}
@@ -0,0 +1,166 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
/// <inheritdoc />
public partial class FixedLeadCustomerRelationship : Migration
{
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Leads_Customers_CustomerId",
table: "Leads");
migrationBuilder.AddColumn<Guid>(
name: "ProductPriceId1",
table: "ShoppingCartItems",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "ShoppingCartId1",
table: "ShoppingCartItems",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "CustomerId1",
table: "Leads",
type: "uuid",
nullable: true);
migrationBuilder.CreateTable(
name: "Customer",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Active = table.Column<bool>(type: "boolean", nullable: false),
Address = table.Column<string>(type: "text", nullable: true),
City = table.Column<string>(type: "text", nullable: true),
Company = table.Column<string>(type: "text", nullable: true),
Country = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Discord = table.Column<string>(type: "text", nullable: true),
Email = table.Column<string>(type: "text", nullable: true),
LastName = table.Column<string>(type: "text", nullable: true),
LinkedIn = table.Column<string>(type: "text", nullable: true),
Name = table.Column<string>(type: "text", nullable: true),
Phone = table.Column<string>(type: "text", nullable: true),
PostalCode = table.Column<string>(type: "text", nullable: true),
Region = table.Column<string>(type: "text", nullable: true),
Slack = table.Column<string>(type: "text", nullable: true),
Tax = table.Column<string>(type: "text", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Website = table.Column<string>(type: "text", nullable: true),
Whatsapp = table.Column<string>(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");
}
}
}
@@ -0,0 +1,788 @@
// <auto-generated />
using System;
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
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customers", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<long?>("AdGroupId")
.HasColumnType("bigint");
b.Property<long?>("AdName")
.HasColumnType("bigint");
b.Property<string>("AppClickId")
.HasColumnType("text");
b.Property<string>("AttributionHash")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("CampaignId")
.HasColumnType("bigint");
b.Property<string>("ClickId")
.HasColumnType("text");
b.Property<string>("ClickLocation")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid?>("CustomerId")
.HasColumnType("uuid");
b.Property<long?>("FeedItemId")
.HasColumnType("bigint");
b.Property<string>("Source")
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("CorrelationId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CorrelationIdType")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<int>("Direction")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Errors")
.HasColumnType("jsonb");
b.Property<bool>("HasError")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsHtml")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsInternal")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Platform")
.HasColumnType("integer");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<bool>("Processed")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("RecipientAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RecipientName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderName")
.HasColumnType("text");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.PrimitiveCollection<string>("Requirements")
.HasColumnType("jsonb");
b.Property<int>("Status")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Terms")
.HasColumnType("jsonb");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<ProductMetadata>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("Thumbnails")
.HasColumnType("jsonb");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<decimal>("Discount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<DateTime?>("ExpiredAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.HasColumnType("text");
b.Property<Guid?>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.Property<int>("Quantity")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1);
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("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
}
}
}
@@ -0,0 +1,71 @@
using System;
using LiteCharms.Features.TechShop.Products.Models;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
/// <inheritdoc />
public partial class AddedProductMetadata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<bool>(
name: "Active",
table: "Products",
type: "boolean",
nullable: false,
defaultValue: false,
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: true);
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "Products",
type: "timestamp with time zone",
nullable: false,
defaultValueSql: "now()");
migrationBuilder.AddColumn<ProductMetadata>(
name: "Metadata",
table: "Products",
type: "jsonb",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "UpdatedAt",
table: "Products",
type: "timestamp with time zone",
nullable: true);
}
/// <inheritdoc />
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<bool>(
name: "Active",
table: "Products",
type: "boolean",
nullable: false,
defaultValue: true,
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: false);
}
}
}
@@ -0,0 +1,785 @@
// <auto-generated />
using System;
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customers", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<long?>("AdGroupId")
.HasColumnType("bigint");
b.Property<long?>("AdName")
.HasColumnType("bigint");
b.Property<string>("AppClickId")
.HasColumnType("text");
b.Property<string>("AttributionHash")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("CampaignId")
.HasColumnType("bigint");
b.Property<string>("ClickId")
.HasColumnType("text");
b.Property<string>("ClickLocation")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid?>("CustomerId")
.HasColumnType("uuid");
b.Property<long?>("FeedItemId")
.HasColumnType("bigint");
b.Property<string>("Source")
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("CorrelationId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CorrelationIdType")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<int>("Direction")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Errors")
.HasColumnType("jsonb");
b.Property<bool>("HasError")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsHtml")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsInternal")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Platform")
.HasColumnType("integer");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<bool>("Processed")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("RecipientAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RecipientName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderName")
.HasColumnType("text");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.PrimitiveCollection<string>("Requirements")
.HasColumnType("jsonb");
b.Property<int>("Status")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Terms")
.HasColumnType("jsonb");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<ProductMetadata>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("Thumbnails")
.HasColumnType("jsonb");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<decimal>("Discount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<DateTime?>("ExpiredAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.HasColumnType("text");
b.Property<Guid?>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.Property<int>("Quantity")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1);
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<DateTime?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("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
}
}
}
@@ -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<ShopDbContext> options) : DbContext(options)
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Lead> Leads { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderRefund> OrderRefunds { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<ProductPrice> ProductPrices { get; set; }
public DbSet<Notification> Notifications { get; set; }
public DbSet<Quote> Quotes { get; set; }
public DbSet<ShoppingCart> ShoppingCarts { get; set; }
public DbSet<ShoppingCartItem> ShoppingCartItems { get; set; }
public DbSet<Package> Packages { get; set; }
public DbSet<PackageItem> PackageItems { get; set; }
public DbSet<ShoppingCartPackage> ShoppingCartPackages { get; set; }
}
@@ -0,0 +1,21 @@
using static LiteCharms.Features.TechShop.Extensions.Postgres;
namespace LiteCharms.Features.TechShop.Postgres;
public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext>
{
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<ShopDbContext>();
optionsBuilder.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName));
return new ShopDbContext(optionsBuilder.Options);
}
}
@@ -1,9 +1,8 @@
using LiteCharms.Entities.Configuration;
namespace LiteCharms.Entities;
namespace LiteCharms.Features.TechShop.Products.Entities;
[EntityTypeConfiguration<ProductConfiguration, Product>]
public class Product : Models.Product
{
public virtual ICollection<ProductPrice>? ProductPrices { get; set; }
}
@@ -0,0 +1,20 @@
namespace LiteCharms.Features.TechShop.Products.Entities;
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> 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);
}
}
@@ -1,6 +1,4 @@
using LiteCharms.Entities.Configuration;
namespace LiteCharms.Entities;
namespace LiteCharms.Features.TechShop.Products.Entities;
[EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>]
public class ProductPrice : Models.ProductPrice
@@ -1,22 +1,23 @@
namespace LiteCharms.Entities.Configuration;
namespace LiteCharms.Features.TechShop.Products.Entities;
public class ProductPriceConfiguration : IEntityTypeConfiguration<ProductPrice>
{
public void Configure(EntityTypeBuilder<ProductPrice> 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);
}
}
@@ -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<string> Thumbnails { get; set; } = [];
}
@@ -0,0 +1,24 @@
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; }
}
@@ -0,0 +1,12 @@
namespace LiteCharms.Features.TechShop.Products.Models;
public class ProductMetadata
{
public string? Manufacturer { get; set; }
public string? ManufactureDate { get; set; }
public string? CopyrightInfo { get; set; }
public string? SerialNumber { get; set; }
}
@@ -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; }
@@ -0,0 +1,16 @@
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; }
}
@@ -0,0 +1,277 @@
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<ShopDbContext> contextFactory)
{
public async ValueTask<Result> 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<Result> 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<Result<Guid>> 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<Guid>($"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<Result<Guid>> 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<Result<Product>> 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<Product>(new Error($"Product with ID {productId} not found."));
}
catch (Exception ex)
{
return Result.Fail<Product>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<ProductPrice>> 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<ProductPrice>(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<ProductPrice>(new Error($"Product price {productPriceId} not found."));
}
catch (Exception ex)
{
return Result.Fail<ProductPrice>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<ProductPrice[]>> 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<ProductPrice[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Product[]>> 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<Product[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Guid>> 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<Guid>($"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<Guid>("Reverted to old price, creation of new price failed")
: Result.Fail<Guid>($"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<Result> 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<Result> 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));
}
}
}
@@ -0,0 +1,15 @@
using LiteCharms.Features.TechShop.Customers.Entities;
using LiteCharms.Features.TechShop.Orders.Entities;
using LiteCharms.Features.TechShop.ShoppingCarts.Entities;
namespace LiteCharms.Features.TechShop.Quotes.Entities;
[EntityTypeConfiguration<QuoteConfiguration, Quote>]
public class Quote : Models.Quote
{
public virtual Customer? Customer { get; set; }
public virtual Order? Order { get; set; }
public virtual ShoppingCart? ShoppingCart { get; set; }
}
@@ -0,0 +1,33 @@
namespace LiteCharms.Features.TechShop.Quotes.Entities;
public class QuoteConfiguration : IEntityTypeConfiguration<Quote>
{
public void Configure(EntityTypeBuilder<Quote> 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<int>();
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<Quote>(q => q.OrderId);
builder.HasOne(q => q.ShoppingCart)
.WithOne(o => o.Quote)
.HasForeignKey<Quote>(q => q.ShoppingCartId);
}
}
@@ -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; }
}
@@ -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<ShopDbContext> contextFactory)
{
public async ValueTask<Result> 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<Result> 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<Result<Quote[]>> 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<Quote[]>(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<Quote[]>(new Error($"No quotes found for customer with Id {customerId}."));
}
catch (Exception ex)
{
return Result.Fail<Quote[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Quote>> 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<Quote>(new Error($"Quote with ID {quoteId} not found."));
}
catch (Exception ex)
{
return Result.Fail<Quote>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Quote[]>> 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<Quote[]>(new Error($"No quotes found for the specified date range {range.From} - {range.To}."));
}
catch (Exception ex)
{
return Result.Fail<Quote[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> 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));
}
}
}
@@ -0,0 +1,19 @@
using LiteCharms.Features.TechShop.Customers.Entities;
using LiteCharms.Features.TechShop.Orders.Entities;
using LiteCharms.Features.TechShop.Quotes.Entities;
namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
[EntityTypeConfiguration<ShoppingCartConfiguration, ShoppingCart>]
public class ShoppingCart : Models.ShoppingCart
{
public virtual Customer? Customer { get; set; }
public virtual Order? Order { get; set; }
public virtual Quote? Quote { get; set; }
public virtual ICollection<ShoppingCartItem>? ShoppingCartItems { get; set; }
public virtual ICollection<ShoppingCartPackage>? ShoppingCartPackages { get; set; }
}
@@ -0,0 +1,26 @@
namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
public class ShoppingCartConfiguration : IEntityTypeConfiguration<ShoppingCart>
{
public void Configure(EntityTypeBuilder<ShoppingCart> 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<ShoppingCart>(s => s.OrderId)
.OnDelete(DeleteBehavior.SetNull);
}
}
@@ -0,0 +1,11 @@
using LiteCharms.Features.TechShop.Products.Entities;
namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
[EntityTypeConfiguration<ShoppingCartItemConfiguration, ShoppingCartItem>]
public class ShoppingCartItem : Models.ShoppingCartItem
{
public virtual ShoppingCart? ShoppingCart { get; set; }
public virtual ProductPrice? ProductPrice { get; set; }
}
@@ -0,0 +1,28 @@
namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
public class ShoppingCartItemConfiguration : IEntityTypeConfiguration<ShoppingCartItem>
{
public void Configure(EntityTypeBuilder<ShoppingCartItem> 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);
}
}
@@ -0,0 +1,11 @@
using LiteCharms.Features.TechShop.CartPackages.Entities;
namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
[EntityTypeConfiguration<ShoppingCartPackageConfiguration, ShoppingCartPackage>]
public class ShoppingCartPackage : Models.ShoppingCartPackage
{
public virtual ShoppingCart? ShoppingCart { get; set; }
public virtual Package? Package { get; set; }
}
@@ -0,0 +1,26 @@
namespace LiteCharms.Features.TechShop.ShoppingCarts.Entities;
public class ShoppingCartPackageConfiguration : IEntityTypeConfiguration<ShoppingCartPackage>
{
public void Configure(EntityTypeBuilder<ShoppingCartPackage> 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);
}
}
@@ -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; }
}
@@ -0,0 +1,16 @@
namespace LiteCharms.Features.TechShop.ShoppingCarts.Models;
public class ShoppingCartItem
{
public Guid Id { get; set; }
public Guid ShoppingCartId { get; set; }
public Guid ProductPriceId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public int Quantity { get; set; }
}
@@ -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; }
}
@@ -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<ShopDbContext> contextFactory)
{
public async ValueTask<Result> 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<Result> 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<Result<Guid>> 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<Guid>($"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<Guid>($"Failed to create shopping cart for customer id {customerId}");
}
catch (Exception ex)
{
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> 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<Result<ShoppingCart[]>> 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<ShoppingCart[]>(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<ShoppingCart[]>(new Error($"No shopping carts found for customer with Id {customerId}."));
}
catch (Exception ex)
{
return Result.Fail<ShoppingCart[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<ShoppingCart>> 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<ShoppingCart>($"Failed to find shopping cart with id {shoppingCartId}");
}
catch (Exception ex)
{
return Result.Fail<ShoppingCart>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<ShoppingCartItem[]>> 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<ShoppingCartItem[]>($"Failed to retrieve shopping cart items with id {shoppingCartId}");
}
catch (Exception ex)
{
return Result.Fail<ShoppingCartItem[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<ShoppingCartPackage[]>> 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<ShoppingCartPackage[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> 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<Result> 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<Result> 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));
}
}
}
@@ -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": "*"
}
+35
View File
@@ -0,0 +1,35 @@
using LiteCharms.Features.Extensions;
namespace LiteCharms.Features.Tests;
public class Fixture : IDisposable
{
public IConfiguration Configuration { get; set; }
public IServiceProvider Services { get; set; }
public IMediator Mediator { get; set; }
public Fixture()
{
Configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddUserSecrets<Fixture>()
.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
Services = new ServiceCollection()
.AddMediator()
.AddLogging()
.AddEmailServiceBus()
.AddGarageS3(Configuration)
.AddEmailServices(Configuration)
.AddSingleton(Configuration)
.BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>();
}
public void Dispose() { }
}
@@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<UserSecretsId>62fa604a-1340-4edb-9ddd-3305fcf46fca</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<!-- Global Usings -->
<ItemGroup>
<Using Include="Mediator" />
<Using Include="Xunit.Abstractions" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Microsoft.Extensions.Configuration" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,54 @@
using LiteCharms.Features.S3.Abstractions;
namespace LiteCharms.Features.Tests;
public class S3ServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
[Fact]
public async Task BookshopS3Service_MustReturnUrl()
{
var service = fixture.Services.GetKeyedService<IS3Service>(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<IS3Service>(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);
}
}
@@ -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": "*"
}
@@ -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; } = SouthAfricanTimeZone.UtcNow();
public string CorrelationId { get; set; } = Guid.CreateVersion7().ToString();
}
@@ -0,0 +1,12 @@
namespace LiteCharms.Features.Abstractions;
public interface IEvent : INotification
{
Guid Id { get; set; }
string Name { get; set; }
DateTimeOffset EnqueueAt { get; set; }
string CorrelationId { get; set; }
}
@@ -1,64 +0,0 @@
namespace LiteCharms.Features.Customers.Commands;
public class CreateCustomerCommand : IRequest<Result<Guid>>
{
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);
}
}
@@ -1,48 +0,0 @@
using LiteCharms.Infrastructure.Database;
namespace LiteCharms.Features.Customers.Commands.Handlers;
public class CreateCustomerCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<CreateCustomerCommand, Result<Guid>>
{
public async ValueTask<Result<Guid>> 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<Guid>(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<Guid>(new Error($"Failed to create customer {customerEmail}"));
}
catch (Exception ex)
{
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -1,38 +0,0 @@
using LiteCharms.Infrastructure.Database;
namespace LiteCharms.Features.Customers.Commands.Handlers;
public class RefundCustomerCommandHandler(IDbContextFactory<LeadGeneratorDbContext> contextFactory) : IRequestHandler<RefundCustomerCommand, Result<Guid>>
{
public async ValueTask<Result<Guid>> 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<Guid>(new Error($"Order with Id: {request.OrderId} does not exist"));
if (!await context.Customers.AnyAsync(c => c.Id == request.CustomerId, cancellationToken))
return Result.Fail<Guid>(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<Guid>(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<Guid>(new Error($"Failed to create refund for OrderId: {request.OrderId}"));
}
catch (Exception ex)
{
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
}

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