Compare commits

...

3 Commits

Author SHA1 Message Date
Tom Pipinic e9d43c4dac moved to src folder
2 years ago
Tom Pipinic 7b6068e108 Refactoring namespaces
2 years ago
Matija Koželj 8aea377d60 Initial commit
2 years ago

@ -0,0 +1,226 @@
# 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 = tab
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = true
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_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_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_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 = false
dotnet_style_allow_statement_immediately_after_block_experimental = false
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = true
csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true
# 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 = false
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_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
# Code-block preferences
csharp_prefer_braces = when_multiline
csharp_prefer_simple_using_statement = true
csharp_style_namespace_declarations = file_scoped
csharp_style_prefer_method_group_conversion = 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_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_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_lines_between_consecutive_braces_experimental = false
csharp_style_allow_embedded_statements_on_same_line_experimental = false
#### 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 = false
#### 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

@ -0,0 +1,149 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.32916.344
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Logistics.Types.Model", "src\Connected.Logistics.Types.Model\Connected.Logistics.Types.Model.csproj", "{A673CACA-8A88-4AE1-B6C4-E31CED477981}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Logistics.Documents.Model", "src\Connected.Logistics.Documents.Model\Connected.Logistics.Documents.Model.csproj", "{BCB56C1C-253F-4BB8-88A2-06653ED3E232}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{04C7CB0E-A6E1-4CCC-AF76-B199137278B7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Logistics.Processes.Receive", "src\Connected.Logistics.Processes.Receive\Connected.Logistics.Processes.Receive.csproj", "{9EB45FF3-4910-4FEA-9553-97410C350AB9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Common", "..\Connected.Common\src\Connected.Common\Connected.Common.csproj", "{A4BF05CA-F790-4296-8647-18CEB1801637}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Common.Model", "..\Connected.Common\src\Connected.Common.Model\Connected.Common.Model.csproj", "{652D8B33-1485-43DD-9BDA-EE8103C2E0C8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected", "..\Connected\src\Connected\Connected.csproj", "{6D40EEF9-9DB8-4755-B307-485523B1E3EF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Logistics.Documents", "src\Connected.Logistics.Documents\Connected.Logistics.Documents.csproj", "{D808FC0D-355C-41D9-8560-DF99A963CA6A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Contacts.Types.Model", "..\Connected.Customers\src\Connected.Contacts.Types.Model\Connected.Contacts.Types.Model.csproj", "{67EF282C-7CDE-4D85-A628-001306629762}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.ServiceModel", "..\Connected.Framework.ServiceModel\src\Connected.ServiceModel\Connected.ServiceModel.csproj", "{6332E9A7-9EE8-4979-94AF-D297E4BBEA26}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Logistics.Stock.Model", "src\Connected.Logistics.Stock.Model\Connected.Logistics.Stock.Model.csproj", "{415704FB-3DCA-41F9-A2D1-0FB72D346532}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Logistics.Stock", "src\Connected.Logistics.Stock\Connected.Logistics.Stock.csproj", "{8C4B2009-1DF6-4358-ABED-64E1F6076CCD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Logistics.Types", "src\Connected.Logistics.Types\Connected.Logistics.Types.csproj", "{A7B4FE4C-9D04-41FC-B036-78FAB6B15899}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Data", "..\connected.framework\src\Connected.Data\Connected.Data.csproj", "{39BE3D38-7D70-4EAF-B249-26E973CB3C58}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Entities", "..\connected.framework\src\Connected.Entities\Connected.Entities.csproj", "{3CAD2F7F-2C32-44E0-A632-907D020FD511}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Hosting", "..\connected.framework\src\Connected.Hosting\Connected.Hosting.csproj", "{ACEE4A52-F398-4149-92F2-0AF80E9B0923}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Middleware", "..\connected.framework\src\Connected.Middleware\Connected.Middleware.csproj", "{349FCD5A-9E47-49BA-8DC2-59E76E99141B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Runtime", "..\connected.framework\src\Connected.Runtime\Connected.Runtime.csproj", "{43AA67F4-177C-43C9-B34F-A9B780A0278B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Services", "..\connected.framework\src\Connected.Services\Connected.Services.csproj", "{69A42F1E-A8FF-4F9A-8036-E53DA8BB7F0C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Validation", "..\connected.framework\src\Connected.Validation\Connected.Validation.csproj", "{1A4834EB-3E24-43F8-8205-E2F2A68D0B54}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A673CACA-8A88-4AE1-B6C4-E31CED477981}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A673CACA-8A88-4AE1-B6C4-E31CED477981}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A673CACA-8A88-4AE1-B6C4-E31CED477981}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A673CACA-8A88-4AE1-B6C4-E31CED477981}.Release|Any CPU.Build.0 = Release|Any CPU
{BCB56C1C-253F-4BB8-88A2-06653ED3E232}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BCB56C1C-253F-4BB8-88A2-06653ED3E232}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BCB56C1C-253F-4BB8-88A2-06653ED3E232}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BCB56C1C-253F-4BB8-88A2-06653ED3E232}.Release|Any CPU.Build.0 = Release|Any CPU
{9EB45FF3-4910-4FEA-9553-97410C350AB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9EB45FF3-4910-4FEA-9553-97410C350AB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9EB45FF3-4910-4FEA-9553-97410C350AB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9EB45FF3-4910-4FEA-9553-97410C350AB9}.Release|Any CPU.Build.0 = Release|Any CPU
{A4BF05CA-F790-4296-8647-18CEB1801637}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4BF05CA-F790-4296-8647-18CEB1801637}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4BF05CA-F790-4296-8647-18CEB1801637}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4BF05CA-F790-4296-8647-18CEB1801637}.Release|Any CPU.Build.0 = Release|Any CPU
{652D8B33-1485-43DD-9BDA-EE8103C2E0C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{652D8B33-1485-43DD-9BDA-EE8103C2E0C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{652D8B33-1485-43DD-9BDA-EE8103C2E0C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{652D8B33-1485-43DD-9BDA-EE8103C2E0C8}.Release|Any CPU.Build.0 = Release|Any CPU
{6D40EEF9-9DB8-4755-B307-485523B1E3EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D40EEF9-9DB8-4755-B307-485523B1E3EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D40EEF9-9DB8-4755-B307-485523B1E3EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D40EEF9-9DB8-4755-B307-485523B1E3EF}.Release|Any CPU.Build.0 = Release|Any CPU
{D808FC0D-355C-41D9-8560-DF99A963CA6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D808FC0D-355C-41D9-8560-DF99A963CA6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D808FC0D-355C-41D9-8560-DF99A963CA6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D808FC0D-355C-41D9-8560-DF99A963CA6A}.Release|Any CPU.Build.0 = Release|Any CPU
{67EF282C-7CDE-4D85-A628-001306629762}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{67EF282C-7CDE-4D85-A628-001306629762}.Debug|Any CPU.Build.0 = Debug|Any CPU
{67EF282C-7CDE-4D85-A628-001306629762}.Release|Any CPU.ActiveCfg = Release|Any CPU
{67EF282C-7CDE-4D85-A628-001306629762}.Release|Any CPU.Build.0 = Release|Any CPU
{6332E9A7-9EE8-4979-94AF-D297E4BBEA26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6332E9A7-9EE8-4979-94AF-D297E4BBEA26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6332E9A7-9EE8-4979-94AF-D297E4BBEA26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6332E9A7-9EE8-4979-94AF-D297E4BBEA26}.Release|Any CPU.Build.0 = Release|Any CPU
{415704FB-3DCA-41F9-A2D1-0FB72D346532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{415704FB-3DCA-41F9-A2D1-0FB72D346532}.Debug|Any CPU.Build.0 = Debug|Any CPU
{415704FB-3DCA-41F9-A2D1-0FB72D346532}.Release|Any CPU.ActiveCfg = Release|Any CPU
{415704FB-3DCA-41F9-A2D1-0FB72D346532}.Release|Any CPU.Build.0 = Release|Any CPU
{8C4B2009-1DF6-4358-ABED-64E1F6076CCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C4B2009-1DF6-4358-ABED-64E1F6076CCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C4B2009-1DF6-4358-ABED-64E1F6076CCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C4B2009-1DF6-4358-ABED-64E1F6076CCD}.Release|Any CPU.Build.0 = Release|Any CPU
{A7B4FE4C-9D04-41FC-B036-78FAB6B15899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7B4FE4C-9D04-41FC-B036-78FAB6B15899}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7B4FE4C-9D04-41FC-B036-78FAB6B15899}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7B4FE4C-9D04-41FC-B036-78FAB6B15899}.Release|Any CPU.Build.0 = Release|Any CPU
{39BE3D38-7D70-4EAF-B249-26E973CB3C58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39BE3D38-7D70-4EAF-B249-26E973CB3C58}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39BE3D38-7D70-4EAF-B249-26E973CB3C58}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39BE3D38-7D70-4EAF-B249-26E973CB3C58}.Release|Any CPU.Build.0 = Release|Any CPU
{3CAD2F7F-2C32-44E0-A632-907D020FD511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3CAD2F7F-2C32-44E0-A632-907D020FD511}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3CAD2F7F-2C32-44E0-A632-907D020FD511}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3CAD2F7F-2C32-44E0-A632-907D020FD511}.Release|Any CPU.Build.0 = Release|Any CPU
{ACEE4A52-F398-4149-92F2-0AF80E9B0923}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ACEE4A52-F398-4149-92F2-0AF80E9B0923}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ACEE4A52-F398-4149-92F2-0AF80E9B0923}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ACEE4A52-F398-4149-92F2-0AF80E9B0923}.Release|Any CPU.Build.0 = Release|Any CPU
{349FCD5A-9E47-49BA-8DC2-59E76E99141B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{349FCD5A-9E47-49BA-8DC2-59E76E99141B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{349FCD5A-9E47-49BA-8DC2-59E76E99141B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{349FCD5A-9E47-49BA-8DC2-59E76E99141B}.Release|Any CPU.Build.0 = Release|Any CPU
{43AA67F4-177C-43C9-B34F-A9B780A0278B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43AA67F4-177C-43C9-B34F-A9B780A0278B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43AA67F4-177C-43C9-B34F-A9B780A0278B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43AA67F4-177C-43C9-B34F-A9B780A0278B}.Release|Any CPU.Build.0 = Release|Any CPU
{69A42F1E-A8FF-4F9A-8036-E53DA8BB7F0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69A42F1E-A8FF-4F9A-8036-E53DA8BB7F0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69A42F1E-A8FF-4F9A-8036-E53DA8BB7F0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69A42F1E-A8FF-4F9A-8036-E53DA8BB7F0C}.Release|Any CPU.Build.0 = Release|Any CPU
{1A4834EB-3E24-43F8-8205-E2F2A68D0B54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A4834EB-3E24-43F8-8205-E2F2A68D0B54}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A4834EB-3E24-43F8-8205-E2F2A68D0B54}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A4834EB-3E24-43F8-8205-E2F2A68D0B54}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A4BF05CA-F790-4296-8647-18CEB1801637} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{652D8B33-1485-43DD-9BDA-EE8103C2E0C8} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{6D40EEF9-9DB8-4755-B307-485523B1E3EF} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{67EF282C-7CDE-4D85-A628-001306629762} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{6332E9A7-9EE8-4979-94AF-D297E4BBEA26} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{39BE3D38-7D70-4EAF-B249-26E973CB3C58} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{3CAD2F7F-2C32-44E0-A632-907D020FD511} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{ACEE4A52-F398-4149-92F2-0AF80E9B0923} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{349FCD5A-9E47-49BA-8DC2-59E76E99141B} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{43AA67F4-177C-43C9-B34F-A9B780A0278B} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{69A42F1E-A8FF-4F9A-8036-E53DA8BB7F0C} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{1A4834EB-3E24-43F8-8205-E2F2A68D0B54} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FC558A1B-D7D9-4869-9589-1877A5E64720}
EndGlobalSection
EndGlobal

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Connected.Logistics.Documents</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Connected.Common\src\Connected.Common.Model\Connected.Common.Model.csproj" />
<ProjectReference Include="..\..\..\Connected.Customers\src\Connected.Contacts.Types.Model\Connected.Contacts.Types.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,5 @@
namespace Connected.Logistics.Documents;
public static class DocumentUrls
{
public const string Receives = "/logistics/documents/receives";
}

@ -0,0 +1,12 @@
using Connected.Common.Documents;
namespace Connected.Logistics.Documents.Receive;
public interface IReceiveDocument : IDocument<int>
{
int? Supplier { get; init; }
DateTimeOffset? ReceiveDate { get; init; }
int ItemCount { get; init; }
int OpenItemCount { get; init; }
}

@ -0,0 +1,84 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Common.Documents;
using Connected.ServiceModel;
namespace Connected.Logistics.Documents.Receive;
/// <summary>
/// Represents service for the <see cref="IReceiveDocument"/> document.
/// </summary>
[Service]
[ServiceUrl(DocumentUrls.Receives)]
public interface IReceiveDocumentService : IDocumentService<int, long>
{
/// <summary>
/// Inserts a new <see cref="IReceiveDocument"/>.
/// </summary>
/// <param name="args">The arguments containing the properties of the new document.</param>
/// <returns>The id of the newly inserted document.</returns>
Task<int> Insert(InsertReceiveDocumentArgs args);
/// <summary>
/// Updates <see cref="IReceiveDocument"/> document.
/// </summary>
/// <param name="args">The arguments containing changed properties of the document.</param>
Task Update(UpdateReceiveDocumentArgs args);
/// <summary>
/// Performs partial update on the <see cref="IReceiveDocument"/> for the properties specified
/// in arguments.
/// </summary>
/// <param name="args">The arguments containing properties that need to be updated.</param>
Task Patch(PatchArgs<int> args);
/// <summary>
/// Deletes <see cref="IReceiveDocument"/> from the storage.
/// </summary>
/// <param name="args">The arguments containing the id of the document that is about to be deleted.</param>
Task Delete(PrimaryKeyArgs<int> args);
/// <summary>
/// Selects <see cref="IReceiveDocument"/> for the specified id.
/// </summary>
/// <param name="args">The arguments containing the id.</param>
/// <returns><see cref="IReceiveDocument"/> if found, <c>null</c> otherwise.</returns>
Task<IReceiveDocument?> Select(PrimaryKeyArgs<int> args);
/// <summary>
/// Searches <see cref="IReceiveDocument">documents</see> for the specified criteria.
/// </summary>
/// <param name="args">The arguments containing the query criteria.</param>
/// <returns>The list of <see cref="IReceiveDocument"/> documents that matches the search criteria.</returns>
Task<ImmutableList<IReceiveDocument>> Query(QueryArgs? args);
/// <summary>
/// Inserts a new <see cref="IReceiveItem"/> into the <see cref="IReceiveDocument"/> document.
/// </summary>
/// <param name="args">The arguments containing the properties of the new item.</param>
/// <returns>The id of the newly inserted <see cref="IReceiveItem"/> item.</returns>
Task<long> InsertItem(InsertReceiveItemArgs args);
/// <summary>
/// Updates <see cref="IReceiveItem"/>.
/// </summary>
/// <param name="args">The arguments containing the properties to be updated.</param>
Task UpdateItem(UpdateReceiveItemArgs args);
/// <summary>
/// Permanently deleted the <see cref="IReceiveItem"/>.
/// </summary>
/// <param name="args">The arguments containing the id of the item to be deleted.</param>
Task DeleteItem(PrimaryKeyArgs<long> args);
/// <summary>
/// Queries the <see cref="IReceiveItem"/> items for the specified <see cref="IReceiveDocument"/>.
/// </summary>
/// <param name="args">The arguments containing the id of the document for which the items to be
/// queried.</param>
/// <returns>The list of items that belong to the specified document.</returns>
Task<ImmutableList<IReceiveItem>> QueryItems(PrimaryKeyArgs<int> args);
/// <summary>
/// Selects the <see cref="IReceiveItem"/> item for the specified id.
/// </summary>
/// <param name="args">The arguments containing the id of the item.</param>
/// <returns>The <see cref="IReceiveItem"/> if found, <c>null</c> otherwise.</returns>
Task<IReceiveItem?> SelectItem(PrimaryKeyArgs<long> args);
/// <summary>
/// Select the <see cref="IReceiveItem"/> for the specified entity and entity id from the
/// specified document.
/// </summary>
/// <param name="args">The arguments containing criteria values.</param>
/// <returns>A first <see cref="IReceiveItem"/> that matches the criteria, <c>null</c> otherwise.</returns>
Task<IReceiveItem?> SelectItem(SelectReceiveItemArgs args);
}

@ -0,0 +1,10 @@
using Connected.Data;
namespace Connected.Logistics.Documents.Receive;
public interface IReceiveItem : IEntityContainer<long>
{
int Document { get; init; }
float Quantity { get; init; }
float PostedQuantity { get; init; }
}

@ -0,0 +1,41 @@
using Connected.Data;
namespace Connected.Logistics.Documents.Receive;
/// <summary>
/// Represents connected (many-to-many) entity between <see cref="IReceivePostingDocument"/>
/// and <see cref="IReceiveItem"/>.
/// </summary>
/// <remarks>
/// Master receive document contains one or more <see cref="IReceiveItem"/> items. Receive document
/// is then divided into one or more <see cref="IReceivePostingDocument"/> documents which contain
/// two lists of items:
/// <list type="bullet">
/// <item><see cref="IReceivePlannedItem"/></item>
/// <item><see cref="IReceivePostingItem"/></item>
/// </list>
/// This entity represents planned items which represents the plan of how what kind of item and how
/// much should be posted to each <see cref="IReceivePostingDocument"/>. This acts only as a guide to the user
/// not the actual items and quantities that arrived into warehouse.
/// </remarks>
public interface IReceivePlannedItem : IPrimaryKey<long>
{
/// <summary>
/// The id of the <see cref="IReceivePostingDocument"/> to which
/// this planned entity belongs.
/// </summary>
int Document { get; init; }
/// <summary>
/// The id of the <see cref="IReceiveItem"/> item to which
/// this planned entity belongs.
/// </summary>
int Item { get; init; }
/// <summary>
/// The planned entity which should be posted into this
/// item.
/// </summary>
float Quantity { get; init; }
/// <summary>
/// The actual posted quantity for this item.
/// </summary>
float PostedQuantity { get; init; }
}

@ -0,0 +1,10 @@
using Connected.Common.Documents;
namespace Connected.Logistics.Documents.Receive;
public interface IReceivePostingDocument : IDocument<int>
{
int Document { get; init; }
int OpenItemCount { get; init; }
int ItemCount { get; init; }
}

@ -0,0 +1,84 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Common.Documents;
using Connected.Notifications;
using Connected.ServiceModel;
namespace Connected.Logistics.Documents.Receive;
/// <summary>
/// Represents service for the <see cref="IReceivePostingDocument"/> document.
/// </summary>
[Service]
[ServiceUrl(DocumentUrls.Receives)]
public interface IReceivePostingDocumentService : IDocumentService<int, long>
{
event ServiceEventHandler<PrimaryKeyEventArgs<long>> PlannedItemUpdated;
/// <summary>
/// Inserts a new <see cref="IReceivePostingDocument"/>.
/// </summary>
/// <param name="args">The arguments containing the properties of the new document.</param>
/// <returns>The id of the newly inserted document.</returns>
Task<int> Insert(InsertReceivePostingDocumentArgs args);
/// <summary>
/// Updates <see cref="IReceivePostingDocument"/> document.
/// </summary>
/// <param name="args">The arguments containing changed properties of the document.</param>
Task Update(UpdateReceivePostingDocumentArgs args);
/// <summary>
/// Performs partial update on the <see cref="IReceivePostingDocument"/> for the properties specified
/// in arguments.
/// </summary>
/// <param name="args">The arguments containing properties that need to be updated.</param>
Task Patch(PatchArgs<int> args);
/// <summary>
/// Deletes <see cref="IReceivePostingDocument"/> from the storage.
/// </summary>
/// <param name="args">The arguments containing the id of the document that is about to be deleted.</param>
Task Delete(PrimaryKeyArgs<int> args);
/// <summary>
/// Selects <see cref="IReceivePostingDocument"/> for the specified id.
/// </summary>
/// <param name="args">The arguments containing the id.</param>
/// <returns><see cref="IReceivePostingDocument"/> is found, <c>null</c> otherwise.</returns>
Task<IReceivePostingDocument?> Select(PrimaryKeyArgs<int> args);
/// <summary>
/// Queries <see cref="IReceivePostingDocument"/> for the specified <see cref="IReceiveDocument"/> document.
/// </summary>
/// <param name="args">The arguments containing the id of the parent receive document.</param>
/// <returns><see cref="IReceivePostingDocument"/> if found, <c>null</c> otherwise.</returns>
Task<ImmutableList<IReceivePostingDocument>> Query(PrimaryKeyArgs<int> args);
/// <summary>
/// Inserts a new <see cref="IReceivePostingItem"/> into the <see cref="IReceivePostingDocument"/> document.
/// </summary>
/// <param name="args">The arguments containing the properties of the new item.</param>
/// <returns>The id of the newly inserted <see cref="IReceivePostingItem"/> item.</returns>
Task<long> InsertItem(InsertReceivePostingItemArgs args);
Task PatchPlanedItem(PatchArgs<long> args);
/// <summary>
/// Queries the <see cref="IReceivePostingItem"/> items for the specified <see cref="IReceivePostingDocument"/>.
/// </summary>
/// <param name="args">The arguments containing the id of the document for which the items to be
/// queried.</param>
/// <returns>The list of items that belong to the specified document.</returns>
Task<ImmutableList<IReceivePostingItem>> QueryItems(PrimaryKeyArgs<int> args);
/// <summary>
/// Selects the <see cref="IReceivePostingItem"/> item for the specified id.
/// </summary>
/// <param name="args">The arguments containing the id of the item.</param>
/// <returns>The <see cref="IReceivePostingItem"/> if found, <c>null</c> otherwise.</returns>
Task<IReceivePostingItem?> SelectItem(PrimaryKeyArgs<long> args);
/// <summary>
/// Updates <see cref="IReceivePlannedItem"/>.
/// </summary>
/// <param name="args">The arguments containing the changed properties of the item.</param>
Task UpdatePlannedItem(UpdateReceivePlannedItemArgs args);
Task<IReceivePlannedItem?> SelectPlannedItem(PrimaryKeyArgs<long> args);
Task<IReceivePlannedItem?> SelectPlannedItem(SelectReceivePlannedItemArgs args);
Task<ImmutableList<IReceivePlannedItem>> QueryPlannedItems(PrimaryKeyArgs<int> args);
Task<ImmutableList<IReceivePlannedItem>> QueryPlannedItems(PrimaryKeyArgs<long> args);
}

@ -0,0 +1,10 @@
using Connected.Data;
namespace Connected.Logistics.Documents.Receive;
public interface IReceivePostingItem : IPrimaryKey<long>
{
int Document { get; init; }
long Serial { get; init; }
float Quantity { get; init; }
int Location { get; init; }
}

@ -0,0 +1,58 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Common.Documents;
using Connected.ServiceModel;
namespace Connected.Logistics.Documents.Receive;
/// <summary>
/// The arguments used when inserting a new <see cref="IReceiveItem"/> item
/// via <see cref="IReceiveDocumentService"/> service.
/// </summary>
public class InsertReceiveItemArgs : Dto
{
/// <summary>
/// The id of the <see cref="IReceiveDocument"/> document.
/// Must exists in the storage.
/// </summary>
[Range(1, int.MaxValue)]
public int Document { get; set; }
[Required, MaxLength(128)]
public string EntityType { get; set; } = default!;
[Required, MaxLength(128)]
public string EntityId { get; set; } = default!;
[Range(0, float.MaxValue)]
public float Quantity { get; set; }
}
public sealed class UpdateReceiveItemArgs : PrimaryKeyArgs<long>
{
[MinValue(0)]
public float PostedQuantity { get; set; }
}
public sealed class InsertReceiveDocumentArgs : InsertDocumentArgs
{
public int? Warehouse { get; set; }
public int? Supplier { get; set; }
}
public sealed class UpdateReceiveDocumentArgs : UpdateDocumentArgs<int>
{
public int? Warehouse { get; set; }
public int? Supplier { get; set; }
}
public sealed class SelectReceiveItemArgs : Dto
{
[MinValue(1)]
public int Document { get; set; }
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
[Required, MaxLength(128)]
public string EntityId { get; set; } = default!;
}

@ -0,0 +1,72 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Common.Documents;
using Connected.ServiceModel;
namespace Connected.Logistics.Documents.Receive;
public sealed class InsertReceivePostingDocumentArgs : InsertDocumentArgs
{
[MinValue(1)]
public int Document { get; set; }
}
public sealed class UpdateReceivePostingDocumentArgs : UpdateDocumentArgs<int>
{
[MinValue(0)]
public int ItemCount { get; set; }
[MinValue(0)]
public int OpenItemCount { get; set; }
}
/// <summary>
/// The arguments used when inserting a new <see cref="IReceivePostingItem"/> item
/// via <see cref="IReceiveDocumentService"/> service.
/// </summary>
public class InsertReceivePostingItemArgs : Dto
{
/// <summary>
/// The id of the <see cref="IReceivePostingDocument"/> document.
/// Must exists in the storage.
/// </summary>
[MinValue(1)]
public int Document { get; set; }
[MinValue(1)]
public int Location { get; set; }
[MinValue(0)]
public float Quantity { get; set; }
[MinValue(1)]
public long? Serial { get; set; }
}
public class InsertReceivePlannedItemArgs : Dto
{
[MinValue(1)]
public int Document { get; set; }
[MinValue(0)]
public float Quantity { get; set; }
public string Entity { get; set; }
public string EntityId { get; set; }
}
public class UpdateReceivePlannedItemArgs : PrimaryKeyArgs<long>
{
[MinValue(0)]
public float PostedQuantity { get; set; }
}
public class SelectReceivePlannedItemArgs : Dto
{
[MinValue(1)]
public int Document { get; set; }
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
[Required, MaxLength(128)]
public string EntityId { get; set; } = default!;
}

@ -0,0 +1,8 @@
using Connected.Annotations;
[assembly: MicroService(MicroServiceType.Service)]
namespace Connected.Logistics.Documents;
internal sealed class Bootstrapper : Startup
{
}

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Connected.Common\src\Connected.Common.Model\Connected.Common.Model.csproj" />
<ProjectReference Include="..\..\..\Connected.Common\src\Connected.Common\Connected.Common.csproj" />
<ProjectReference Include="..\..\..\Connected.Framework\src\Connected.Entities\Connected.Entities.csproj" />
<ProjectReference Include="..\Connected.Logistics.Documents.Model\Connected.Logistics.Documents.Model.csproj" />
<ProjectReference Include="..\Connected.Logistics.Types.Model\Connected.Logistics.Types.Model.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="SR.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>SR.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="SR.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>SR.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

@ -0,0 +1,24 @@
using Connected.Annotations;
using Connected.Common.Documents;
using Connected.Entities.Annotations;
using Connected.Logistics.Types;
namespace Connected.Logistics.Documents.Receive;
/// <inheritdoc cref="IReceiveDocument"/>
[Table(Schema = Domain.Code)]
internal sealed record ReceiveDocument : Document<int>, IReceiveDocument
{
public const string EntityKey = $"{Domain.Code}.{nameof(ReceiveDocument)}";
/// <inheritdoc cref="IReceiveDocument.Supplier"/>
[Ordinal(1), Nullable]
public int? Supplier { get; init; }
/// <inheritdoc cref="IReceiveDocument.ReceiveDate"/>
[Ordinal(2), Nullable]
public DateTimeOffset? ReceiveDate { get; init; }
/// <inheritdoc cref="IReceiveDocument.ItemCount"/>
public int ItemCount { get; init; }
/// <inheritdoc cref="IReceiveDocument.OpenItemCount"/>
public int OpenItemCount { get; init; }
}

@ -0,0 +1,162 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Connected.Logistics.Documents.Receive;
internal sealed class ReceiveDocumentItemOps
{
public sealed class Insert : ServiceFunction<InsertReceiveItemArgs, long>
{
public Insert(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<long> OnInvoke()
{
var result = await Storage.Open<ReceiveItem>().Update(Arguments.AsEntity<ReceiveItem>(State.New));
return result.Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, typeof(ReceiveDocumentService), nameof(IReceiveDocumentService.ItemInserted), new PrimaryKeyArgs<long> { Id = Result });
}
}
public sealed class Delete : ServiceAction<PrimaryKeyArgs<long>>
{
public Delete(IStorageProvider storage, IEventService events, ICacheContext cache, IReceiveDocumentService documents)
{
Storage = storage;
Events = events;
Cache = cache;
Documents = documents;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private ICacheContext Cache { get; }
private IReceiveDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (SetState(await Documents.SelectItem(Arguments)) is not IReceiveItem entity)
return;
await Storage.Open<ReceiveItem>().Update(Arguments.AsEntity<ReceiveItem>(State.Deleted));
}
protected override async Task OnCommitted()
{
await Cache.Remove(ReceiveItem.EntityKey, Arguments.Id);
await Events.Enqueue(this, Documents, nameof(IReceiveDocumentService.ItemDeleted), Arguments);
}
}
public sealed class Query : ServiceFunction<PrimaryKeyArgs<int>, ImmutableList<IReceiveItem>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceiveItem>> OnInvoke()
{
return await (from e in Storage.Open<ReceiveItem>() where e.Document == Arguments.Id select e).AsEntities<IReceiveItem>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<long>, IReceiveItem?>
{
public Select(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceiveItem?> OnInvoke()
{
return await Cache.Get(ReceiveItem.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<ReceiveItem>() where e.Id == Arguments.Id select e).AsEntity();
});
}
}
public sealed class SelectByEntity : NullableServiceFunction<SelectReceiveItemArgs, IReceiveItem?>
{
public SelectByEntity(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceiveItem?> OnInvoke()
{
return await Cache.Get(ReceiveItem.EntityKey,
f => f.Document == Arguments.Document
&& string.Equals(f.Entity, Arguments.Entity, StringComparison.OrdinalIgnoreCase)
&& string.Equals(f.EntityId, Arguments.EntityId, StringComparison.OrdinalIgnoreCase), async (f) =>
{
return await (from e in Storage.Open<ReceiveItem>()
where e.Document == Arguments.Document
&& string.Equals(e.Entity, Arguments.Entity, StringComparison.OrdinalIgnoreCase)
&& string.Equals(e.EntityId, Arguments.EntityId, StringComparison.OrdinalIgnoreCase)
select e).AsEntity();
});
}
}
public sealed class Update : ServiceAction<UpdateReceiveItemArgs>
{
public Update(IStorageProvider storage, ICacheContext cache, IEventService events, IReceiveDocumentService documents)
{
Storage = storage;
Cache = cache;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IReceiveDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (await Documents.SelectItem(Arguments.Id) is not ReceiveItem entity)
return;
await Storage.Open<ReceiveItem>().Update(entity, Arguments, async () =>
{
await Cache.Remove(ReceiveItem.EntityKey, Arguments.Id);
return (await Documents.SelectItem(Arguments.Id)) as ReceiveItem;
});
}
protected override async Task OnCommitted()
{
await Cache.Remove(ReceiveItem.EntityKey, Arguments.Id);
await Events.Enqueue(this, Documents, nameof(IReceiveDocumentService.ItemUpdated), Arguments);
}
}
}

@ -0,0 +1,144 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Connected.Logistics.Documents.Receive;
internal sealed class ReceiveDocumentOps
{
public sealed class Delete : ServiceAction<PrimaryKeyArgs<int>>
{
public Delete(IReceiveDocumentService documents, IStorageProvider storage, ICacheContext cache, IEventService events)
{
Documents = documents;
Storage = storage;
Cache = cache;
Events = events;
}
private IReceiveDocumentService Documents { get; }
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
if (SetState(await Documents.Select(Arguments)) is not IReceiveDocument document)
return;
/*
* Delete all items
*/
foreach (var item in await Documents.QueryItems(document.Id))
await Documents.DeleteItem(item.Id);
/*
* Delete document
*/
await Storage.Open<ReceiveDocument>().Update(Arguments.AsEntity<ReceiveDocument>(State.Deleted));
}
protected override async Task OnCommitted()
{
await Cache.Remove(ReceiveDocument.EntityKey, Arguments.Id);
await Events.Enqueue(this, Documents, nameof(IReceiveDocumentService.Deleted), Arguments);
}
}
public sealed class Insert : ServiceFunction<InsertReceiveDocumentArgs, int>
{
public Insert(IStorageProvider storage, IEventService events, IReceiveDocumentService documents)
{
Storage = storage;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private IReceiveDocumentService Documents { get; }
protected override async Task<int> OnInvoke()
{
return (await Storage.Open<ReceiveDocument>().Update(Arguments.AsEntity<ReceiveDocument>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceiveDocumentService.Inserted), new PrimaryKeyArgs<int> { Id = Result });
}
}
public sealed class Query : ServiceFunction<QueryArgs, ImmutableList<IReceiveDocument>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
public IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceiveDocument>> OnInvoke()
{
return await (from e in Storage.Open<ReceiveDocument>() select e).WithArguments(Arguments).AsEntities<IReceiveDocument>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<int>, IReceiveDocument?>
{
public Select(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceiveDocument?> OnInvoke()
{
return await Cache.Get<ReceiveDocument>(ReceiveDocument.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<ReceiveDocument>() where e.Id == Arguments.Id select e).AsEntity();
});
}
}
public sealed class Update : ServiceAction<UpdateReceiveDocumentArgs>
{
public Update(IStorageProvider storage, ICacheContext cache, IEventService events, IReceiveDocumentService documents)
{
Storage = storage;
Cache = cache;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IReceiveDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (await Documents.Select(Arguments.Id) is not ReceiveDocument entity)
return;
await Storage.Open<ReceiveDocument>().Update(entity, Arguments, async () =>
{
await Cache.Remove(ReceiveDocument.EntityKey, Arguments.Id);
return (await Documents.Select(Arguments.Id)) as ReceiveDocument;
});
}
protected override async Task OnCommitted()
{
await Cache.Remove(ReceiveDocument.EntityKey, Arguments.Id);
await Events.Enqueue(this, Documents, nameof(IReceiveDocumentService.Updated), Arguments);
}
}
}

@ -0,0 +1,82 @@
using System.Collections.Immutable;
using Connected.Common.Documents;
using Connected.Entities;
using Connected.ServiceModel;
using ItemOps = Connected.Logistics.Documents.Receive.ReceiveDocumentItemOps;
using Ops = Connected.Logistics.Documents.Receive.ReceiveDocumentOps;
namespace Connected.Logistics.Documents.Receive;
/// <inheritdoc cref="IReceiveDocumentService"/>
internal sealed class ReceiveDocumentService : DocumentService<int, long>, IReceiveDocumentService
{
/// <summary>
/// Create a new <see cref="ReceiveDocument"/> instance
/// </summary>
/// <param name="context">The DI scope used by this instance.</param>
public ReceiveDocumentService(IContext context) : base(context)
{
}
/// <inheritdoc cref="IReceiveDocumentService.Delete(PrimaryKeyArgs{int})"/>
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.DeleteItem(PrimaryKeyArgs{long})"/>
public async Task DeleteItem(PrimaryKeyArgs<long> args)
{
await Invoke(GetOperation<ItemOps.Delete>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.Insert(InsertReceiveDocumentArgs)"/>
public async Task<int> Insert(InsertReceiveDocumentArgs args)
{
return await Invoke(GetOperation<Ops.Insert>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.InsertItem(InsertReceiveItemArgs)"/>
public async Task<long> InsertItem(InsertReceiveItemArgs args)
{
return await Invoke(GetOperation<ItemOps.Insert>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.Patch(PatchArgs{int})"/>
public async Task Patch(PatchArgs<int> args)
{
if (await Select(args.Id) is not ReceiveDocument entity)
return;
await Update(args.Patch<UpdateReceiveDocumentArgs, ReceiveDocument>(entity));
}
/// <inheritdoc cref="IReceiveDocumentService.Query(QueryArgs?)"/>
public async Task<ImmutableList<IReceiveDocument>> Query(QueryArgs? args)
{
return await Invoke(GetOperation<Ops.Query>(), args ?? QueryArgs.Default);
}
/// <inheritdoc cref="IReceiveDocumentService.QueryItems(PrimaryKeyArgs{int})"/>
public async Task<ImmutableList<IReceiveItem>> QueryItems(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<ItemOps.Query>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.Select(PrimaryKeyArgs{int})"/>
public async Task<IReceiveDocument?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.SelectItem(PrimaryKeyArgs{long})"/>
public async Task<IReceiveItem?> SelectItem(PrimaryKeyArgs<long> args)
{
return await Invoke(GetOperation<ItemOps.Select>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.SelectItem(SelectReceiveItemArgs)"/>
public async Task<IReceiveItem?> SelectItem(SelectReceiveItemArgs args)
{
return await Invoke(GetOperation<ItemOps.SelectByEntity>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.Update(UpdateReceiveDocumentArgs)"/>
public async Task Update(UpdateReceiveDocumentArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.UpdateItem(UpdateReceiveItemArgs)"/>
public async Task UpdateItem(UpdateReceiveItemArgs args)
{
await Invoke(GetOperation<ItemOps.Update>(), args);
}
}

@ -0,0 +1,23 @@
using Connected.Annotations;
using Connected.Common;
using Connected.Entities;
using Connected.Entities.Annotations;
using Connected.Logistics.Types;
namespace Connected.Logistics.Documents.Receive;
/// <inheritdoc cref="IReceiveItem"/>
[Table(Schema = CommonSchemas.DocumentSchema)]
internal sealed record ReceiveItem : EntityContainer<long>, IReceiveItem
{
public const string EntityKey = $"{Domain.Code}.{nameof(ReceiveItem)}";
/// <inheritdoc cref="IReceiveItem.Document"/>
[Ordinal(0), Index]
public int Document { get; init; }
/// <inheritdoc cref="IReceiveItem.Quantity"/>
[Ordinal(1)]
public float Quantity { get; init; }
/// <inheritdoc cref="IReceiveItem.PostedQuantity"/>
[Ordinal(4)]
public float PostedQuantity { get; init; }
}

@ -0,0 +1,25 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Connected.Logistics.Types;
namespace Connected.Logistics.Documents.Receive;
/// <inheritdoc cref="IReceivePlannedItem"/>
[Table(Schema = Domain.Code)]
internal sealed record ReceivePlannedItem : ConsistentEntity<long>, IReceivePlannedItem
{
public const string EntityKey = $"{Domain.Code}.{nameof(ReceivePlannedItem)}";
/// <inheritdoc cref="IReceivePlannedItem.Document"/>
[Ordinal(0), Index]
public int Document { get; init; }
/// <inheritdoc cref="IReceivePlannedItem.Item"/>
[Ordinal(1), Index]
public int Item { get; init; }
/// <inheritdoc cref="IReceivePlannedItem.Quantity"/>
[Ordinal(2)]
public float Quantity { get; init; }
/// <inheritdoc cref="IReceivePlannedItem.PostedQuantity"/>
[Ordinal(3)]
public float PostedQuantity { get; init; }
}

@ -0,0 +1,128 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Interop;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Connected.Logistics.Documents.Receive;
internal sealed class ReceivePlannedItemsOps
{
public sealed class Query : ServiceFunction<PrimaryKeyArgs<int>, ImmutableList<IReceivePlannedItem>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceivePlannedItem>> OnInvoke()
{
return await (from e in Storage.Open<ReceivePlannedItem>() where e.Document == Arguments.Id select e).AsEntities<IReceivePlannedItem>();
}
}
public sealed class QueryByItem : ServiceFunction<PrimaryKeyArgs<long>, ImmutableList<IReceivePlannedItem>>
{
public QueryByItem(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceivePlannedItem>> OnInvoke()
{
return await (from e in Storage.Open<ReceivePlannedItem>() where e.Item == Arguments.Id select e).AsEntities<IReceivePlannedItem>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<long>, IReceivePlannedItem>
{
public Select(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceivePlannedItem?> OnInvoke()
{
return await Cache.Get(ReceivePlannedItem.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<ReceivePlannedItem>() where e.Id == Arguments.Id select e).AsEntity();
});
}
}
public sealed class SelectByEntity : NullableServiceFunction<SelectReceivePlannedItemArgs, IReceivePlannedItem>
{
public SelectByEntity(IStorageProvider storage, ICacheContext cache, IReceiveDocumentService documents, IReceivePostingDocumentService postingDocuments)
{
Storage = storage;
Cache = cache;
Documents = documents;
PostingDocuments = postingDocuments;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IReceiveDocumentService Documents { get; }
private IReceivePostingDocumentService PostingDocuments { get; }
protected override async Task<IReceivePlannedItem?> OnInvoke()
{
if (await PostingDocuments.Select(Arguments.Document) is not IReceivePostingDocument postingDocument)
return null;
if (await Documents.SelectItem(Arguments.AsArguments<SelectReceiveItemArgs>(new { postingDocument.Document })) is not IReceiveItem item)
return null;
return await Cache.Get<IReceivePlannedItem>(ReceivePlannedItem.EntityKey, f => f.Item == item.Id, async (f) =>
{
return await (from e in Storage.Open<ReceivePlannedItem>() where e.Item == item.Id select e).AsEntity();
});
}
}
public sealed class Update : ServiceAction<UpdateReceivePlannedItemArgs>
{
public Update(IStorageProvider storage, ICacheContext cache, IEventService events, IReceivePostingDocumentService documents)
{
Storage = storage;
Cache = cache;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IReceivePostingDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (await Documents.SelectPlannedItem(Arguments.Id) is not ReceivePlannedItem entity)
return;
await Storage.Open<ReceivePlannedItem>().Update(entity, Arguments, async () =>
{
await Cache.Remove(ReceivePlannedItem.EntityKey, Arguments.Id);
return (await Documents.SelectPlannedItem(Arguments.Id)) as ReceivePlannedItem;
});
await Cache.Remove(ReceivePlannedItem.EntityKey, Arguments.Id);
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceivePostingDocumentService.PlannedItemUpdated), Arguments);
}
}
}

@ -0,0 +1,22 @@
using Connected.Annotations;
using Connected.Common.Documents;
using Connected.Entities.Annotations;
using Connected.Logistics.Types;
namespace Connected.Logistics.Documents.Receive;
/// <inheritdoc cref="IReceivePostingDocument"/>
[Table(Schema = Domain.Code)]
internal sealed record ReceivePostingDocument : Document<int>, IReceivePostingDocument
{
public const string EntityKey = $"{Domain.Code}.{nameof(ReceivePostingDocument)}";
/// <inheritdoc cref="IReceivePostingDocument.Document"/>
[Ordinal(0)]
public int Document { get; init; }
/// <inheritdoc cref="IReceivePostingDocument.OpenItemCount"/>
[Ordinal(1)]
public int OpenItemCount { get; init; }
/// <inheritdoc cref="IReceivePostingDocument.ItemCount"/>
[Ordinal(2)]
public int ItemCount { get; init; }
}

@ -0,0 +1,133 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Connected.Logistics.Documents.Receive;
internal sealed class ReceivePostingDocumentOps
{
public sealed class Delete : ServiceAction<PrimaryKeyArgs<int>>
{
public Delete(IStorageProvider storage, ICacheContext cache, IEventService events, IReceivePostingDocumentService documents)
{
Storage = storage;
Cache = cache;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IReceivePostingDocumentService Documents { get; }
protected override async Task OnInvoke()
{
await Storage.Open<ReceivePostingDocument>().Update(Arguments.AsEntity<ReceivePostingDocument>(State.Deleted));
await Cache.Remove(ReceivePostingDocument.EntityKey, Arguments.Id);
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceivePostingDocumentService.Deleted), Arguments);
}
}
public sealed class Insert : ServiceFunction<InsertReceivePostingDocumentArgs, int>
{
public Insert(IStorageProvider storage, IEventService events, IReceivePostingDocumentService documents)
{
Storage = storage;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private IReceivePostingDocumentService Documents { get; }
protected override async Task<int> OnInvoke()
{
return (await Storage.Open<ReceivePostingDocument>().Update(Arguments.AsEntity<ReceivePostingDocument>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceivePostingDocumentService.Inserted), new PrimaryKeyArgs<int> { Id = Result });
}
}
public sealed class Query : ServiceFunction<PrimaryKeyArgs<int>, ImmutableList<IReceivePostingDocument>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceivePostingDocument>> OnInvoke()
{
return await (from e in Storage.Open<ReceivePostingDocument>() where e.Document == Arguments.Id select e).AsEntities<IReceivePostingDocument>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<int>, IReceivePostingDocument>
{
public Select(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceivePostingDocument?> OnInvoke()
{
return await Cache.Get(ReceivePostingDocument.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<ReceivePostingDocument>() where e.Id == Arguments.Id select e).AsEntity();
});
}
}
public sealed class Update : ServiceAction<UpdateReceivePostingDocumentArgs>
{
public Update(IStorageProvider storage, ICacheContext cache, IEventService events, IReceivePostingDocumentService documents)
{
Storage = storage;
Cache = cache;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IReceivePostingDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (await Documents.Select(Arguments.Id) is not ReceivePostingDocument entity)
return;
await Storage.Open<ReceivePostingDocument>().Update(entity, Arguments, async () =>
{
await Cache.Remove(ReceivePostingDocument.EntityKey, Arguments.Id);
return (await Documents.Select(Arguments.Id)) as ReceivePostingDocument;
});
await Cache.Remove(ReceivePostingDocument.EntityKey, Arguments.Id);
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceivePostingDocumentService.Updated), Arguments);
}
}
}

@ -0,0 +1,98 @@
using System.Collections.Immutable;
using Connected.Common.Documents;
using Connected.Entities;
using Connected.Notifications;
using Connected.ServiceModel;
using ItemOps = Connected.Logistics.Documents.Receive.ReceivePostingItemOps;
using Ops = Connected.Logistics.Documents.Receive.ReceivePostingDocumentOps;
using PlannedOps = Connected.Logistics.Documents.Receive.ReceivePlannedItemsOps;
namespace Connected.Logistics.Documents.Receive;
internal sealed class ReceivePostingDocumentService : DocumentService<int, long>, IReceivePostingDocumentService
{
public event ServiceEventHandler<PrimaryKeyEventArgs<long>> PlannedItemUpdated;
public ReceivePostingDocumentService(IContext context) : base(context)
{
}
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
public async Task<int> Insert(InsertReceivePostingDocumentArgs args)
{
return await Invoke(GetOperation<Ops.Insert>(), args);
}
public async Task<long> InsertItem(InsertReceivePostingItemArgs args)
{
return await Invoke(GetOperation<ItemOps.Insert>(), args);
}
public async Task Patch(PatchArgs<int> args)
{
if (await Select(args.Id) is not ReceivePostingDocument entity)
return;
await Update(entity.Merge(args, State.Default).AsArguments<UpdateReceivePostingDocumentArgs>());
}
public async Task PatchPlanedItem(PatchArgs<long> args)
{
if (await SelectPlannedItem(args.Id) is not ReceivePlannedItem entity)
return;
await UpdatePlannedItem(entity.Merge(args, State.Default).AsArguments<UpdateReceivePlannedItemArgs>());
}
public async Task<ImmutableList<IReceivePostingDocument>> Query(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Query>(), args);
}
public async Task<ImmutableList<IReceivePostingItem>> QueryItems(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<ItemOps.Query>(), args);
}
public async Task<ImmutableList<IReceivePlannedItem>> QueryPlannedItems(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<PlannedOps.Query>(), args);
}
public async Task<ImmutableList<IReceivePlannedItem>> QueryPlannedItems(PrimaryKeyArgs<long> args)
{
return await Invoke(GetOperation<PlannedOps.QueryByItem>(), args);
}
public async Task<IReceivePostingDocument?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
public async Task<IReceivePostingItem?> SelectItem(PrimaryKeyArgs<long> args)
{
return await Invoke(GetOperation<ItemOps.Select>(), args);
}
public async Task<IReceivePlannedItem?> SelectPlannedItem(PrimaryKeyArgs<long> args)
{
return await Invoke(GetOperation<PlannedOps.Select>(), args);
}
public async Task<IReceivePlannedItem?> SelectPlannedItem(SelectReceivePlannedItemArgs args)
{
return await Invoke(GetOperation<PlannedOps.SelectByEntity>(), args);
}
public async Task Update(UpdateReceivePostingDocumentArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
public Task UpdatePlannedItem(UpdateReceivePlannedItemArgs args)
{
throw new NotImplementedException();
}
}

@ -0,0 +1,24 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Connected.Logistics.Types;
namespace Connected.Logistics.Documents.Receive;
/// <inheritdoc cref="IReceivePostingItem"/>
[Table(Schema = Domain.Code)]
internal sealed record ReceivePostingItem : ConsistentEntity<long>, IReceivePostingItem
{
public const string EntityKey = $"{Domain.Code}.{nameof(ReceivePostingItem)}";
/// <inheritdoc cref="IReceivePostingItem.Document"/>
[Ordinal(0)]
public int Document { get; init; }
/// <inheritdoc cref="IReceivePostingItem.Serial"/>
[Ordinal(1)]
public long Serial { get; init; }
/// <inheritdoc cref="IReceivePostingItem.Quantity"/>
[Ordinal(2)]
public float Quantity { get; init; }
/// <inheritdoc cref="IReceivePostingItem.Location"/>
[Ordinal(3)]
public int Location { get; init; }
}

@ -0,0 +1,70 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Connected.Logistics.Documents.Receive;
internal sealed class ReceivePostingItemOps
{
public sealed class Insert : ServiceFunction<InsertReceivePostingItemArgs, long>
{
public Insert(IStorageProvider storage, IEventService events, IReceivePostingDocumentService documents)
{
Storage = storage;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private IReceivePostingDocumentService Documents { get; }
protected override async Task<long> OnInvoke()
{
return (await Storage.Open<ReceivePostingItem>().Update(Arguments.AsEntity<ReceivePostingItem>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceivePostingDocumentService.ItemInserted), new PrimaryKeyArgs<long> { Id = Result });
}
}
public sealed class Query : ServiceFunction<PrimaryKeyArgs<int>, ImmutableList<IReceivePostingItem>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceivePostingItem>> OnInvoke()
{
return await (from e in Storage.Open<ReceivePostingItem>() where e.Document == Arguments.Id select e).AsEntities<IReceivePostingItem>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<long>, IReceivePostingItem>
{
public Select(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceivePostingItem?> OnInvoke()
{
return await Cache.Get(ReceivePostingItem.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<ReceivePostingItem>() where e.Id == Arguments.Id select e).AsEntity();
});
}
}
}

@ -0,0 +1,64 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Common.Documents;
using Connected.Contacts.Types;
using Connected.Data;
using Connected.Logistics.Types.Warehouses;
using Connected.Security.Identity;
using Connected.Validation;
namespace Connected.Logistics.Documents.Receive;
[Priority(0)]
internal sealed class InsertReceiveDocumentValidator : InsertDocumentValidator<InsertReceiveDocumentArgs>
{
public InsertReceiveDocumentValidator(IUserService users, IBusinessPartnerService businessPartners, IWarehouseService warehouses) : base(users)
{
BusinessPartners = businessPartners;
Warehouses = warehouses;
}
private IBusinessPartnerService BusinessPartners { get; }
private IWarehouseService Warehouses { get; }
protected override async Task OnValidating()
{
await ValidateSupplier();
await ValidateWarehouse();
}
private async Task ValidateSupplier()
{
/*
* If supplier is not set there is no need for a validation.
*/
if (Arguments.Supplier is null)
return;
/*
* Check is business partner exists.
*/
if (await BusinessPartners.Select((int)Arguments.Supplier) is not IBusinessPartner supplier)
throw ValidationExceptions.NotFound(nameof(Arguments.Supplier), Arguments.Supplier);
/*
* Check if business partner has Supplier role which means it's actually a supplier.
*/
if (!supplier.Roles.HasFlag(CustomerRoles.Supplier))
throw new ValidationException($"{SR.ValPartnerNotSupplier} ({Arguments.Supplier})");
}
private async Task ValidateWarehouse()
{
if (Arguments.Warehouse is null)
return;
/*
* Check is warehouse exists.
*/
if (await Warehouses.Select(Arguments.Warehouse) is not IWarehouse warehouse)
throw ValidationExceptions.NotFound(nameof(Arguments.Warehouse), Arguments.Warehouse);
/*
* Only Enabled warehouses can be used.
*/
if (warehouse.Status == Status.Disabled)
throw ValidationExceptions.Disabled(nameof(Arguments.Warehouse));
}
}

@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Connected.Logistics.Documents {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class SR {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal SR() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Connected.Logistics.Documents.SR", typeof(SR).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Business partner is not a supplier..
/// </summary>
internal static string ValPartnerNotSupplier {
get {
return ResourceManager.GetString("ValPartnerNotSupplier", resourceCulture);
}
}
}
}

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ValPartnerNotSupplier" xml:space="preserve">
<value>Business partner is not a supplier.</value>
</data>
</root>

@ -0,0 +1,10 @@
using Connected.Annotations;
[assembly: MicroService(MicroServiceType.Process)]
namespace Connected.Logistics.Documents;
internal sealed class Bootstrapper : Startup
{
}

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Connected.Logistics.Documents</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Connected.Common\src\Connected.Common\Connected.Common.csproj" />
<ProjectReference Include="..\..\..\Connected\src\Connected\Connected.csproj" />
<ProjectReference Include="..\..\..\Connected.Framework\src\Connected.Data\Connected.Data.csproj" />
<ProjectReference Include="..\..\..\Connected.Framework\src\Connected.Entities\Connected.Entities.csproj" />
<ProjectReference Include="..\..\..\Connected.Framework\src\Connected.Hosting\Connected.Hosting.csproj" />
<ProjectReference Include="..\..\..\Connected.Framework\src\Connected.Runtime\Connected.Runtime.csproj" />
<ProjectReference Include="..\..\..\Connected.Framework\src\Connected.Services\Connected.Services.csproj" />
<ProjectReference Include="..\Connected.Logistics.Documents.Model\Connected.Logistics.Documents.Model.csproj" />
<ProjectReference Include="..\Connected.Logistics.Stock.Model\Connected.Logistics.Stock.Model.csproj" />
<ProjectReference Include="..\Connected.Logistics.Types.Model\Connected.Logistics.Types.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,74 @@
using Connected.Logistics.Documents.Receive;
using Connected.Middleware.Annotations;
using Connected.Notifications;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Microsoft.Extensions.Logging;
namespace Connected.Logistics.Documents.Listeners;
[Middleware<IReceivePostingDocumentService>(nameof(IReceivePostingDocumentService.PlannedItemUpdated))]
internal sealed class PlannedItemListener : EventListener<PrimaryKeyEventArgs<long>>
{
public PlannedItemListener(ILogger<PlannedItemListener> logger, IReceivePostingDocumentService documents, IReceiveDocumentService receiveDocuments)
{
Logger = logger;
Documents = documents;
ReceiveDocuments = receiveDocuments;
}
private ILogger<PlannedItemListener> Logger { get; }
private IReceivePostingDocumentService Documents { get; }
private IReceiveDocumentService ReceiveDocuments { get; }
protected override async Task OnInvoke()
{
if (await Documents.SelectPlannedItem(Arguments.Id) is not IReceivePlannedItem item)
{
Logger.LogWarning("The IReceivePlannedItem not found ({id}}.", Arguments.Id);
return;
}
if (await ReceiveDocuments.SelectItem(item.Item) is not IReceiveItem receiveItem)
{
Logger.LogWarning("The IReceiveItem not found ({id}}.", item.Item);
return;
}
if (await Documents.Select(item.Document) is not IReceivePostingDocument document)
{
Logger.LogWarning("The IReceivePostingDocument not found ({id}}.", item.Document);
return;
}
await UpdateOpenItems(document);
await UpdatePostedQuantity(receiveItem);
}
private async Task UpdateOpenItems(IReceivePostingDocument document)
{
var items = await Documents.QueryPlannedItems(new PrimaryKeyArgs<int> { Id = document.Id });
await Documents.Patch(new PatchArgs<int>
{
Id = document.Id,
Properties = new Dictionary<string, object>
{
{nameof(IReceivePostingDocument.OpenItemCount), items.Count(f => f.PostedQuantity < f.Quantity) }
}
});
}
private async Task UpdatePostedQuantity(IReceiveItem item)
{
var items = await Documents.QueryPlannedItems(new PrimaryKeyArgs<long> { Id = item.Id });
await Documents.PatchPlanedItem(new PatchArgs<long>
{
Id = item.Id,
Properties = new Dictionary<string, object>
{
{nameof(IReceiveItem.PostedQuantity), items.Sum(f => f.PostedQuantity) }
}
});
}
}

@ -0,0 +1,89 @@
using Connected.Logistics.Documents.Receive;
using Connected.Logistics.Stock;
using Connected.Logistics.Types.Serials;
using Connected.Middleware.Annotations;
using Connected.Notifications;
using Connected.Notifications.Events;
using Microsoft.Extensions.Logging;
namespace Connected.Logistics.Documents.Listeners;
/// <summary>
/// Represents the event listener to the <see cref="IReceivePostingDocumentService"/> Updated event.
/// </summary>
/// <remarks>
/// This middleware reacts when the item is inserted and updates the <see cref="IStockItem"/>.
/// </remarks>
[Middleware<IReceivePostingDocumentService>(nameof(IReceivePostingDocumentService.ItemInserted))]
internal sealed class PostingItemListener : EventListener<PrimaryKeyEventArgs<long>>
{
/// <summary>
/// Creates a new instance of the <see cref="PostingItemListener"/>
/// </summary>
public PostingItemListener(ILogger<PostingItemListener> logger, IStockService stock, IReceivePostingDocumentService documents, ISerialService serials)
{
Logger = logger;
Stock = stock;
Documents = documents;
Serials = serials;
}
private ILogger<PostingItemListener> Logger { get; }
private IStockService Stock { get; }
private IReceivePostingDocumentService Documents { get; }
private ISerialService Serials { get; }
protected override async Task OnInvoke()
{
/*
* Stage 1 is to prepare all data neede to perform operation
*
* Load posting item
*/
if (await Documents.SelectItem(Arguments.Id) is not IReceivePostingItem item)
{
Logger.LogWarning("The IReceivePostingItem not found ({id}}.", Arguments.Id);
return;
}
/*
* Now load the serial number
*/
if (await Serials.Select(item.Serial) is not ISerial serial)
{
Logger.LogWarning("The ISerial not found ({id}}.", item.Serial);
return;
}
/*
* Now load the serial number
*/
if (await Documents.SelectPlannedItem(new SelectReceivePlannedItemArgs
{
Document = item.Document,
Entity = serial.Entity,
EntityId = serial.EntityId
}) is not ISerial plannedItem)
{
Logger.LogWarning("The IReceivePlannedItem not found ({entity}, {entityId}).", serial.Entity, serial.EntityId);
return;
}
/*
* The idea here is simple:
* update (increase) the stock for the specified item
* and posted quantity and update the statictics for
* the immediate parents.
*/
await Stock.Update(new UpdateStockArgs
{
Location = item.Location,
Quantity = item.Quantity,
Serial = item.Serial
});
/*
* Now update the planned item with posted quantity
*/
await Documents.UpdatePlannedItem(new UpdateReceivePlannedItemArgs
{
Id = plannedItem.Id,
PostedQuantity = item.Quantity
});
}
}

@ -0,0 +1,40 @@
using Connected.Logistics.Documents.Receive;
using Connected.Middleware.Annotations;
using Connected.Notifications;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Microsoft.Extensions.Logging;
namespace Connected.Logistics.Documents.Listeners;
[Middleware<IReceiveDocumentService>(nameof(IReceiveDocumentService.ItemUpdated))]
internal sealed class ReceiveItemListener : EventListener<PrimaryKeyEventArgs<long>>
{
public ReceiveItemListener(ILogger<ReceiveItemListener> logger, IReceiveDocumentService documents)
{
Logger = logger;
Documents = documents;
}
private ILogger<ReceiveItemListener> Logger { get; }
private IReceiveDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (await Documents.SelectItem(Arguments.Id) is not IReceiveItem item)
{
Logger.LogWarning("The IReceiveItem not found ({id}}.", Arguments.Id);
return;
}
var items = await Documents.QueryItems(item.Document);
await Documents.Patch(new PatchArgs<int>
{
Id = item.Document,
Properties = new Dictionary<string, object>
{
{nameof(IReceiveDocument.OpenItemCount), items.Count(f=>f.PostedQuantity<f.Quantity)}
}
});
}
}

@ -0,0 +1,24 @@
using Connected.Data.DataProtection;
using Connected.Data.EntityProtection;
using Connected.Logistics.Documents.Receive;
using Connected.Middleware;
using Connected.ServiceModel;
namespace Connected.Logistics.Documents.Protection;
internal sealed class ReceiveProtector : MiddlewareComponent, IEntityProtector<IReceiveDocument>
{
public ReceiveProtector(IReceiveDocumentService documents)
{
Documents = documents;
}
public IReceiveDocumentService Documents { get; }
public async Task Invoke(EntityProtectionArgs<IReceiveDocument> args)
{
if (await Documents.Select(new PrimaryKeyArgs<int> { Id = args.Entity.Id }) is not IReceiveDocument document)
return;
throw new NotImplementedException();
}
}

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

@ -0,0 +1,9 @@
using Connected.Data;
namespace Connected.Logistics.Stock.Aggregations;
public interface IStockAggregation : IPrimaryKey<long>
{
long Stock { get; init; }
DateTimeOffset Date { get; init; }
float Quantity { get; init; }
}

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Connected.Logistics.Stock</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Connected\src\Connected\Connected.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,33 @@
using Connected.Data;
namespace Connected.Logistics.Stock;
/// <summary>
/// The stock descriptor which describes what kind of entity it
/// represents. The entity could be Product, Semi product or any
/// other type of entity.
/// </summary>
public interface IStock : IPrimaryKey<long>
{
/// <summary>
/// The type of the entity.
/// </summary>
string Entity { get; init; }
/// <summary>
/// The primary key of the entity.
/// </summary>
string EntityId { get; init; }
/// <summary>
/// The total quantity currently available.
/// </summary>
float Quantity { get; init; }
/// <summary>
/// The minimum quantity that should be always available
/// in the stock.
/// </summary>
float? Min { get; init; }
/// <summary>
/// The maximum quantity that should be stored in
/// the stock.
/// </summary>
float? Max { get; init; }
}

@ -0,0 +1,40 @@
using Connected.Data;
namespace Connected.Logistics.Stock;
/// <summary>
/// Represents a single stock item.
/// </summary>
/// <remarks>
/// Goods are typically stored in the warehouse. Warehouse is
/// organized into locations or storage bins and each location contains
/// zero or more goods.
/// </remarks>
public interface IStockItem : IPrimaryKey<long>
{
/// <summary>
/// The <see cref="IStock"/> to which the item belong.
/// </summary>
/// <remarks>
/// Stock contains information about the type of the entity whereas
/// the stock item contains information about actual storage.
/// </remarks>
long Stock { get; init; }
/// <summary>
/// The location where the goods are stored.
/// </summary>
int Location { get; init; }
/// <summary>
/// The serial number of the goods.
/// </summary>
/// <remarks>
/// Each item has a serial number which uniquely identifies
/// the items even from the same type but from
/// different documents.
/// </remarks>
long Serial { get; init; }
/// <summary>
/// The quantity left in this location. Once the quantity reaches zero
/// the item gets deleted from the location.
/// </summary>
float Quantity { get; init; }
}

@ -0,0 +1,38 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Notifications;
using Connected.ServiceModel;
namespace Connected.Logistics.Stock;
/// <summary>
/// Represents the service which manipulates with stock items.
/// </summary>
[Service]
[ServiceUrl(StockUrls.Stock)]
public interface IStockService : IServiceNotifications<long>
{
/// <summary>
/// Updates the stock items at the specified location.
/// </summary>
/// <param name="args"></param>
Task Update(UpdateStockArgs args);
Task<IStock?> Select(PrimaryKeyArgs<long> args);
Task<IStock?> Select(EntityArgs args);
/// <summary>
/// Queries all stock items for the specified stock.
/// </summary>
/// <param name="args">The arguments containing the id of the stock</param>
/// <returns>The list of stock items that belong to the specified stock id.</returns>
Task<ImmutableList<IStockItem>> QueryItems(PrimaryKeyArgs<long> args);
/// <summary>
/// Queries stock items for the specified stock that are present in the specified
/// warehouse location.
/// </summary>
/// <param name="args">The arguments containing the crieria used by query.</param>
/// <returns>The list of stock items that are present in the specified warehouse location and
/// belong to the specified stock id.</returns>
Task<ImmutableList<IStockItem>> QueryItems(QueryStockItemsArgs args);
Task<IStockItem?> SelectItem(PrimaryKeyArgs<long> args);
}

@ -0,0 +1,36 @@
using Connected.Annotations;
using Connected.ServiceModel;
namespace Connected.Logistics.Stock;
/// <summary>
/// Represents the arguments when updating the stock items.
/// </summary>
public sealed class UpdateStockArgs : Dto
{
/// <summary>
/// The serial number of the item.
/// </summary>
[MinValue(1)]
public long Serial { get; set; }
/// <summary>
/// The warehouse location where the items are stored.
/// </summary>
[MinValue(1)]
public int Location { get; set; }
/// <summary>
/// The changed quantity. Can be a positive or negative
/// value.
/// </summary>
public float Quantity { get; set; }
}
public sealed class QueryStockItemsArgs : PrimaryKeyArgs<long>
{
[MinValue(1)]
public int Location { get; set; }
/// <summary>
/// The optional serial number.
/// </summary>
public long? Serial { get; set; }
}

@ -0,0 +1,5 @@
namespace Connected.Logistics.Stock;
public static class StockUrls
{
public const string Stock = "/logistics/stock";
}

@ -0,0 +1,8 @@
using Connected.Annotations;
[assembly: MicroService(MicroServiceType.Service)]
namespace Connected.Logistics.Stock;
internal sealed class Bootstrapper : Startup
{
}

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Connected\src\Connected\Connected.csproj" />
<ProjectReference Include="..\..\..\Connected.Framework\src\Connected.Runtime\Connected.Runtime.csproj" />
<ProjectReference Include="..\..\..\Connected.Framework\src\Connected.Services\Connected.Services.csproj" />
<ProjectReference Include="..\Connected.Logistics.Stock.Model\Connected.Logistics.Stock.Model.csproj" />
<ProjectReference Include="..\Connected.Logistics.Types.Model\Connected.Logistics.Types.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,60 @@
using Connected.Collections.Queues;
using Connected.Logistics.Types.WarehouseLocations;
using Connected.Middleware;
using Microsoft.Extensions.Logging;
namespace Connected.Logistics.Stock.Services;
internal sealed class StockAggregator : MiddlewareComponent, IQueueClient<PrimaryKeyQueueArgs<long>>
{
public StockAggregator(ILogger<StockAggregator> logger, IWarehouseLocationService locations, IStockService stock)
{
Logger = logger;
Locations = locations;
Stock = stock;
}
private ILogger<StockAggregator> Logger { get; }
private IWarehouseLocationService Locations { get; }
private IStockService Stock { get; }
public async Task Invoke(IQueueMessage message, PrimaryKeyQueueArgs<long> args)
{
if (await Stock.SelectItem(args.Id) is not IStockItem stock)
{
Logger.LogWarning("IStockItem not found {id}", args.Id);
return;
}
await Calculate(stock, stock.Location);
}
private async Task Calculate(IStockItem stock, int locationId)
{
if (await Locations.Select(locationId) is not IWarehouseLocation location)
{
Logger.LogWarning("IWarehouseLocation not found {id}", locationId);
return;
}
if (location.Parent is null)
return;
var parent = (int)location.Parent;
var sum = (await Stock.QueryItems(new QueryStockItemsArgs
{
Id = stock.Id,
Location = parent,
Serial = stock.Serial
})).Sum(f => f.Quantity);
await Stock.Update(new UpdateStockArgs
{
Location = parent,
Quantity = sum,
Serial = stock.Serial
});
await Calculate(stock, parent);
}
}

@ -0,0 +1,24 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Connected.Logistics.Types;
namespace Connected.Logistics.Stock;
[Table(Schema = Domain.Code)]
internal sealed record Stock : ConsistentEntity<long>, IStock
{
[Ordinal(0), Length(128), Index(Name = $"ix_{Domain.Code}_{nameof(Entity)}_{nameof(EntityId)}", Unique = true)]
public string Entity { get; init; } = default!;
[Ordinal(1), Length(128), Index(Name = $"ix_{Domain.Code}_{nameof(Entity)}_{nameof(EntityId)}", Unique = true)]
public string EntityId { get; init; } = default!;
[Ordinal(2)]
public float Quantity { get; init; }
[Ordinal(3)]
public float? Min { get; init; }
[Ordinal(4)]
public float? Max { get; init; }
}

@ -0,0 +1,24 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Connected.Logistics.Types;
namespace Connected.Logistics.Stock;
/// <inheritdoc cref="IStockItem"/>
[Table(Schema = Domain.Code)]
internal sealed record StockItem : ConsistentEntity<long>, IStockItem
{
public const string EntityKey = $"{Domain.Code}.{nameof(StockItem)}";
/// <inheritdoc cref="IStockItem.Stock"/>
[Ordinal(0)]
public long Stock { get; init; }
/// <inheritdoc cref="IStockItem.Location"/>
[Ordinal(1)]
public int Location { get; init; }
/// <inheritdoc cref="IStockItem.Serial"/>
[Ordinal(2)]
public long Serial { get; init; }
/// <inheritdoc cref="IStockItem.Quantity"/>
[Ordinal(3)]
public float Quantity { get; init; }
}

@ -0,0 +1,187 @@
using Connected.Caching;
using Connected.Collections.Queues;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Logistics.Stock.Services;
using Connected.Logistics.Types.Serials;
using Connected.Logistics.Types.WarehouseLocations;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
using Connected.Threading;
namespace Connected.Logistics.Stock;
internal sealed class StockOps
{
public const string StockQueue = "Stock";
static StockOps()
{
Locker = new();
}
private static AsyncLockerSlim Locker { get; }
/// <summary>
/// This method ensures that a stock (parent) record exists.
/// </summary>
/// <remarks>
/// The stock record is not created explicitly since this would introduce unnecessary complexity. It is
/// instead created on the fly when the first request is made. The tricky part is it must be thread safe
/// so we need an async locker since lock statement does not support async calls.
/// </remarks>
private static async Task<IStock> Ensure(IStorageProvider storage, IStockService stock, EntityArgs args)
{
/*
* First check for existence so we don't need to perform a lock if the record is found.
*/
if (await stock.Select(args) is IStock existing)
return existing;
/*
* Doesn't exist.
* Perform an async lock to ensure no one else is trying to insert the item.
*/
return await Locker.LockAsync(async () =>
{
/*
* Read again if two or more threads were competing for the insert. The thing is this
* is happening quite frequently even in semi loaded warehouse systems.
*/
if (await stock.Select(args) is IStock existing2)
return existing2;
/*
* Still nothing. We are safe to insert a new stock descriptor. Note that in scalable environments
* there is still a possibillity that two requests would made it here but from different processes.
* Thus we should have a unique constraint on the entity ensuring only one request will win, all the others
* lose. This also means the provider owning the entity must support unique constraints.
*/
var entity = await storage.Open<Stock>().Update(args.AsEntity<Stock>(State.New));
var result = await stock.Select(entity.Id);
/*
* This should not happen anyway but we'll do it for the sake of sompiler warning.
*/
if (result is null)
throw new NullReferenceException(nameof(IStock));
return result;
});
}
public sealed class Update : ServiceFunction<UpdateStockArgs, long>
{
public Update(IStorageProvider storage, ICacheContext cache, IEventService events, IStockService stock,
ISerialService serials, IQueueService queue, IWarehouseLocationService locations)
{
Storage = storage;
Cache = cache;
Events = events;
Stock = stock;
Serials = serials;
Queue = queue;
Locations = locations;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IStockService Stock { get; }
private ISerialService Serials { get; }
private IQueueService Queue { get; }
private IWarehouseLocationService Locations { get; }
private bool IsLeaf { get; set; }
protected override async Task<long> OnInvoke()
{
/*
* We need this info for queueing aggregations.
*/
IsLeaf = (await Locations.Select(Arguments.Location)).ItemCount == 0;
/*
* Validators should validate the existence. Serials don't get deleted.
*/
if (await Serials.Select(Arguments.Serial) is not ISerial serial)
return 0;
/*
* Ensure the stock record exists.
*/
var stock = await Ensure(Storage, Stock, serial.AsArguments<EntityArgs, long>());
/*
* Now we must check if the stock item exists for the specified serial and
* warehouse location. If so we'll only update the quantity.
*/
if (await FindExisting(stock) is not StockItem existing)
return await InsertItem(stock);
else
return await UpdateItem(existing);
}
private async Task<long> InsertItem(IStock stock)
{
return await Locker.LockAsync(async () =>
{
/*
* Query again if someone overtook us.
*/
if (await FindExisting(stock) is not StockItem existing)
{
/*
* Still doesn't exist, it's safe to insert it since we are in the locked area.
*/
return (await Storage.Open<StockItem>().Update(Arguments.AsEntity<StockItem>(State.New))).Id;
}
else
{
/*
* Indeed, there was a record inserted in the meantime.
*/
return await UpdateItem(existing);
}
});
}
/// <summary>
/// Performs the update on the existing stock item.
/// </summary>
/// <param name="item">The stock item to be updated.</param>
private async Task<long> UpdateItem(StockItem item)
{
await Storage.Open<StockItem>().Update(item, Arguments, async () =>
{
await Cache.Remove(StockItem.EntityKey, item.Id);
return SetState((await Stock.SelectItem(item.Id)) as StockItem);
},
async (e) =>
{
var quantity = item.Quantity + Arguments.Quantity;
await Task.CompletedTask;
return e.Merge(Arguments, State.Default, new { Quantity = quantity });
});
return item.Id;
}
private async Task<StockItem?> FindExisting(IStock stock)
{
var items = await Stock.QueryItems(new QueryStockItemsArgs
{
Id = stock.Id,
Location = Arguments.Location,
Serial = Arguments.Serial
});
if (items.IsEmpty || items[0] is not StockItem existing)
return null;
return existing;
}
protected override async Task OnCommitted()
{
await Cache.Remove(StockItem.EntityKey, Result);
await Events.Enqueue(this, Stock, nameof(IStockService.Updated), new PrimaryKeyArgs<long> { Id = Result });
if (IsLeaf)
await Queue.Enqueue<StockAggregator, PrimaryKeyQueueArgs<long>>(new PrimaryKeyQueueArgs<long> { Id = Result });
}
}
}

@ -0,0 +1,41 @@
using System.Collections.Immutable;
using Connected.ServiceModel;
using Connected.Services;
namespace Connected.Logistics.Stock;
internal sealed class StockService : EntityService<long>, IStockService
{
public StockService(IContext context) : base(context)
{
}
public Task<ImmutableList<IStockItem>> QueryItems(PrimaryKeyArgs<long> args)
{
throw new NotImplementedException();
}
public Task<ImmutableList<IStockItem>> QueryItems(QueryStockItemsArgs args)
{
throw new NotImplementedException();
}
public Task<IStock?> Select(PrimaryKeyArgs<long> args)
{
throw new NotImplementedException();
}
public Task<IStock?> Select(EntityArgs args)
{
throw new NotImplementedException();
}
public Task<IStockItem?> SelectItem(PrimaryKeyArgs<long> args)
{
throw new NotImplementedException();
}
public Task Update(UpdateStockArgs args)
{
throw new NotImplementedException();
}
}

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Connected.Logistics.Types</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Connected\src\Connected\Connected.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,6 @@
namespace Connected.Logistics.Types;
public static class Domain
{
public const string Name = "Logistics";
public const string Code = "lgs";
}

@ -0,0 +1,8 @@
namespace Connected.Logistics.Types;
public static class LogisticsUrls
{
public const string Warehouses = "/logistics/types/warehouses";
public const string WarehouseLocations = "/logistics/types/warehouseLocations";
public const string Packing = "/logistics/types/packing";
public const string Serials = "/logistics/types/serials";
}

@ -0,0 +1,21 @@
using Connected.Data;
namespace Connected.Logistics.Types.Packaging;
public interface IPacking : IPrimaryKey<int>
{
string Ean { get; init; }
string Entity { get; init; }
string EntityId { get; init; }
float? Quantity { get; init; }
float? NetWeight { get; init; }
float? GrossWeight { get; init; }
int? Width { get; init; }
int? Height { get; init; }
int? Depth { get; init; }
int? ItemCount { get; init; }
Status Status { get; init; }
}

@ -0,0 +1,34 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Notifications;
using Connected.ServiceModel;
namespace Connected.Logistics.Types.Packaging;
[Service]
[ServiceUrl(LogisticsUrls.Packing)]
public interface IPackingService : IServiceNotifications<int>
{
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<IPacking>> Query(QueryArgs? args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<IPacking>> Query(PrimaryKeyListArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<IPacking?> Select(PrimaryKeyArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<IPacking?> Select(SelectPackingArgs args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Put)]
Task<int> Insert(InsertPackingArgs args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Update(UpdatePackingArgs args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Patch(PatchArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Delete | ServiceMethodVerbs.Post)]
Task Delete(PrimaryKeyArgs<int> args);
}

@ -0,0 +1,54 @@
using System.ComponentModel.DataAnnotations;
using Connected.Data;
using Connected.ServiceModel;
namespace Connected.Logistics.Types.Packaging;
public sealed class InsertPackingArgs : EntityArgs
{
[Required, MaxLength(32)]
public string Ean { get; set; } = default!;
public float? Quantity { get; set; }
public float? NetWeight { get; set; }
public float? GrossWeight { get; set; }
public int? Width { get; set; }
public int? Height { get; set; }
public int? Depth { get; set; }
public int? ItemCount { get; set; }
public Status Status { get; set; } = Status.Disabled;
}
public sealed class UpdatePackingArgs : PrimaryKeyArgs<int>
{
[Required, MaxLength(32)]
public string Ean { get; set; } = default!;
public float? Quantity { get; set; }
public float? NetWeight { get; set; }
public float? GrossWeight { get; set; }
public int? Width { get; set; }
public int? Height { get; set; }
public int? Depth { get; set; }
public int? ItemCount { get; set; }
public Status Status { get; set; } = Status.Disabled;
}
public sealed class SelectPackingArgs : Dto
{
[Required, MaxLength(32)]
public string Ean { get; set; } = default!;
}

@ -0,0 +1,54 @@
using Connected.Data;
namespace Connected.Logistics.Types.Serials;
/// <summary>
/// Represents a serial number in the logistic environment.
/// </summary>
/// <remarks>
/// The primary usage of the Serial is in warehouse management
/// system, where every item in the stock is labeled with serial number.
/// Once the item is received and before it is put in the stock locations,
/// it receives a unique serial number. If the item is moved between stock
/// locations and even warehouses, its serial value remains the same. Serial
/// number plays a key role in traceability.
/// </remarks>
public interface ISerial : IPrimaryKey<long>
{
/// <summary>
/// The entity which owns the serial number. This could
/// be Product, Semi product, Raw or any other type of
/// entity which needs some kind of labeling.
/// </summary>
string Entity { get; init; }
/// <summary>
/// The primary key of the entity. This points to the exact record of
/// the Entity type.
/// </summary>
string EntityId { get; init; }
/// <summary>
/// The actual Serial number. System can use different middleware techniques
/// to obtain this value because it's very common to be have project specific
/// implementation to calculate this value.
/// </summary>
string Value { get; init; }
/// <summary>
/// The remaining quantity in the stock for this serial. This value can increase
/// or decrease depending on the warehouse implementation. Some systems do reuse
/// the same serial between different receives.
/// </summary>
float Quantity { get; init; }
/// <summary>
/// The date serial was created.
/// </summary>
DateTimeOffset Created { get; init; }
/// <summary>
/// If the item has limited shelf life, this value should hold the date when
/// the shelf life expires.
/// </summary>
DateTimeOffset? BestBefore { get; init; }
/// <summary>
/// The status of the serial number. If the status is <see cref="Status.Disabled"/> the
/// processes using the serial number should not allow the entity to be used in documents.
/// </summary>
Status Status { get; init; }
}

@ -0,0 +1,71 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Notifications;
using Connected.ServiceModel;
namespace Connected.Logistics.Types.Serials;
/// <summary>
/// The service for manipulating with serials. A <see cref="ISerial"/> is a fundamental
/// entity used by labeling and traceability systems.
/// </summary>
[Service]
[ServiceUrl(LogisticsUrls.Serials)]
public interface ISerialService : IServiceNotifications<long>
{
/// <summary>
/// Queries all serial numbers.
/// </summary>
/// <param name="args">The optional arguments specifiying the
/// behavior of the result set.</param>
/// <returns>A list of <see cref="ISerial"/> entities.</returns>
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<ISerial>> Query(QueryArgs? args);
/// <summary>
/// Performs a lookup on the serials for the specified set of ids.
/// </summary>
/// <param name="args">The arguments containing the list of ids for
/// which the entities will be returned.</param>
/// <returns>A list of entities that matches the specified ids.</returns>
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<ISerial>> Query(PrimaryKeyListArgs<long> args);
/// <summary>
/// Returns the first serial that matches the specified id.
/// </summary>
/// <param name="args">The arguments containing the id of the entity.</param>
/// <returns>The <see cref="ISerial"/> if found, <c>null</c> otherwise.</returns>
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ISerial?> Select(PrimaryKeyArgs<long> args);
/// <summary>
/// Returns the first serial with the specified value.
/// </summary>
/// <param name="args">The arguments containing the value for which serial
/// entity will be returned.</param>
/// <returns>The <see cref="ISerial"/> if found, <c>null</c> otherwise.</returns>
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ISerial?> Select(SelectSerialArgs args);
/// <summary>
/// Inserts a new serial number.
/// </summary>
/// <param name="args">The arguments containing the properties of the new serial.</param>
/// <returns>The id of the newly inserted serial.</returns>
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Put)]
Task<long> Insert(InsertSerialArgs args);
/// <summary>
/// Updates an existing serial.
/// </summary>
/// <param name="args">The arguments containing properties which will change the entity.</param>
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Update(UpdateSerialArgs args);
/// <summary>
/// Performs a partial update on the serial.
/// </summary>
/// <param name="args">The arguments containing properties which has to be updated.</param>
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Patch(PatchArgs<long> args);
/// <summary>
/// Peranently deletes the serial from the storage.
/// </summary>
/// <param name="args">The arguments containing the id of the entity to be deleted.</param>
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Delete)]
Task Delete(PrimaryKeyArgs<long> args);
}

@ -0,0 +1,59 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Data;
using Connected.ServiceModel;
namespace Connected.Logistics.Types.Serials;
/// <summary>
/// Arguments used when inserting a new serial number.
/// </summary>
public sealed class InsertSerialArgs : Dto
{
/// <inheritdoc cref="ISerial.Entity"/>
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
/// <inheritdoc cref="ISerial.EntityId"/>
[Required, MaxLength(128)]
public string EntityId { get; set; } = default!;
/// <inheritdoc cref="ISerial.Quantity"/>
[MinValue(0)]
public float Quantity { get; set; }
/// <inheritdoc cref="ISerial.Created"/>
/// <remarks>
/// If this property is null, the process will most likely
/// set the value of the current date (DateTime.UtcNow).
/// </remarks>
public DateTimeOffset? Created { get; set; }
/// <inheritdoc cref="ISerial.BestBefore"/>
public DateTimeOffset? BestBefore { get; set; }
/// <inheritdoc cref="ISerial.Status"/>
public Status Status { get; set; } = Status.Disabled;
/// <inheritdoc cref="ISerial.Value"/>
[Required, MaxLength(128)]
public string Value { get; set; } = default!;
}
/// <summary>
/// The arguments used when updating the existing serial entity.
/// </summary>
public sealed class UpdateSerialArgs : PrimaryKeyArgs<long>
{
/// <summary>
/// The new quantity. This is an absolute value, do not provide
/// delta values.
/// </summary>
[MinValue(0)]
public float Quantity { get; set; }
/// <inheritdoc cref="ISerial.BestBefore"/>
public DateTimeOffset? BestBefore { get; set; }
/// <inheritdoc cref="ISerial.Status"/>
public Status Status { get; set; } = Status.Disabled;
}
/// <summary>
/// The arguments used for selecting serial by its value.
/// </summary>
public sealed class SelectSerialArgs : Dto
{
/// <inheritdoc cref="ISerial.Value"/>
[Required, MaxLength(128)]
public string Value { get; set; } = default!;
}

@ -0,0 +1,26 @@
using Connected.Data;
namespace Connected.Logistics.Types.WarehouseLocations;
/// <summary>
/// Represents a physical or logical location inside a <see cref="IWarehouse"/>.
/// </summary>
/// <remarks>
/// Each <see cref="IWarehouse"/> contains zero or more <see cref="IWarehouseLocation">locations</see>.
/// Location can be a container, which means it contains child locations, or leaf, which doesn't contain
/// child locations. Items can be put only in leaf locations, whereas containers acts only as aggregators
/// which means they provide calculated values for items contained in the child locations.
/// </remarks>
public interface IWarehouseLocation : IPrimaryKey<int>
{
int? Parent { get; init; }
int Warehouse { get; init; }
string Name { get; init; }
string Code { get; init; }
Status Status { get; init; }
/// <summary>
/// The number of direct child items that belong to this
/// location. If this value is 0 it means the location
/// is leaf. If not, it's a container.
/// </summary>
int ItemCount { get; init; }
}

@ -0,0 +1,28 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Notifications;
using Connected.ServiceModel;
namespace Connected.Logistics.Types.WarehouseLocations;
[Service]
[ServiceUrl(LogisticsUrls.WarehouseLocations)]
public interface IWarehouseLocationService : IServiceNotifications<int>
{
Task<ImmutableList<IWarehouseLocation>> Query(QueryArgs? args);
/// <summary>
/// Queries warehouse locations for the specified <see cref="IWarehouse"/>.
/// </summary>
/// <param name="args">The arguments containing the id of the warehouse.</param>
/// <returns>The list of warehouse locations that belong to the specified warehouse.</returns>
Task<ImmutableList<IWarehouseLocation>> Query(QueryWarehouseLocationArgs args);
Task<ImmutableList<IWarehouseLocation>> Query(PrimaryKeyListArgs<int> args);
Task<ImmutableList<IWarehouseLocation>> QueryChildren(QueryWarehouseLocationChildrenArgs args);
Task<IWarehouseLocation?> Select(PrimaryKeyArgs<int> args);
Task<IWarehouseLocation?> Select(SelectWarehouseLocationArgs args);
Task<int> Insert(InsertWarehouseLocationArgs args);
Task Update(UpdateWarehouseLocationArgs args);
Task Patch(PatchArgs<int> args);
Task Delete(PrimaryKeyArgs<int> args);
}

@ -0,0 +1,54 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Data;
using Connected.ServiceModel;
namespace Connected.Logistics.Types.WarehouseLocations;
public sealed class InsertWarehouseLocationArgs : Dto
{
public int? Parent { get; set; }
[MinValue(1)]
public int Warehouse { get; set; }
[Required, MaxLength(128)]
public string Name { get; set; } = default!;
[Required, MaxLength(32)]
public string Code { get; set; } = default!;
public Status Status { get; set; } = Status.Disabled;
}
public sealed class UpdateWarehouseLocationArgs : PrimaryKeyArgs<int>
{
public int? Parent { get; set; }
[Required, MaxLength(128)]
public string Name { get; set; } = default!;
[Required, MaxLength(32)]
public string Code { get; set; } = default!;
public Status Status { get; set; } = Status.Disabled;
}
public sealed class SelectWarehouseLocationArgs : Dto
{
[Required, MaxLength(32)]
public string Code { get; set; } = default!;
}
public sealed class QueryWarehouseLocationArgs : QueryArgs
{
[MinValue(1)]
public int Warehouse { get; set; }
}
public sealed class QueryWarehouseLocationChildrenArgs : QueryArgs
{
[MinValue(1)]
public int Warehouse { get; set; }
public int? Parent { get; set; }
}

@ -0,0 +1,10 @@
using Connected.Data;
namespace Connected.Logistics.Types.Warehouses;
public interface IWarehouse : IPrimaryKey<int>
{
string Name { get; init; }
string Code { get; init; }
Status Status { get; init; }
}

@ -0,0 +1,32 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Notifications;
using Connected.ServiceModel;
namespace Connected.Logistics.Types.Warehouses;
[Service]
[ServiceUrl(LogisticsUrls.Warehouses)]
public interface IWarehouseService : IServiceNotifications<int>
{
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<IWarehouse>> Query(QueryArgs? args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<IWarehouse>> Query(PrimaryKeyListArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<IWarehouse?> Select(PrimaryKeyArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Put)]
Task<int> Insert(InsertWarehouseArgs args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Update(UpdateWarehouseArgs args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Patch(PatchArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Delete)]
Task Delete(PrimaryKeyArgs<int> args);
}

@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
using Connected.Data;
using Connected.ServiceModel;
namespace Connected.Logistics.Types.Warehouses;
public class InsertWarehouseArgs : Dto
{
[Required, MaxLength(128)]
public string Name { get; set; } = default!;
[Required, MaxLength(32)]
public string Code { get; set; } = default!;
public Status Status { get; set; } = Status.Disabled;
}
public class UpdateWarehouseArgs : PrimaryKeyArgs<int>
{
[Required, MaxLength(128)]
public string Name { get; set; } = default!;
[Required, MaxLength(32)]
public string Code { get; set; } = default!;
public Status Status { get; set; } = Status.Disabled;
}

@ -0,0 +1,8 @@
using Connected.Annotations;
[assembly: MicroService(MicroServiceType.Service)]
namespace Connected.Logistics.Types;
internal sealed class Bootstrapper : Startup
{
}

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Connected.Framework\src\Connected.Entities\Connected.Entities.csproj" />
<ProjectReference Include="..\..\..\Connected.Framework\src\Connected.Services\Connected.Services.csproj" />
<ProjectReference Include="..\Connected.Logistics.Types.Model\Connected.Logistics.Types.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,46 @@
using Connected.Annotations;
using Connected.Data;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
namespace Connected.Logistics.Types.Packaging;
[Table(Schema = Domain.Code)]
internal sealed record Packing : ConsistentEntity<int>, IPacking
{
public const string EntityKey = $"{Domain.Code}.{nameof(Packing)}";
[Ordinal(0), Length(32)]
[Index(Unique = true)]
public string Ean { get; init; } = default!;
[Ordinal(1), Length(128)]
public string Entity { get; init; } = default!;
[Ordinal(2), Length(128)]
public string EntityId { get; init; } = default!;
[Ordinal(3)]
public float? Quantity { get; init; }
[Ordinal(4)]
public float? NetWeight { get; init; }
[Ordinal(5)]
public float? GrossWeight { get; init; }
[Ordinal(6)]
public int? Width { get; init; }
[Ordinal(7)]
public int? Height { get; init; }
[Ordinal(8)]
public int? Depth { get; init; }
[Ordinal(9)]
public int? ItemCount { get; init; }
[Ordinal(10)]
public Status Status { get; init; } = Status.Disabled;
}

@ -0,0 +1,177 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Connected.Logistics.Types.Packaging;
internal sealed class PackingOps
{
public sealed class Delete : ServiceAction<PrimaryKeyArgs<int>>
{
public Delete(IStorageProvider storage, IPackingService packingService, IEventService events, ICachingService cache)
{
Storage = storage;
PackingService = packingService;
Events = events;
Cache = cache;
}
private IStorageProvider Storage { get; }
private IPackingService PackingService { get; }
private IEventService Events { get; }
private ICachingService Cache { get; }
protected override async Task OnInvoke()
{
if (await PackingService.Select(Arguments.Id) is not IPacking packing)
return;
SetState(packing);
await Storage.Open<Packing>().Update(Arguments.AsEntity<Packing>(State.Deleted));
}
protected override async Task OnCommitted()
{
await Cache.Remove(Packing.EntityKey, Arguments.Id);
await Events.Enqueue(this, Events, nameof(IPackingService.Deleted), Arguments);
}
}
public sealed class Insert : ServiceFunction<InsertPackingArgs, int>
{
public Insert(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<int> OnInvoke()
{
return (await Storage.Open<Packing>().Update(Arguments.AsEntity<Packing>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Events, nameof(IPackingService.Inserted), Arguments);
}
}
public sealed class Query : ServiceFunction<QueryArgs, ImmutableList<IPacking>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IPacking>?> OnInvoke()
{
return await (from e in Storage.Open<Packing>()
select e).WithArguments(Arguments).AsEntities<IPacking>();
}
}
public sealed class Lookup : ServiceFunction<PrimaryKeyListArgs<int>, ImmutableList<IPacking>>
{
public Lookup(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IPacking>> OnInvoke()
{
return await (from e in Storage.Open<Packing>()
where Arguments.IdList.Any(f => f == e.Id)
select e).AsEntities<IPacking>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<int>, IPacking>
{
public Select(IStorageProvider storage, ICachingService cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
protected override async Task<IPacking?> OnInvoke()
{
return await Cache.Get<IPacking>(Packing.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<Packing>()
where e.Id == Arguments.Id
select e).AsEntity();
});
}
}
public sealed class SelectByEan : NullableServiceFunction<SelectPackingArgs, IPacking>
{
public SelectByEan(IStorageProvider storage, ICachingService cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
protected override async Task<IPacking?> OnInvoke()
{
return await Cache.Get<IPacking>(Packing.EntityKey, f => string.Equals(f.Ean, Arguments.Ean, StringComparison.OrdinalIgnoreCase), async (f) =>
{
return await (from e in Storage.Open<Packing>()
where string.Equals(e.Ean, Arguments.Ean, StringComparison.OrdinalIgnoreCase)
select e).AsEntity();
});
}
}
public sealed class Update : ServiceAction<UpdatePackingArgs>
{
public Update(IStorageProvider storage, ICachingService cache, IPackingService packingService, IEventService events)
{
Storage = storage;
Cache = cache;
PackingService = packingService;
Events = events;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
private IPackingService PackingService { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
if (SetState(await PackingService.Select(Arguments.Id)) is not Packing entity)
return;
await Storage.Open<Packing>().Update(entity.Merge(Arguments, State.Default), Arguments, async () =>
{
await Cache.Remove(Packing.EntityKey, Arguments.Id);
return SetState(await PackingService.Select(Arguments.Id)) as Packing;
});
}
protected override async Task OnCommitted()
{
await Cache.Remove(Packing.EntityKey, Arguments.Id);
await Events.Enqueue(this, PackingService, nameof(PackingService.Updated), Arguments);
}
}
}

@ -0,0 +1,56 @@
using System.Collections.Immutable;
using Connected.Entities;
using Connected.ServiceModel;
using Connected.Services;
using Ops = Connected.Logistics.Types.Packaging.PackingOps;
namespace Connected.Logistics.Types.Packaging;
internal sealed class PackingService : EntityService<int>, IPackingService
{
public PackingService(IContext context) : base(context)
{
}
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
public async Task<int> Insert(InsertPackingArgs args)
{
return await Invoke(GetOperation<Ops.Insert>(), args);
}
public async Task Patch(PatchArgs<int> args)
{
if (await Select(args.Id) is not Packing entity)
return;
await Update(args.Patch<UpdatePackingArgs, Packing>(entity));
}
public async Task<ImmutableList<IPacking>> Query(QueryArgs? args)
{
return await Invoke(GetOperation<Ops.Query>(), args ?? QueryArgs.Default);
}
public async Task<ImmutableList<IPacking>> Query(PrimaryKeyListArgs<int> args)
{
return await Invoke(GetOperation<Ops.Lookup>(), args);
}
public async Task<IPacking?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
public async Task<IPacking?> Select(SelectPackingArgs args)
{
return await Invoke(GetOperation<Ops.SelectByEan>(), args);
}
public async Task Update(UpdatePackingArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
}

@ -0,0 +1,45 @@
using Connected.Middleware;
using Connected.Validation;
namespace Connected.Logistics.Types.Packaging;
internal sealed class InsertPackingValidator : MiddlewareComponent, IValidator<InsertPackingArgs>
{
public InsertPackingValidator(IPackingService packingService)
{
PackingService = packingService;
}
public IPackingService PackingService { get; }
public async Task Validate(InsertPackingArgs args)
{
if (await PackingService.Select(new SelectPackingArgs
{
Ean = args.Ean
}) is not null)
{
throw ValidationExceptions.ValueExists(nameof(args.Ean), args.Ean);
}
}
}
internal sealed class UpdatePackingValidator : MiddlewareComponent, IValidator<UpdatePackingArgs>
{
public UpdatePackingValidator(IPackingService packingService)
{
PackingService = packingService;
}
public IPackingService PackingService { get; }
public async Task Validate(UpdatePackingArgs args)
{
if (await PackingService.Select(new SelectPackingArgs
{
Ean = args.Ean
}) is Packing entity && entity.Id != args.Id)
{
throw ValidationExceptions.ValueExists(nameof(args.Ean), args.Ean);
}
}
}

@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Data;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
namespace Connected.Logistics.Types.Serials;
/// <inheritdoc cref="ISerial"/>
[Table(Schema = Domain.Code)]
internal sealed record Serial : ConsistentEntity<long>, ISerial
{
/// <summary>
/// The entity identifier which can be used in caching keys for example.
/// </summary>
public const string EntityKey = $"{Domain.Code}.{nameof(Serial)}";
/// <inheritdoc cref="ISerial.Entity"/>
[Ordinal(0), MaxLength(128)]
public string Entity { get; init; } = default!;
/// <inheritdoc cref="ISerial.EntityId"/>
[Ordinal(1), MaxLength(128)]
public string EntityId { get; init; } = default!;
/// <inheritdoc cref="ISerial.Value"/>
[Ordinal(2), MaxLength(128), Index(Unique = true)]
public string Value { get; init; } = default!;
/// <inheritdoc cref="ISerial.Quantity"/>
[Ordinal(3)]
public float Quantity { get; init; }
/// <inheritdoc cref="ISerial.Created"/>
[Ordinal(4)]
public DateTimeOffset Created { get; init; }
/// <inheritdoc cref="ISerial.BestBefore"/>
[Ordinal(5)]
public DateTimeOffset? BestBefore { get; init; }
/// <inheritdoc cref="ISerial.Status"/>
[Ordinal(6)]
public Status Status { get; init; } = Status.Disabled;
}

@ -0,0 +1,220 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Connected.Logistics.Types.Serials;
internal sealed class SerialOps
{
/// <inheritdoc cref="ISerialService.Delete(PrimaryKeyArgs{long})"/>
public sealed class Delete : ServiceAction<PrimaryKeyArgs<long>>
{
public Delete(IStorageProvider storage, ISerialService serials, IEventService events, ICachingService cache)
{
Storage = storage;
Serials = serials;
Events = events;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ISerialService Serials { get; }
private IEventService Events { get; }
private ICachingService Cache { get; }
protected override async Task OnInvoke()
{
if (await Serials.Select(Arguments.Id) is not ISerial serial)
return;
/*
* Setting state of the entity enable other middleware to use the entity even after it
* is deleted. For example, IEventListener will receive the state of this operation.
*/
SetState(serial);
/*
* Perform delete.
*/
await Storage.Open<Serial>().Update(Arguments.AsEntity<Serial>(State.Deleted));
}
protected override async Task OnCommitted()
{
/*
* Remove entity from the cache.
*/
await Cache.Remove(Serial.EntityKey, Arguments.Id);
/*
* Enqueue event so event listeners can respond to the transaction.
*/
await Events.Enqueue(this, Events, nameof(ISerialService.Deleted), Arguments);
}
}
/// <inheritdoc cref=" ISerialService.Insert(InsertSerialArgs)"/>
public sealed class Insert : ServiceFunction<InsertSerialArgs, long>
{
public Insert(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<long> OnInvoke()
{
/*
* Perform insert and return the newly inserted id.
*/
return (await Storage.Open<Serial>().Update(Arguments.AsEntity<Serial>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Events, nameof(ISerialService.Inserted), Arguments);
}
}
/// <inheritdoc cref="ISerialService.Query(QueryArgs?)"/>
public sealed class Query : ServiceFunction<QueryArgs, ImmutableList<ISerial>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<ISerial>> OnInvoke()
{
/*
* For non cached entities query always hits the storage.
*/
return await (from e in Storage.Open<Serial>()
select e).WithArguments(Arguments).AsEntities<ISerial>();
}
}
/// <inheritdoc cref="ISerialService.Query(PrimaryKeyListArgs{long})"/>
public sealed class Lookup : ServiceFunction<PrimaryKeyListArgs<long>, ImmutableList<ISerial>>
{
public Lookup(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<ISerial>> OnInvoke()
{
return await (from e in Storage.Open<Serial>()
where Arguments.IdList.Any(f => f == e.Id)
select e).AsEntities<ISerial>();
}
}
/// <inheritdoc cref="ISerialService.Select(PrimaryKeyArgs{long})"/>
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<long>, ISerial>
{
public Select(IStorageProvider storage, ICachingService cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
protected override async Task<ISerial?> OnInvoke()
{
/*
* First, try to receive entity from the cache. If it doesn't exist in the cache
* load it from storage. If storage returns non null value, store it in the cache
* for subsequent calls. The entity gets remove either because of inactivity or when
* updating or deleting it.
*/
return await Cache.Get<ISerial>(Serial.EntityKey, Arguments.Id, async (f) =>
{
/*
* Doesn't exist in the cache. Let's do the storage action.
*/
return await (from e in Storage.Open<Serial>()
where e.Id == Arguments.Id
select e).AsEntity();
});
}
}
/// <inheritdoc cref="ISerialService.Select(SelectSerialArgs)"/>
public sealed class SelectByValue : NullableServiceFunction<SelectSerialArgs, ISerial>
{
public SelectByValue(IStorageProvider storage, ICachingService cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
protected override async Task<ISerial?> OnInvoke()
{
return await Cache.Get<ISerial>(Serial.EntityKey, f => string.Equals(f.Value, Arguments.Value, StringComparison.OrdinalIgnoreCase), async (f) =>
{
return await (from e in Storage.Open<Serial>()
where string.Equals(e.Value, Arguments.Value, StringComparison.OrdinalIgnoreCase)
select e).AsEntity();
});
}
}
/// <inheritdoc cref="ISerialService.Update(UpdateSerialArgs)"/>
public sealed class Update : ServiceAction<UpdateSerialArgs>
{
public Update(IStorageProvider storage, ICachingService cache, ISerialService packingService, IEventService events)
{
Storage = storage;
Cache = cache;
Serials = packingService;
Events = events;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
private ISerialService Serials { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
/*
* Set the state of the unchanged entity. This enable other middleware to
* calculate quantity delta, for example because they receive the state of
* this operation. Not all middleware supports this, the primary example of
* such state client is IEventListener.
*/
if (SetState(await Serials.Select(Arguments.Id)) is not Serial entity)
return;
/*
* Sinc ethis is concurrent entity we must perform retry if the concurrency fails.
*/
await Storage.Open<Serial>().Update(entity.Merge(Arguments, State.Default), Arguments, async () =>
{
/*
* The update failed because of concurrency. Remove the entity from the cache to ensure
* it gets loaded from the storage next time with fresh values and try again.
*/
await Cache.Remove(Serial.EntityKey, Arguments.Id);
/*
* Since the entity reloaded we must overwrite its state.
*/
return SetState(await Serials.Select(Arguments.Id)) as Serial;
});
}
protected override async Task OnCommitted()
{
await Cache.Remove(Serial.EntityKey, Arguments.Id);
await Events.Enqueue(this, Serials, nameof(Serials.Updated), Arguments);
}
}
}

@ -0,0 +1,61 @@
using System.Collections.Immutable;
using Connected.Entities;
using Connected.ServiceModel;
using Connected.Services;
using Ops = Connected.Logistics.Types.Serials.SerialOps;
namespace Connected.Logistics.Types.Serials;
/// <inheritdoc cref="ISerialService"/>
internal sealed class SerialService : EntityService<long>, ISerialService
{
/// <summary>
/// Create a new <see cref="SerialService"/> service.
/// </summary>
/// <param name="context">The context which serves as a DI scope.</param>
public SerialService(IContext context) : base(context)
{
}
/// <inheritdoc cref="ISerialService.Delete(PrimaryKeyArgs{long})"/>
public async Task Delete(PrimaryKeyArgs<long> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
/// <inheritdoc cref="ISerialService.Insert(InsertSerialArgs)"/>
public async Task<long> Insert(InsertSerialArgs args)
{
return await Invoke(GetOperation<Ops.Insert>(), args);
}
/// <inheritdoc cref="ISerialService.Query(QueryArgs?)"/>
public async Task<ImmutableList<ISerial>> Query(QueryArgs? args)
{
return await Invoke(GetOperation<Ops.Query>(), args ?? QueryArgs.Default);
}
/// <inheritdoc cref="ISerialService.Query(PrimaryKeyListArgs{long})"/>
public async Task<ImmutableList<ISerial>> Query(PrimaryKeyListArgs<long> args)
{
return await Invoke(GetOperation<Ops.Lookup>(), args);
}
/// <inheritdoc cref="ISerialService.Select(PrimaryKeyArgs{long})"/>
public async Task<ISerial?> Select(PrimaryKeyArgs<long> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
/// <inheritdoc cref="ISerialService.Select(SelectSerialArgs)"/>
public async Task<ISerial?> Select(SelectSerialArgs args)
{
return await Invoke(GetOperation<Ops.SelectByValue>(), args);
}
/// <inheritdoc cref="ISerialService.Update(UpdateSerialArgs)"/>
public async Task Update(UpdateSerialArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
/// <inheritdoc cref="ISerialService.Patch(PatchArgs{long})"/>
public async Task Patch(PatchArgs<long> args)
{
if (await Select(args.Id) is not Serial entity)
return;
await Update(args.Patch<UpdateSerialArgs, Serial>(entity));
}
}

@ -0,0 +1,24 @@
using Connected.Middleware;
using Connected.Validation;
namespace Connected.Logistics.Types.Serials;
internal sealed class InsertSerialValidator : MiddlewareComponent, IValidator<InsertSerialArgs>
{
public InsertSerialValidator(ISerialService serials)
{
Serials = serials;
}
public ISerialService Serials { get; }
public async Task Validate(InsertSerialArgs args)
{
if (await Serials.Select(new SelectSerialArgs
{
Value = args.Value
}) is not null)
{
throw ValidationExceptions.ValueExists(nameof(args.Value), args.Value);
}
}
}

@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Data;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
namespace Connected.Logistics.Types.WarehouseLocations;
/// <inheritdoc cref="IWarehouseLocation"/>
internal sealed record WarehouseLocation : ConsistentEntity<int>, IWarehouseLocation
{
public const string EntityKey = $"{Domain.Code}.{nameof(WarehouseLocation)}";
/// <inheritdoc cref="IWarehouseLocation.Parent"/>
[Ordinal(0)]
public int? Parent { get; init; }
/// <inheritdoc cref="IWarehouseLocation.Warehouse"/>
[Ordinal(1)]
public int Warehouse { get; init; }
/// <inheritdoc cref="IWarehouseLocation.Name"/>
[Ordinal(2), MaxLength(128)]
public string Name { get; init; } = default!;
/// <inheritdoc cref="IWarehouseLocation.Code"/>
[Ordinal(3), MaxLength(32), Index(Unique = true)]
public string Code { get; init; } = default!;
/// <inheritdoc cref="IWarehouseLocation.Status"/>
[Ordinal(4)]
public Status Status { get; init; }
/// <inheritdoc cref="IWarehouseLocation.ItemCount"/>
[Ordinal(5)]
public int ItemCount { get; init; }
}

@ -0,0 +1,14 @@
using Connected.Entities.Caching;
using Connected.Logistics.Types.Warehouses;
namespace Connected.Logistics.Types.WarehouseLocations;
internal interface IWarehouseLocationCache : IEntityCacheClient<WarehouseLocation, int> { }
/// <summary>
/// Cache for the <see cref="WarehouseLocation"/> entity.
/// </summary>
internal sealed class WarehouseLocationCache : EntityCacheClient<WarehouseLocation, int>, IWarehouseLocationCache
{
public WarehouseLocationCache(IEntityCacheContext context) : base(context, Warehouse.EntityKey)
{
}
}

@ -0,0 +1,202 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Connected.Logistics.Types.WarehouseLocations;
internal sealed class WarehouseLocationOps
{
public sealed class Delete : ServiceAction<PrimaryKeyArgs<int>>
{
public Delete(IStorageProvider storage, IWarehouseLocationService locations, IEventService events, ICachingService cache)
{
Storage = storage;
Locations = locations;
Events = events;
Cache = cache;
}
private IStorageProvider Storage { get; }
private IWarehouseLocationService Locations { get; }
private IEventService Events { get; }
private ICachingService Cache { get; }
protected override async Task OnInvoke()
{
if (await Locations.Select(Arguments.Id) is not IWarehouseLocation entity)
return;
SetState(entity);
await Storage.Open<WarehouseLocation>().Update(Arguments.AsEntity<WarehouseLocation>(State.Deleted));
}
protected override async Task OnCommitted()
{
await Cache.Remove(WarehouseLocation.EntityKey, Arguments.Id);
await Events.Enqueue(this, Events, nameof(IWarehouseLocationService.Deleted), Arguments);
}
}
public sealed class Insert : ServiceFunction<InsertWarehouseLocationArgs, int>
{
public Insert(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<int> OnInvoke()
{
return (await Storage.Open<WarehouseLocation>().Update(Arguments.AsEntity<WarehouseLocation>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Events, nameof(IWarehouseLocationService.Inserted), Arguments);
}
}
public sealed class Query : ServiceFunction<QueryArgs, ImmutableList<IWarehouseLocation>>
{
public Query(IWarehouseLocationCache locations)
{
Locations = locations;
}
public IWarehouseLocationCache Locations { get; }
protected override async Task<ImmutableList<IWarehouseLocation>> OnInvoke()
{
return await (from e in Locations
select e).WithArguments(Arguments).AsEntities<IWarehouseLocation>();
}
}
public sealed class QueryByWarehouse : ServiceFunction<QueryWarehouseLocationArgs, ImmutableList<IWarehouseLocation>>
{
public QueryByWarehouse(IWarehouseLocationCache locations)
{
Locations = locations;
}
public IWarehouseLocationCache Locations { get; }
protected override async Task<ImmutableList<IWarehouseLocation>> OnInvoke()
{
return await (from e in Locations
where e.Warehouse == Arguments.Warehouse
select e).WithArguments(Arguments).AsEntities<IWarehouseLocation>();
}
}
public sealed class QueryChildren : ServiceFunction<QueryWarehouseLocationChildrenArgs, ImmutableList<IWarehouseLocation>>
{
public QueryChildren(IWarehouseLocationCache locations)
{
Locations = locations;
}
public IWarehouseLocationCache Locations { get; }
protected override async Task<ImmutableList<IWarehouseLocation>> OnInvoke()
{
return await (from e in Locations
where e.Warehouse == Arguments.Warehouse
&& (Arguments.Parent is null || e.Parent == Arguments.Parent)
select e).WithArguments(Arguments).AsEntities<IWarehouseLocation>();
}
}
public sealed class Lookup : ServiceFunction<PrimaryKeyListArgs<int>, ImmutableList<IWarehouseLocation>>
{
public Lookup(IWarehouseLocationCache locations)
{
Locations = locations;
}
public IWarehouseLocationCache Locations { get; }
protected override async Task<ImmutableList<IWarehouseLocation>> OnInvoke()
{
return await (from e in Locations
where Arguments.IdList.Any(f => f == e.Id)
select e).AsEntities<IWarehouseLocation>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<int>, IWarehouseLocation>
{
public Select(IWarehouseLocationCache locations)
{
Locations = locations;
}
private IWarehouseLocationCache Locations { get; }
protected override async Task<IWarehouseLocation?> OnInvoke()
{
return await (from e in Locations
where e.Id == Arguments.Id
select e).AsEntity();
}
}
public sealed class SelectByCode : NullableServiceFunction<SelectWarehouseLocationArgs, IWarehouseLocation>
{
public SelectByCode(IWarehouseLocationCache locations)
{
Locations = locations;
}
private IWarehouseLocationCache Locations { get; }
protected override async Task<IWarehouseLocation?> OnInvoke()
{
return await (from e in Locations
where string.Equals(e.Code, Arguments.Code, StringComparison.OrdinalIgnoreCase)
select e).AsEntity();
}
}
public sealed class Update : ServiceAction<UpdateWarehouseLocationArgs>
{
public Update(IStorageProvider storage, IWarehouseLocationCache cache, IWarehouseLocationService locations, IEventService events)
{
Storage = storage;
Cache = cache;
Locations = locations;
Events = events;
}
private IStorageProvider Storage { get; }
private IWarehouseLocationCache Cache { get; }
private IWarehouseLocationService Locations { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
if (SetState(await Locations.Select(Arguments.Id)) is not WarehouseLocation entity)
return;
await Storage.Open<WarehouseLocation>().Update(entity.Merge(Arguments, State.Default), Arguments, async () =>
{
await Cache.Refresh(Arguments.Id);
return SetState(await Locations.Select(Arguments.Id)) as WarehouseLocation;
});
}
protected override async Task OnCommitted()
{
await Cache.Refresh(Arguments.Id);
await Events.Enqueue(this, Locations, nameof(Locations.Updated), Arguments);
}
}
}

@ -0,0 +1,29 @@
using Connected.Data.DataProtection;
using Connected.Data.EntityProtection;
using Connected.Entities;
using Connected.Middleware;
using Connected.Validation;
namespace Connected.Logistics.Types.WarehouseLocations;
internal class WarehouseLocationProtection : MiddlewareComponent, IEntityProtector<IWarehouseLocation>
{
public WarehouseLocationProtection(IWarehouseLocationCache cache)
{
Cache = cache;
}
public IWarehouseLocationCache Cache { get; }
public async Task Invoke(EntityProtectionArgs<IWarehouseLocation> args)
{
if (args.State != State.Deleted)
return;
/*
* We are protecting the children because warehouse locations support nesting entities.
*/
if (args.Entity.ItemCount > 0)
throw ValidationExceptions.ReferenceExists(args.Entity.GetType(), args.Entity.Id);
await Task.CompletedTask;
}
}

@ -0,0 +1,66 @@
using System.Collections.Immutable;
using Connected.Entities;
using Connected.ServiceModel;
using Connected.Services;
using Ops = Connected.Logistics.Types.WarehouseLocations.WarehouseLocationOps;
namespace Connected.Logistics.Types.WarehouseLocations;
internal sealed class WarehouseLocationService : EntityService<int>, IWarehouseLocationService
{
public WarehouseLocationService(IContext context) : base(context)
{
}
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
public Task<int> Insert(InsertWarehouseLocationArgs args)
{
return Invoke(GetOperation<Ops.Insert>(), args);
}
public async Task Patch(PatchArgs<int> args)
{
if (await Select(args.Id) is not WarehouseLocation entity)
return;
await Update(args.Patch<UpdateWarehouseLocationArgs, WarehouseLocation>(entity));
}
public async Task<ImmutableList<IWarehouseLocation>> Query(QueryArgs? args)
{
return await Invoke(GetOperation<Ops.Query>(), args ?? QueryArgs.Default);
}
public async Task<ImmutableList<IWarehouseLocation>> Query(QueryWarehouseLocationArgs args)
{
return await Invoke(GetOperation<Ops.QueryByWarehouse>(), args);
}
public async Task<ImmutableList<IWarehouseLocation>> Query(PrimaryKeyListArgs<int> args)
{
return await Invoke(GetOperation<Ops.Lookup>(), args);
}
public async Task<ImmutableList<IWarehouseLocation>> QueryChildren(QueryWarehouseLocationChildrenArgs args)
{
return await Invoke(GetOperation<Ops.QueryChildren>(), args);
}
public async Task<IWarehouseLocation?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
public async Task<IWarehouseLocation?> Select(SelectWarehouseLocationArgs args)
{
return await Invoke(GetOperation<Ops.SelectByCode>(), args);
}
public async Task Update(UpdateWarehouseLocationArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
}

@ -0,0 +1,98 @@
using Connected.Entities;
using Connected.Logistics.Types.Warehouses;
using Connected.Middleware;
using Connected.Validation;
namespace Connected.Logistics.Types.WarehouseLocations;
internal sealed class InsertWarehouseLocationValidation : MiddlewareComponent, IValidator<InsertWarehouseLocationArgs>
{
public InsertWarehouseLocationValidation(IWarehouseLocationCache cache, IWarehouseService warehouses)
{
Cache = cache;
Warehouses = warehouses;
}
private IWarehouseLocationCache Cache { get; }
private IWarehouseService Warehouses { get; }
public async Task Validate(InsertWarehouseLocationArgs args)
{
/*
* Warehouse existence
*/
if (await Warehouses.Select(args.Warehouse) is null)
throw ValidationExceptions.NotFound(nameof(args.Warehouse), args.Warehouse);
/*
* Code is unique
*/
if (await (from e in Cache where string.Equals(e.Code, args.Code, StringComparison.OrdinalIgnoreCase) select e).AsEntity() is not null)
throw ValidationExceptions.ValueExists(nameof(args.Code), args.Code);
/*
* Check parent
*/
if (args.Parent is not null)
{
var parent = await (from e in Cache where e.Id == args.Parent select e).AsEntity();
if (parent is null)
throw ValidationExceptions.NotFound(nameof(args.Parent), args.Parent);
/*
* Parent and entity must be in the same warehouse.
*/
if (parent.Warehouse != args.Warehouse)
throw ValidationExceptions.Mismatch(nameof(args.Warehouse), args.Warehouse);
}
}
}
internal sealed class UpdateWarehouseLocationValidation : MiddlewareComponent, IValidator<UpdateWarehouseLocationArgs>
{
public UpdateWarehouseLocationValidation(IWarehouseLocationCache cache)
{
Cache = cache;
}
private IWarehouseLocationCache Cache { get; }
public async Task Validate(UpdateWarehouseLocationArgs args)
{
if (await (from e in Cache where e.Id == args.Id select e).AsEntity() is not WarehouseLocation entity)
return;
/*
* Code is unique
*/
if (await (from e in Cache where string.Equals(e.Code, args.Code, StringComparison.OrdinalIgnoreCase) && e.Id != args.Id select e).AsEntity() is not null)
throw ValidationExceptions.ValueExists(nameof(args.Code), args.Code);
/*
* Check parent
*/
if (args.Parent is not null)
{
var parent = await (from e in Cache where e.Id == args.Parent select e).AsEntity();
if (parent is null)
throw ValidationExceptions.NotFound(nameof(args.Parent), args.Parent);
/*
* Parent and entity must be in the same warehouse.
*/
if (parent.Warehouse != entity.Warehouse)
throw ValidationExceptions.Mismatch(nameof(args.Parent), args.Parent);
/*
* nesting under self
*/
if (parent.Id == args.Id)
throw ValidationExceptions.Mismatch(nameof(args.Parent), args.Parent);
var currentParent = await (from e in Cache where e.Id == parent.Parent select e).AsEntity();
while (currentParent is not null)
{
if (currentParent.Id == args.Id)
throw ValidationExceptions.Mismatch(nameof(args.Parent), args.Parent);
currentParent = await (from e in Cache where e.Id == currentParent.Parent select e).AsEntity();
}
}
}
}

@ -0,0 +1,22 @@
using Connected.Annotations;
using Connected.Data;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
namespace Connected.Logistics.Types.Warehouses;
/// <inheritdoc cref="IWarehouse"/>
[Table(Schema = Domain.Code)]
internal sealed record Warehouse : ConsistentEntity<int>, IWarehouse
{
public const string EntityKey = $"{Domain.Code}.{nameof(Warehouse)}";
/// <inheritdoc cref="IWarehouse.Name"/>
[Ordinal(0), Length(128)]
public string Name { get; init; } = default!;
/// <inheritdoc cref="IWarehouse.Code"/>
[Ordinal(1), Length(32), Index(Unique = true)]
public string Code { get; init; } = default!;
/// <inheritdoc cref="IWarehouse.Status"/>
[Ordinal(2)]
public Status Status { get; init; }
}

@ -0,0 +1,14 @@
using Connected.Entities.Caching;
namespace Connected.Logistics.Types.Warehouses;
internal interface IWarehouseCache : IEntityCacheClient<Warehouse, int> { }
/// <summary>
/// Cache for the <see cref="Warehouse"/> entity.
/// </summary>
internal sealed class WarehouseCache : EntityCacheClient<Warehouse, int>, IWarehouseCache
{
public WarehouseCache(IEntityCacheContext context) : base(context, Warehouse.EntityKey)
{
}
}

@ -0,0 +1,150 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Connected.Logistics.Types.Warehouses;
internal sealed class WarehouseOps
{
public sealed class Delete : ServiceAction<PrimaryKeyArgs<int>>
{
public Delete(IStorageProvider storage, IWarehouseService warehouses, IEventService events, ICachingService cache)
{
Storage = storage;
Warehouses = warehouses;
Events = events;
Cache = cache;
}
private IStorageProvider Storage { get; }
private IWarehouseService Warehouses { get; }
private IEventService Events { get; }
private ICachingService Cache { get; }
protected override async Task OnInvoke()
{
if (await Warehouses.Select(Arguments.Id) is not IWarehouse warehouse)
return;
SetState(warehouse);
await Storage.Open<Warehouse>().Update(Arguments.AsEntity<Warehouse>(State.Deleted));
}
protected override async Task OnCommitted()
{
await Cache.Remove(Warehouse.EntityKey, Arguments.Id);
await Events.Enqueue(this, Events, nameof(IWarehouseService.Deleted), Arguments);
}
}
public sealed class Insert : ServiceFunction<InsertWarehouseArgs, int>
{
public Insert(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<int> OnInvoke()
{
return (await Storage.Open<Warehouse>().Update(Arguments.AsEntity<Warehouse>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Events, nameof(IWarehouseService.Inserted), Arguments);
}
}
public sealed class Query : ServiceFunction<QueryArgs, ImmutableList<IWarehouse>>
{
public Query(IWarehouseCache warehouses)
{
Warehouses = warehouses;
}
public IWarehouseCache Warehouses { get; }
protected override async Task<ImmutableList<IWarehouse>> OnInvoke()
{
return await (from e in Warehouses
select e).WithArguments(Arguments).AsEntities<IWarehouse>();
}
}
public sealed class Lookup : ServiceFunction<PrimaryKeyListArgs<int>, ImmutableList<IWarehouse>>
{
public Lookup(IWarehouseCache warehouses)
{
Warehouses = warehouses;
}
public IWarehouseCache Warehouses { get; }
protected override async Task<ImmutableList<IWarehouse>> OnInvoke()
{
return await (from e in Warehouses
where Arguments.IdList.Any(f => f == e.Id)
select e).AsEntities<IWarehouse>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<int>, IWarehouse>
{
public Select(IWarehouseCache warehouses)
{
Warehouses = warehouses;
}
private IWarehouseCache Warehouses { get; }
protected override async Task<IWarehouse?> OnInvoke()
{
return await (from e in Warehouses
where e.Id == Arguments.Id
select e).AsEntity();
}
}
public sealed class Update : ServiceAction<UpdateWarehouseArgs>
{
public Update(IStorageProvider storage, IWarehouseCache cache, IWarehouseService warehouses, IEventService events)
{
Storage = storage;
Cache = cache;
Warehouses = warehouses;
Events = events;
}
private IStorageProvider Storage { get; }
private IWarehouseCache Cache { get; }
private IWarehouseService Warehouses { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
if (SetState(await Warehouses.Select(Arguments.Id)) is not Warehouse entity)
return;
await Storage.Open<Warehouse>().Update(entity.Merge(Arguments, State.Default), Arguments, async () =>
{
await Cache.Refresh(Arguments.Id);
return SetState(await Warehouses.Select(Arguments.Id)) as Warehouse;
});
}
protected override async Task OnCommitted()
{
await Cache.Refresh(Arguments.Id);
await Events.Enqueue(this, Warehouses, nameof(Warehouses.Updated), Arguments);
}
}
}

@ -0,0 +1,56 @@
using System.Collections.Immutable;
using Connected.Entities;
using Connected.ServiceModel;
using Connected.Services;
using Ops = Connected.Logistics.Types.Warehouses.WarehouseOps;
namespace Connected.Logistics.Types.Warehouses;
/// <inheritdoc cref="IWarehouseService"/>
internal sealed class WarehouseService : EntityService<int>, IWarehouseService
{
/// <summary>
/// Creates a new <see cref="WarehouseService"/> instance.
/// </summary>
/// <param name="context">The context acting as a DI scope.</param>
public WarehouseService(IContext context) : base(context)
{
}
/// <inheritdoc cref="IWarehouseService.Delete(PrimaryKeyArgs{int})"/>
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
/// <inheritdoc cref="IWarehouseService.Insert(InsertWarehouseArgs)"/>
public async Task<int> Insert(InsertWarehouseArgs args)
{
return await Invoke(GetOperation<Ops.Insert>(), args);
}
/// <inheritdoc cref="IWarehouseService.Query(QueryArgs?)"/>
public async Task<ImmutableList<IWarehouse>> Query(QueryArgs? args)
{
return await Invoke(GetOperation<Ops.Query>(), args ?? QueryArgs.Default);
}
/// <inheritdoc cref="IWarehouseService.Query(PrimaryKeyListArgs{int})"/>
public async Task<ImmutableList<IWarehouse>> Query(PrimaryKeyListArgs<int> args)
{
return await Invoke(GetOperation<Ops.Lookup>(), args);
}
/// <inheritdoc cref="IWarehouseService.Select(PrimaryKeyArgs{int})"/>
public async Task<IWarehouse?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
/// <inheritdoc cref="IWarehouseService.Update(UpdateWarehouseArgs)"/>
public async Task Update(UpdateWarehouseArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
/// <inheritdoc cref="IWarehouseService.Patch(PatchArgs{int})"/>
public async Task Patch(PatchArgs<int> args)
{
if (await Select(args.Id) is not Warehouse entity)
return;
await Update(args.Patch<UpdateWarehouseArgs, Warehouse>(entity));
}
}

@ -0,0 +1,36 @@
using Connected.Entities;
using Connected.Middleware;
using Connected.Validation;
namespace Connected.Logistics.Types.Warehouses;
internal sealed class InsertWarehouseValidator : MiddlewareComponent, IValidator<InsertWarehouseArgs>
{
public InsertWarehouseValidator(IWarehouseCache cache)
{
Cache = cache;
}
private IWarehouseCache Cache { get; }
public async Task Validate(InsertWarehouseArgs args)
{
if (await (from e in Cache where string.Equals(e.Code, args.Code, StringComparison.OrdinalIgnoreCase) select e).AsEntity() is not null)
throw ValidationExceptions.ValueExists(nameof(args.Code), args.Code);
}
}
internal sealed class UpdateWarehouseValidator : MiddlewareComponent, IValidator<UpdateWarehouseArgs>
{
public UpdateWarehouseValidator(IWarehouseCache cache)
{
Cache = cache;
}
private IWarehouseCache Cache { get; }
public async Task Validate(UpdateWarehouseArgs args)
{
if (await (from e in Cache where string.Equals(e.Code, args.Code, StringComparison.OrdinalIgnoreCase) && e.Id == args.Id select e).AsEntity() is not null)
throw ValidationExceptions.ValueExists(nameof(args.Code), args.Code);
}
}
Loading…
Cancel
Save