From 1c302bf791d4ddb6fdbb5174d83ba1f2f6525f01 Mon Sep 17 00:00:00 2001 From: Konstantin Savosteev Date: Mon, 17 Jun 2024 13:07:43 +0200 Subject: [PATCH] feat: add XCatalog module --- .editorconfig | 169 ++++++ .gitattributes | 1 + .gitignore | 339 ++++++++++++ Directory.Build.props | 12 + LICENSE | 12 + README.md | 58 ++ VirtoCommerce.XCatalog.sln | 64 +++ VirtoCommerce.XCatalog.sln.DotSettings | 3 + docs/media/.keep | 0 module.ignore | 19 + .../Binding/CatalogProductBinder.cs | 63 +++ .../Binding/CategoryBinder.cs | 29 + .../Binding/KeyBinder.cs | 15 + .../Binding/MinVariationPriceBinder.cs | 73 +++ .../Binding/VariationsBinder.cs | 20 + .../CoreAssemblyMarker.cs | 6 + .../Extensions/FacetsExtensions.cs | 69 +++ .../Extensions/IFiltertExtensions.cs | 78 +++ .../Extensions/OutlineExtensions.cs | 166 ++++++ .../Extensions/PropertyExtensions.cs | 103 ++++ .../ResolveFieldContextExtensions.cs | 29 + .../Extensions/RewardExtensions.cs | 69 +++ .../Models/Breadcrumb.cs | 16 + .../Models/ChildCategoriesQueryResponse.cs | 8 + .../Models/ExpAvailabilityData.cs | 38 ++ .../Models/ExpCategory.cs | 23 + .../Models/ExpProduct.cs | 191 +++++++ .../Models/ExpProductResponseGroup.cs | 19 + .../Models/ExpVariation.cs | 13 + .../Models/ICatalogQuery.cs | 12 + .../Models/LoadCategoryResponse.cs | 14 + .../Models/LoadProductResponse.cs | 14 + .../Models/LoadPromotionsResponse.cs | 10 + .../Models/LoadPropertiesResponse.cs | 10 + .../LoadRelatedCatalogOutlineResponse.cs | 7 + .../Models/LoadRelatedSlugPathResponse.cs | 7 + .../Models/ProductSuggestionsQueryResponse.cs | 8 + .../Models/SearchCategoryResponse.cs | 15 + .../SearchProductAssociationsResponse.cs | 9 + .../Models/SearchProductResponse.cs | 21 + .../Models/SearchPropertiesResponse.cs | 9 + .../SearchPropertyDictionaryItemResponse.cs | 9 + .../Models/SearchVideoQueryResponse.cs | 9 + .../ModuleConstants.cs | 7 + .../Queries/CatalogQueryBase.cs | 38 ++ .../Queries/ChildCategoriesQuery.cs | 38 ++ .../Queries/GetFulfillmentCenterQuery.cs | 12 + .../Queries/LoadCategoryQuery.cs | 9 + .../Queries/LoadProductsQuery.cs | 11 + .../Queries/LoadPromotionsQuery.cs | 11 + .../Queries/LoadPropertiesQuery.cs | 11 + .../Queries/LoadRelatedCatalogOutlineQuery.cs | 11 + .../Queries/LoadRelatedSlugPathQuery.cs | 11 + .../Queries/ProductSuggestionsQuery.cs | 28 + .../Queries/SearchCategoryQuery.cs | 66 +++ .../Queries/SearchFulfillmentCentersQuery.cs | 17 + .../Queries/SearchProductAssociationsQuery.cs | 14 + .../Queries/SearchProductQuery.cs | 159 ++++++ .../Queries/SearchPropertiesQuery.cs | 14 + .../SearchPropertyDictionaryItemQuery.cs | 13 + .../Queries/SearchVideoQuery.cs | 14 + .../Schemas/AssetType.cs | 25 + .../Schemas/AvailabilityDataType.cs | 37 ++ .../Schemas/BreadcrumbsType.cs | 18 + .../Schemas/CatalogDiscountType.cs | 40 ++ .../Schemas/CategoryDescriptionType.cs | 16 + .../Schemas/CategoryType.cs | 201 +++++++ .../ChildCategoriesQueryResponseType.cs | 15 + .../Schemas/DescriptionType.cs | 16 + .../Schemas/FulfillmentCenterAddressType.cs | 34 ++ .../Schemas/FulfillmentCenterType.cs | 35 ++ .../Schemas/ImageType.cs | 51 ++ .../Schemas/InventoryInfoType.cs | 34 ++ .../Schemas/OutlineItemType.cs | 19 + .../Schemas/OutlineType.cs | 15 + .../Schemas/PriceType.cs | 62 +++ .../Schemas/ProductAssociationType.cs | 55 ++ .../ProductSuggestionsQueryResponseType.cs | 15 + .../Schemas/ProductType.cs | 401 ++++++++++++++ .../Schemas/ProductsConnectonType.cs | 38 ++ .../Schemas/PromotionType.cs | 19 + .../Schemas/PropertyDictionaryItemType.cs | 26 + .../Schemas/PropertyType.cs | 131 +++++ .../Schemas/PropertyTypeEnum.cs | 13 + .../Schemas/PropertyValueTypeEnum.cs | 14 + .../ScalarTypes/PropertyValueGraphType.cs | 27 + .../Schemas/TierPriceType.cs | 22 + .../Schemas/VariationType.cs | 107 ++++ .../Schemas/VideoType.cs | 23 + .../CatalogProductIsAvailableSpecification.cs | 25 + .../CatalogProductIsBuyableSpecification.cs | 35 ++ .../CatalogProductIsInStockSpecification.cs | 25 + .../VirtoCommerce.XCatalog.Core.csproj | 20 + .../DataAssemblyMarker.cs | 6 + .../Extensions/ServiceCollectionExtensions.cs | 47 ++ .../Index/FilterSyntaxMapper.cs | 147 ++++++ .../Index/IndexFieldsMapper.cs | 114 ++++ .../Index/IndexSearchRequestBuilder.cs | 436 +++++++++++++++ .../Mapping/CategoryMappingProfile.cs | 15 + .../Mapping/FacetMappingProfile.cs | 62 +++ .../Mapping/ProductMappingProfile.cs | 143 +++++ .../Mapping/PropertyMappingProfile.cs | 28 + .../EnsureCatalogProductLoadedMiddleware.cs | 47 ++ .../EnsureCategoryLoadedMiddleware.cs | 47 ++ .../EnsurePropertyMetadataLoadedMiddleware.cs | 69 +++ .../EvalProductsDiscountsMiddleware.cs | 84 +++ .../EvalProductsInventoryMiddleware.cs | 86 +++ .../EvalProductsPricesMiddleware.cs | 108 ++++ .../Middlewares/EvalProductsTaxMiddleware.cs | 82 +++ .../EvalProductsVendorMiddleware.cs | 133 +++++ .../EvalSearchRequestUserGroupsMiddleware.cs | 82 +++ .../RemoveNullCatalogProductsMiddleware.cs | 27 + .../Queries/CatalogQueryBuilder.cs | 56 ++ .../Queries/ChildCategoriesQueryBuilder.cs | 76 +++ .../Queries/ChildCategoriesQueryHandler.cs | 136 +++++ .../GetFulfillmentCenterQueryHandler.cs | 27 + .../Queries/LoadPromotionsQueryHandler.cs | 34 ++ .../Queries/LoadPropertiesQueryHandler.cs | 31 ++ .../LoadRelatedCatalogOutlineQueryHandler.cs | 35 ++ .../LoadRelatedSlugPathQueryHandler.cs | 38 ++ .../Queries/ProductSuggestionsQueryBuilder.cs | 18 + .../Queries/ProductSuggestionsQueryHandler.cs | 53 ++ .../Queries/SearchCategoryQueryHandler.cs | 173 ++++++ .../SearchFulfillmentCentersQueryHandler.cs | 61 +++ .../SearchProductAssociationsQueryHandler.cs | 33 ++ .../Queries/SearchProductQueryBuilder.cs | 63 +++ .../Queries/SearchProductQueryHandler.cs | 194 +++++++ .../Queries/SearchPropertiesQueryHandler.cs | 48 ++ ...earchPropertyDictionaryItemQueryHandler.cs | 33 ++ .../Queries/SearchVideoQueryHandler.cs | 40 ++ .../Schemas/DigitalCatalogSchema.cs | 257 +++++++++ .../Schemas/InventorySchema.cs | 102 ++++ .../Services/PropertySearchCriteriaBuilder.cs | 63 +++ .../VirtoCommerce.XCatalog.Data.csproj | 18 + .../Content/logo.png | Bin 0 -> 14153 bytes .../Localizations/en.XCatalog.json | 8 + src/VirtoCommerce.XCatalog.Web/Module.cs | 30 ++ .../VirtoCommerce.XCatalog.Web.csproj | 13 + .../module.manifest | 37 ++ .../GetBreadcrumbsFromOutLineTests.cs | 120 +++++ .../Extensions/IFilterExtensionTests.cs | 183 +++++++ .../Helpers/BaseMoqHelper.cs | 122 +++++ .../Helpers/XCatalogMoqHelper.cs | 57 ++ .../Index/IndexSearchRequestBuilderTests.cs | 495 ++++++++++++++++++ .../Mappers/IndexFieldsMapperTests.cs | 27 + .../Mappers/MappingTermFilterTests.cs | 43 ++ .../EvalProductsTaxMiddlewareTest.cs | 210 ++++++++ .../Schemas/ProductTypeTests.cs | 202 +++++++ .../Schemas/PropertyTypeTests.cs | 92 ++++ .../PropertyValueGraphTypeTests.cs | 42 ++ ...logProductIsAvailableSpecificationTests.cs | 44 ++ ...talogProductIsBuyableSpecificationTests.cs | 41 ++ .../VirtoCommerce.XCatalog.Tests.csproj | 21 + 153 files changed, 9096 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Directory.Build.props create mode 100644 LICENSE create mode 100644 README.md create mode 100644 VirtoCommerce.XCatalog.sln create mode 100644 VirtoCommerce.XCatalog.sln.DotSettings create mode 100644 docs/media/.keep create mode 100644 module.ignore create mode 100644 src/VirtoCommerce.XCatalog.Core/Binding/CatalogProductBinder.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Binding/CategoryBinder.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Binding/KeyBinder.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Binding/MinVariationPriceBinder.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Binding/VariationsBinder.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/CoreAssemblyMarker.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Extensions/FacetsExtensions.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Extensions/IFiltertExtensions.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Extensions/OutlineExtensions.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Extensions/PropertyExtensions.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Extensions/ResolveFieldContextExtensions.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Extensions/RewardExtensions.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/Breadcrumb.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/ChildCategoriesQueryResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/ExpAvailabilityData.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/ExpCategory.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/ExpProduct.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/ExpProductResponseGroup.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/ExpVariation.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/ICatalogQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/LoadCategoryResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/LoadProductResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/LoadPromotionsResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/LoadPropertiesResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/LoadRelatedCatalogOutlineResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/LoadRelatedSlugPathResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/ProductSuggestionsQueryResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/SearchCategoryResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/SearchProductAssociationsResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/SearchProductResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/SearchPropertiesResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/SearchPropertyDictionaryItemResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Models/SearchVideoQueryResponse.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/ModuleConstants.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/CatalogQueryBase.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/ChildCategoriesQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/GetFulfillmentCenterQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/LoadCategoryQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/LoadProductsQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/LoadPromotionsQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/LoadPropertiesQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/LoadRelatedCatalogOutlineQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/LoadRelatedSlugPathQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/ProductSuggestionsQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/SearchCategoryQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/SearchFulfillmentCentersQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/SearchProductAssociationsQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/SearchProductQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/SearchPropertiesQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/SearchPropertyDictionaryItemQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Queries/SearchVideoQuery.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/AssetType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/AvailabilityDataType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/BreadcrumbsType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/CatalogDiscountType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/CategoryDescriptionType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/CategoryType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/ChildCategoriesQueryResponseType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/DescriptionType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/FulfillmentCenterAddressType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/FulfillmentCenterType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/ImageType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/InventoryInfoType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/OutlineItemType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/OutlineType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/PriceType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/ProductAssociationType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/ProductSuggestionsQueryResponseType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/ProductType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/ProductsConnectonType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/PromotionType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/PropertyDictionaryItemType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/PropertyType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/PropertyTypeEnum.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/PropertyValueTypeEnum.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/ScalarTypes/PropertyValueGraphType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/TierPriceType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/VariationType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Schemas/VideoType.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Specifications/CatalogProductIsAvailableSpecification.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Specifications/CatalogProductIsBuyableSpecification.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/Specifications/CatalogProductIsInStockSpecification.cs create mode 100644 src/VirtoCommerce.XCatalog.Core/VirtoCommerce.XCatalog.Core.csproj create mode 100644 src/VirtoCommerce.XCatalog.Data/DataAssemblyMarker.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Index/FilterSyntaxMapper.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Index/IndexFieldsMapper.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Index/IndexSearchRequestBuilder.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Mapping/CategoryMappingProfile.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Mapping/FacetMappingProfile.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Mapping/ProductMappingProfile.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Mapping/PropertyMappingProfile.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Middlewares/EnsureCatalogProductLoadedMiddleware.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Middlewares/EnsureCategoryLoadedMiddleware.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Middlewares/EnsurePropertyMetadataLoadedMiddleware.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Middlewares/EvalProductsDiscountsMiddleware.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Middlewares/EvalProductsInventoryMiddleware.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Middlewares/EvalProductsPricesMiddleware.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Middlewares/EvalProductsTaxMiddleware.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Middlewares/EvalProductsVendorMiddleware.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Middlewares/EvalSearchRequestUserGroupsMiddleware.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Middlewares/RemoveNullCatalogProductsMiddleware.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/CatalogQueryBuilder.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/ChildCategoriesQueryBuilder.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/ChildCategoriesQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/GetFulfillmentCenterQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/LoadPromotionsQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/LoadPropertiesQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/LoadRelatedCatalogOutlineQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/LoadRelatedSlugPathQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/ProductSuggestionsQueryBuilder.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/ProductSuggestionsQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/SearchCategoryQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/SearchFulfillmentCentersQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/SearchProductAssociationsQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/SearchProductQueryBuilder.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/SearchProductQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/SearchPropertiesQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/SearchPropertyDictionaryItemQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Queries/SearchVideoQueryHandler.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Schemas/DigitalCatalogSchema.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Schemas/InventorySchema.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/Services/PropertySearchCriteriaBuilder.cs create mode 100644 src/VirtoCommerce.XCatalog.Data/VirtoCommerce.XCatalog.Data.csproj create mode 100644 src/VirtoCommerce.XCatalog.Web/Content/logo.png create mode 100644 src/VirtoCommerce.XCatalog.Web/Localizations/en.XCatalog.json create mode 100644 src/VirtoCommerce.XCatalog.Web/Module.cs create mode 100644 src/VirtoCommerce.XCatalog.Web/VirtoCommerce.XCatalog.Web.csproj create mode 100644 src/VirtoCommerce.XCatalog.Web/module.manifest create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Extensions/GetBreadcrumbsFromOutLineTests.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Extensions/IFilterExtensionTests.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Helpers/BaseMoqHelper.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Helpers/XCatalogMoqHelper.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Index/IndexSearchRequestBuilderTests.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Mappers/IndexFieldsMapperTests.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Mappers/MappingTermFilterTests.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Middlewares/EvalProductsTaxMiddlewareTest.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Schemas/ProductTypeTests.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Schemas/PropertyTypeTests.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Schemas/ScalarTypes/PropertyValueGraphTypeTests.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Specifications/CatalogProductIsAvailableSpecificationTests.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/Specifications/CatalogProductIsBuyableSpecificationTests.cs create mode 100644 tests/VirtoCommerce.XCatalog.Tests/VirtoCommerce.XCatalog.Tests.csproj diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..52a5b14 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,169 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = crlf +trim_trailing_whitespace = true +insert_final_newline = true + +# Project files +[*.{csproj,props}] +insert_final_newline = false + +# HTML files +[*.{html,htm}] +insert_final_newline = false + +# Code +[*.{cs,js,ts,ps1,sh,bat,cmd}] +indent_size = 4 + +# Dotnet code style settings +[*.{cs,vb}] + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true + +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Use explicit accessibility modifiers +dotnet_style_require_accessibility_modifiers = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_prefer_inferred_tuple_names = true:suggestion +dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion + +# CSharp code style settings +[*.cs] + +# Prefer curly braces even for one line of code +csharp_prefer_braces = true:suggestion + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_parentheses = false + +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent + +[*.{cs,vb}] +#### 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.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 + +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.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_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea99cfe --- /dev/null +++ b/.gitignore @@ -0,0 +1,339 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Virto +*.nupkg +*.zip +.nuke +dist/ +**/Properties/launchSettings.json diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..4834e4f --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,12 @@ + + + + + 3.800.0 + + $(VersionSuffix)-$(BuildNumber) + + + true + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3caa727 --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +Copyright (c) Virto Solutions LTD.  All rights reserved. + +Licensed under the Virto Commerce Open Software License (the "License"); you +may not use this file except in compliance with the License. You may +obtain a copy of the License at + +https://virtocommerce.com/open-source-license + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e08c61a --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# XCatalog + +## Overview + +Short overview of what the new module is. + +- What is the new or updated experience? + +- Does this module replace an existing module/experience? If yes, what is the transition plan? + +- Does this module has dependency on other ? If yes, list/explain the dependencies. + +- List the key deployment scenarios - why would people use this module? + +## Functional Requirements + +Short description of the new module functional requirements. + +## Scenarios + +List of scenarios that the new module implements + +1. [Scenario 1](/doc/scenario-name1.md) +1. [Scenario 2](/doc/scenario-name2.md) +1. [Scenario 3](/doc/scenario-name3.md) + 1. [Scenario 3.1](/doc/scenario-name31.md) + 1. [Scenario 3.2](/doc/scenario-name32.md) +1. [Scenario 4](/doc/scenario-name4.md) + +## Web API + +Web API documentation for each module is built out automatically and can be accessed by following the link bellow: + + +## Database Model + +![DB model](./docs/media/diagram-db-model.png) + +## Related topics + +[Some Article1](some-article1.md) + +[Some Article2](some-article2.md) + +## License + +Copyright (c) Virto Solutions LTD. All rights reserved. + +Licensed under the Virto Commerce Open Software License (the "License"); you +may not use this file except in compliance with the License. You may +obtain a copy of the License at + + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. diff --git a/VirtoCommerce.XCatalog.sln b/VirtoCommerce.XCatalog.sln new file mode 100644 index 0000000..d87c7b0 --- /dev/null +++ b/VirtoCommerce.XCatalog.sln @@ -0,0 +1,64 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32630.192 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0E35805D-33EB-4F07-BE87-9A39DADDD044}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtoCommerce.XCatalog.Web", "src\VirtoCommerce.XCatalog.Web\VirtoCommerce.XCatalog.Web.csproj", "{DBCA2B60-104C-4FDA-8471-97A3B2003F06}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtoCommerce.XCatalog.Data", "src\VirtoCommerce.XCatalog.Data\VirtoCommerce.XCatalog.Data.csproj", "{EE8C684B-B16E-45C3-8AF5-F9466EC3D0E0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtoCommerce.XCatalog.Core", "src\VirtoCommerce.XCatalog.Core\VirtoCommerce.XCatalog.Core.csproj", "{73AF2F73-02A8-4D4D-9FCD-1C944CFB0848}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{58249D0A-2D27-4CCF-B173-E401846FA5D4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtoCommerce.XCatalog.Tests", "tests\VirtoCommerce.XCatalog.Tests\VirtoCommerce.XCatalog.Tests.csproj", "{10B2410F-EA36-4749-8AC1-78722513847E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{681FC933-B87E-457E-80C9-E3EB12B19B51}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + Directory.Build.props = Directory.Build.props + LICENSE = LICENSE + module.ignore = module.ignore + README.md = README.md + VirtoCommerce.XCatalog.sln.DotSettings = VirtoCommerce.XCatalog.sln.DotSettings + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DBCA2B60-104C-4FDA-8471-97A3B2003F06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBCA2B60-104C-4FDA-8471-97A3B2003F06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBCA2B60-104C-4FDA-8471-97A3B2003F06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBCA2B60-104C-4FDA-8471-97A3B2003F06}.Release|Any CPU.Build.0 = Release|Any CPU + {EE8C684B-B16E-45C3-8AF5-F9466EC3D0E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE8C684B-B16E-45C3-8AF5-F9466EC3D0E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE8C684B-B16E-45C3-8AF5-F9466EC3D0E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE8C684B-B16E-45C3-8AF5-F9466EC3D0E0}.Release|Any CPU.Build.0 = Release|Any CPU + {73AF2F73-02A8-4D4D-9FCD-1C944CFB0848}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73AF2F73-02A8-4D4D-9FCD-1C944CFB0848}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73AF2F73-02A8-4D4D-9FCD-1C944CFB0848}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73AF2F73-02A8-4D4D-9FCD-1C944CFB0848}.Release|Any CPU.Build.0 = Release|Any CPU + {10B2410F-EA36-4749-8AC1-78722513847E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10B2410F-EA36-4749-8AC1-78722513847E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10B2410F-EA36-4749-8AC1-78722513847E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10B2410F-EA36-4749-8AC1-78722513847E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DBCA2B60-104C-4FDA-8471-97A3B2003F06} = {0E35805D-33EB-4F07-BE87-9A39DADDD044} + {EE8C684B-B16E-45C3-8AF5-F9466EC3D0E0} = {0E35805D-33EB-4F07-BE87-9A39DADDD044} + {73AF2F73-02A8-4D4D-9FCD-1C944CFB0848} = {0E35805D-33EB-4F07-BE87-9A39DADDD044} + {10B2410F-EA36-4749-8AC1-78722513847E} = {58249D0A-2D27-4CCF-B173-E401846FA5D4} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {34CF91FB-2C99-4391-BB39-C484319B399A} + EndGlobalSection +EndGlobal diff --git a/VirtoCommerce.XCatalog.sln.DotSettings b/VirtoCommerce.XCatalog.sln.DotSettings new file mode 100644 index 0000000..deaa0df --- /dev/null +++ b/VirtoCommerce.XCatalog.sln.DotSettings @@ -0,0 +1,3 @@ + + True + diff --git a/docs/media/.keep b/docs/media/.keep new file mode 100644 index 0000000..e69de29 diff --git a/module.ignore b/module.ignore new file mode 100644 index 0000000..5387e02 --- /dev/null +++ b/module.ignore @@ -0,0 +1,19 @@ +AutoMapper.dll +AutoMapper.Extensions.Microsoft.DependencyInjection.dll +FluentAssertions.dll +GraphQL.dll +GraphQL.Authorization.dll +GraphQL.DataLoader.dll +GraphQL.NewtonsoftJson.dll +GraphQL.Relay.dll +GraphQL.Server.Core.dll +GraphQL.Server.Transports.AspNetCore.dll +GraphQL.Server.Transports.AspNetCore.NewtonsoftJson.dll +GraphQL-Parser.dll +MediatR.dll +MediatR.Extensions.Microsoft.DependencyInjection.dll +Microsoft.Data.SqlClient.SNI.dll +Nager.Country.dll +PipelineNet.dll +TimeZoneConverter.dll +VirtoCommerce.Tools.dll diff --git a/src/VirtoCommerce.XCatalog.Core/Binding/CatalogProductBinder.cs b/src/VirtoCommerce.XCatalog.Core/Binding/CatalogProductBinder.cs new file mode 100644 index 0000000..f519f74 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Binding/CatalogProductBinder.cs @@ -0,0 +1,63 @@ +using System; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.SearchModule.Core.Model; +using VirtoCommerce.Xapi.Core.Binding; + +namespace VirtoCommerce.XCatalog.Core.Binding +{ + public class CatalogProductBinder : IIndexModelBinder + { + private static readonly Type _productType = AbstractTypeFactory.TryCreateInstance().GetType(); + + public BindingInfo BindingInfo { get; set; } = new BindingInfo { FieldName = "__object" }; + + public virtual object BindModel(SearchDocument searchDocument) + { + var result = default(CatalogProduct); + + if (!searchDocument.ContainsKey(BindingInfo.FieldName)) + { + // No object in index + return result; + } + + var obj = searchDocument[BindingInfo.FieldName]; + + // check if __object document field name contains string or jObject + if (obj is string sObj) + { + try + { + obj = JObject.Parse(sObj); + } + catch (JsonReaderException) + { + return result; + } + } + + if (obj is JObject jobj) + { + result = (CatalogProduct)jobj.ToObject(_productType); + + var productProperties = result.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var property in productProperties) + { + var binder = property.GetIndexModelBinder(); + + if (binder != null) + { + property.SetValue(result, binder.BindModel(searchDocument)); + } + } + } + + return result; + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Binding/CategoryBinder.cs b/src/VirtoCommerce.XCatalog.Core/Binding/CategoryBinder.cs new file mode 100644 index 0000000..1a8cbca --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Binding/CategoryBinder.cs @@ -0,0 +1,29 @@ +using System; +using Newtonsoft.Json.Linq; +using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.Xapi.Core.Binding; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.SearchModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Binding +{ + public class CategoryBinder : IIndexModelBinder + { + private static readonly Type _productType = AbstractTypeFactory.TryCreateInstance().GetType(); + + public BindingInfo BindingInfo { get; set; } = new BindingInfo { FieldName = "__object" }; + + public virtual object BindModel(SearchDocument searchDocument) + { + var fieldName = BindingInfo.FieldName; + + if (searchDocument.ContainsKey(BindingInfo.FieldName) && searchDocument[fieldName] is JObject jobj) + { + return (Category)jobj.ToObject(_productType); + } + + // No object in index + return null; + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Binding/KeyBinder.cs b/src/VirtoCommerce.XCatalog.Core/Binding/KeyBinder.cs new file mode 100644 index 0000000..c34f935 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Binding/KeyBinder.cs @@ -0,0 +1,15 @@ +using VirtoCommerce.Xapi.Core.Binding; +using VirtoCommerce.SearchModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Binding +{ + public class KeyBinder : IIndexModelBinder + { + public BindingInfo BindingInfo { get; set; } + + public virtual object BindModel(SearchDocument searchDocument) + { + return searchDocument.Id; + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Binding/MinVariationPriceBinder.cs b/src/VirtoCommerce.XCatalog.Core/Binding/MinVariationPriceBinder.cs new file mode 100644 index 0000000..680b29a --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Binding/MinVariationPriceBinder.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using VirtoCommerce.Xapi.Core.Binding; +using VirtoCommerce.PricingModule.Core.Model; +using VirtoCommerce.SearchModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Binding +{ + public class MinVariationPriceBinder : IIndexModelBinder + { + public BindingInfo BindingInfo { get; set; } = new BindingInfo { FieldName = "__minvariationprice" }; + + public object BindModel(SearchDocument searchDocument) + { + var result = new List(); + + if (!searchDocument.ContainsKey(BindingInfo.FieldName)) + { + return result; + } + + var pricesDocumentRecord = searchDocument[BindingInfo.FieldName]; + switch (pricesDocumentRecord) + { + case Array jArray: + { + var jObjects = new List(); + foreach (var sObj in jArray.OfType()) + { + try + { + var jObj = JObject.Parse(sObj); + jObjects.Add(jObj); + } + catch (JsonReaderException) + { + // Intentionally left empty + } + } + + jObjects = jObjects.Any() ? jObjects : jArray.OfType().ToList(); + foreach (var jObject in jObjects) + { + AddPrice(result, jObject); + } + + break; + } + + case JObject jObject: + { + AddPrice(result, jObject); + break; + } + } + + return result; + } + + private static void AddPrice(List result, JObject jObject) + { + var indexedPrice = jObject.ToObject(); + result.Add(new Price + { + Currency = indexedPrice.Currency, + List = indexedPrice.Value, + }); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Binding/VariationsBinder.cs b/src/VirtoCommerce.XCatalog.Core/Binding/VariationsBinder.cs new file mode 100644 index 0000000..1a022c3 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Binding/VariationsBinder.cs @@ -0,0 +1,20 @@ +using System.Linq; +using VirtoCommerce.Xapi.Core.Binding; +using VirtoCommerce.SearchModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Binding +{ + public class VariationsBinder : IIndexModelBinder + { + public BindingInfo BindingInfo { get; set; } = new BindingInfo { FieldName = "__variations" }; + + public virtual object BindModel(SearchDocument searchDocument) + { + var fieldName = BindingInfo.FieldName; + + return searchDocument.ContainsKey(fieldName) && searchDocument[fieldName] is object[] objs + ? objs.Select(x => (string)x).ToList() + : Enumerable.Empty().ToList(); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/CoreAssemblyMarker.cs b/src/VirtoCommerce.XCatalog.Core/CoreAssemblyMarker.cs new file mode 100644 index 0000000..156f6d4 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/CoreAssemblyMarker.cs @@ -0,0 +1,6 @@ +namespace VirtoCommerce.XCatalog.Core +{ + public class CoreAssemblyMarker + { + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Extensions/FacetsExtensions.cs b/src/VirtoCommerce.XCatalog.Core/Extensions/FacetsExtensions.cs new file mode 100644 index 0000000..279253e --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Extensions/FacetsExtensions.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using VirtoCommerce.CatalogModule.Core.Model.Search; + +namespace VirtoCommerce.XCatalog.Core.Extensions +{ + public static class FacetsExtensions + { + /// + /// Add to the facet phrase language-specific facet name in a hope the sought facet can be made by non-dictionary, multivalue and multilanguage property. + /// See details: PT-3517 + /// + /// + /// + /// + public static string AddLanguageSpecificFacets(this string requestFacets, string cultureName) + { + if (string.IsNullOrEmpty(requestFacets) || string.IsNullOrEmpty(cultureName)) + { + return requestFacets; + } + + var resultBuilder = new StringBuilder(); + var facets = requestFacets.Split(' '); + foreach (var facet in facets) + { + if (facet.StartsWith("__")) + { + resultBuilder.Append(' '); + resultBuilder.Append(facet); + } + else + { + resultBuilder.Append(' '); + resultBuilder.Append(facet); + resultBuilder.Append(' '); + resultBuilder.Append(facet); + resultBuilder.Append('_'); + resultBuilder.Append(cultureName.ToLowerInvariant()); + } + } + + return resultBuilder.ToString().Trim(); + } + + /// + /// Apply language-specific facet result + /// See details: PT-3517 + /// + /// + /// + /// + public static IEnumerable ApplyLanguageSpecificFacetResult(this Aggregation[] aggregations, string languageCode) + { + return aggregations?.Select(x => + { + // Apply language-specific facet result + // To do this, copy facet items from the fake language-specific facet to the real facet + var languageSpecificAggregation = aggregations.FirstOrDefault(y => y.Field == $"{x.Field}_{languageCode.ToLowerInvariant()}"); + if (languageSpecificAggregation != null) + x.Items = languageSpecificAggregation.Items; + return x; + }) + .Where(x => !Regex.IsMatch(x.Field, @"_\w\w-\w\w$", RegexOptions.IgnoreCase)); // Drop fake language-specific facets from results + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Extensions/IFiltertExtensions.cs b/src/VirtoCommerce.XCatalog.Core/Extensions/IFiltertExtensions.cs new file mode 100644 index 0000000..1270314 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Extensions/IFiltertExtensions.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using VirtoCommerce.CatalogModule.Core.Model.Search; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.SearchModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Extensions +{ + public static class IFiltertExtensions + { + /// + /// Checks aggregation item values for the equality with the filters, and set to those, whose value equal to on of the filters + /// + /// Search request + /// Calculated aggregation results + public static void SetAppliedAggregations(this SearchRequest searchRequest, Aggregation[] aggregations) + { + if (searchRequest == null) + { + throw new ArgumentNullException(nameof(searchRequest)); + } + if (aggregations == null) + { + throw new ArgumentNullException(nameof(aggregations)); + } + + foreach (var childFilter in searchRequest.GetChildFilters()) + { + var aggregationItems = aggregations.Where(x => x.Field.EqualsInvariant(childFilter.GetFieldName())) + .SelectMany(x => x.Items) + .ToArray(); + + childFilter.FillIsAppliedForItems(aggregationItems); + } + } + + public static IList GetChildFilters(this SearchRequest searchRequest) => + (searchRequest?.Filter as AndFilter)?.ChildFilters ?? Array.Empty(); + + public static string GetFieldName(this IFilter filter) + { + // TermFilter names are equal, RangeFilter can contain underscore in the name + var fieldName = (filter as INamedFilter)?.FieldName; + if (string.IsNullOrEmpty(fieldName)) + return fieldName; + + if (fieldName.StartsWith("__")) + return fieldName; + + if (filter is RangeFilter) + return fieldName.Split('_')[0]; + + return fieldName; + } + + public static void FillIsAppliedForItems(this IFilter filter, IEnumerable aggregationItems) + { + foreach (var aggregationItem in aggregationItems) + { + switch (filter) + { + case TermFilter termFilter: + // For term filters: just check result value in filter values + aggregationItem.IsApplied = termFilter.Values.Any(x => x.EqualsInvariant(aggregationItem.Value?.ToString())); + break; + case RangeFilter rangeFilter: + // For range filters check the values have the same bounds + aggregationItem.IsApplied = rangeFilter.Values.Any(x => + x.Lower.EqualsInvariant(aggregationItem.RequestedLowerBound) && x.Upper.EqualsInvariant(aggregationItem.RequestedUpperBound)); + break; + default: + break; + } + } + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Extensions/OutlineExtensions.cs b/src/VirtoCommerce.XCatalog.Core/Extensions/OutlineExtensions.cs new file mode 100644 index 0000000..8c6943e --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Extensions/OutlineExtensions.cs @@ -0,0 +1,166 @@ +using System.Collections.Generic; +using System.Linq; +using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.CoreModule.Core.Outlines; +using VirtoCommerce.CoreModule.Core.Seo; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.StoreModule.Core.Model; +using VirtoCommerce.Tools; +using VirtoCommerce.XCatalog.Core.Extensions; +using VirtoCommerce.XCatalog.Core.Models; +using SeoStoreSetting = VirtoCommerce.StoreModule.Core.ModuleConstants.Settings.SEO; +using toolsDto = VirtoCommerce.Tools.Models; + +namespace VirtoCommerce.XCatalog.Core.Extensions +{ + public static class OutlineExtensions + { + /// + /// Returns SEO path if all outline items of the first outline have SEO keywords, otherwise returns default value. + /// Path: GrandParentCategory/ParentCategory/ProductCategory/Product + /// + /// + /// + /// + /// + /// + public static string GetSeoPath(this IEnumerable outlines, Store store, string language, string defaultValue) + { + string result = null; + + var toolsStore = new toolsDto.Store + { + Id = store.Id, + Url = store.Url, + SecureUrl = store.SecureUrl, + Catalog = store.Catalog, + DefaultLanguage = store.DefaultLanguage, + SeoLinksType = EnumUtility.SafeParse(store.Settings.GetValue(SeoStoreSetting.SeoLinksType), toolsDto.SeoLinksType.Collapsed), + Languages = store.Languages?.ToList(), + }; + var toolsOutlines = outlines?.Select(o => o.JsonConvert()).ToArray(); + if (toolsOutlines != null) + { + result = toolsOutlines.GetSeoPath(toolsStore, language ?? store.DefaultLanguage, defaultValue); + } + return result; + } + + /// + /// Returns best matching outline path for the given catalog: CategoryId/CategoryId2. + /// + /// + /// + /// + public static string GetOutlinePath(this IEnumerable outlines, string catalogId) + { + return outlines?.Select(o => o.JsonConvert()).GetOutlinePath(catalogId); + } + + /// + /// Returns product's category outline. + /// + /// + /// + public static string GetCategoryOutline(this CatalogProduct product) + { + var result = string.Empty; + + if (product != null && !string.IsNullOrEmpty(product.Outline)) + { + var i = product.Outline.LastIndexOf('/'); + if (i >= 0) + { + result = product.Outline.Substring(0, i); + } + } + + return result; + } + + /// + /// Returns all concatinated relative outlines for the given catalog + /// + /// + /// + /// + public static string GetOutlinePaths(this IEnumerable outlines, string catalogId) + { + var result = string.Empty; + var catalogOutlines = outlines?.Where(o => o.Items.Any(i => i.SeoObjectType == "Catalog" && i.Id == catalogId)); + var outlinesList = catalogOutlines?.Where(x => x != null).Select(x => x.ToCatalogRelativePath()).ToList(); + + if (!outlinesList.IsNullOrEmpty()) + { + result = string.Join(";", outlinesList); + } + + return result; + } + + /// s + /// Returns catalog's relative outline path + /// + /// + /// + public static string ToCatalogRelativePath(this Outline outline) + { + return outline.Items == null ? null : string.Join("/", + outline.Items + .Where(x => x != null && x.SeoObjectType != "Catalog") + .Select(x => x.Id) + ); + } + + public static IEnumerable GetBreadcrumbsFromOutLine(this IEnumerable outlines, Store store, string cultureName) + { + var outlineItems = outlines + ?.FirstOrDefault(outline => outline.Items != null && outline.Items.Any(item => item.Id == store.Catalog && item.SeoObjectType == "Catalog")) + ?.Items + .ToList(); + + if (outlineItems.IsNullOrEmpty()) + { + return Enumerable.Empty(); + } + + var breadcrumbs = new List(); + +#pragma warning disable S2259 // False positive by IsNullOrEmpty + for (var i = outlineItems.Count - 1; i > 0; i--) + { + var item = outlineItems[i]; + + var innerOutline = new List { new Outline { Items = outlineItems } }; + var seoPath = innerOutline.GetSeoPath(store, cultureName, null); + + outlineItems.Remove(item); + if (string.IsNullOrWhiteSpace(seoPath)) + { + continue; + } + + var seoInfoForStoreAndLanguage = SeoInfoForStoreAndLanguage(item, store.Id, cultureName); + + var breadcrumb = new Breadcrumb(item.SeoObjectType) + { + ItemId = item.Id, + Title = seoInfoForStoreAndLanguage?.PageTitle?.EmptyToNull() ?? item.Name, + SemanticUrl = seoInfoForStoreAndLanguage?.SemanticUrl, + SeoPath = seoPath + }; + breadcrumbs.Insert(0, breadcrumb); + } +#pragma warning restore S2259 // Null pointers should not be dereferenced + + return breadcrumbs; + } + + public static SeoInfo SeoInfoForStoreAndLanguage(OutlineItem item, string storeId, string cultureName) + { + return item.SeoInfos?.FirstOrDefault(x => x.StoreId == storeId && x.LanguageCode == cultureName); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Extensions/PropertyExtensions.cs b/src/VirtoCommerce.XCatalog.Core/Extensions/PropertyExtensions.cs new file mode 100644 index 0000000..4b3115a --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Extensions/PropertyExtensions.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.Platform.Core.Common; + +namespace VirtoCommerce.XCatalog.Core.Extensions +{ + public static class PropertyExtensions + { + /// + /// Flattens the tree-like structure of Property-PropertyValues into flat list of Properties, + /// with each Property having a single PropertyValue in its Values collection + /// + public static IList ExpandByValues(this IEnumerable properties, string cultureName) + { + return properties + .Where(x => !x.Hidden) + .SelectMany(property => + { + var propertyValues = property.Dictionary + // Group by Alias for dictionary properties + ? property.Values + .GroupBy(propertyValue => propertyValue.Alias) + .Select(aliasGroup + => aliasGroup.FirstOrDefault(propertyValue => propertyValue.LanguageCode.EqualsInvariant(cultureName)) + // If localization not found build default value + ?? aliasGroup.Select(propertyValue => + { + var clonedValue = (PropertyValue)propertyValue.Clone(); + clonedValue.Value = aliasGroup.Key; + return clonedValue; + }).First() + ) + : property.Values.Where(x => x.LanguageCode.EqualsInvariant(cultureName) || x.LanguageCode.IsNullOrEmpty()); + + // wrap each PropertyValue into a Property + return propertyValues + .Select(propertyValue => propertyValue.CopyPropertyWithValue(property)) + .DefaultIfEmpty(property.CopyPropertyWithoutValues()); + }) + .ToList(); + } + + /// + /// Sorts properties by Priority attribute, then flattens the key-value tree + /// + public static IList ExpandOrderedByValues(this IEnumerable properties, string cultureName) + { + if (properties.IsNullOrEmpty()) + { + return new List(); + } + + properties = properties + .OrderByDescending(x => x.IsManageable) + .ThenBy(x => x.DisplayOrder ?? int.MaxValue) + .ThenBy(x => x.Name); + + return properties.ExpandByValues(cultureName); + } + + /// + /// Filters and sorts properties by KeyProperty attribute, then flattens the key-value tree + /// + public static IList ExpandKeyPropertiesByValues(this IEnumerable properties, string cultureName, int take = 0) + { + if (properties.IsNullOrEmpty()) + { + return new List(); + } + + properties = properties + .Where(x => !x.Attributes.IsNullOrEmpty() && x.Attributes.Any(a => a.Name.EqualsInvariant(ModuleConstants.KeyProperty))) + .OrderBy(x => + { + var keyPropertyAttr = x.Attributes?.FirstOrDefault(x => x.Name.EqualsInvariant(ModuleConstants.KeyProperty)); + return keyPropertyAttr?.Value.TryParse(int.MaxValue); + }); + + if (take > 0) + { + properties = properties.Take(take); + } + + return properties.ExpandByValues(cultureName); + } + + public static Property CopyPropertyWithValue(this PropertyValue propertyValue, Property property) + { + var clonedProperty = (Property)property.Clone(); + clonedProperty.Values = new List { propertyValue }; + return clonedProperty; + } + + private static Property CopyPropertyWithoutValues(this Property property) + { + var clonedProperty = (Property)property.Clone(); + clonedProperty.Values = Array.Empty(); + return clonedProperty; + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Extensions/ResolveFieldContextExtensions.cs b/src/VirtoCommerce.XCatalog.Core/Extensions/ResolveFieldContextExtensions.cs new file mode 100644 index 0000000..efba52f --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Extensions/ResolveFieldContextExtensions.cs @@ -0,0 +1,29 @@ +using GraphQL; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Extensions +{ + public static class ResolveFieldContextExtensions + { + public static T GetCatalogQuery(this IResolveFieldContext context) where T : ICatalogQuery + { + var result = AbstractTypeFactory.TryCreateInstance(); + result.StoreId = context.GetArgumentOrValue("storeId"); + result.UserId = context.GetArgumentOrValue("userId") ?? context.GetCurrentUserId(); + result.CurrencyCode = context.GetArgumentOrValue("currencyCode"); + result.CultureName = context.GetArgumentOrValue("cultureName"); + + return result; + } + + public static void SetCatalogQuery(this IResolveFieldContext context, ICatalogQuery query) + { + context.UserContext["storeId"] = query.StoreId; + context.UserContext["userId"] = query.UserId; + context.UserContext["currencyCode"] = query.CurrencyCode; + context.UserContext["cultureName"] = query.CultureName; + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Extensions/RewardExtensions.cs b/src/VirtoCommerce.XCatalog.Core/Extensions/RewardExtensions.cs new file mode 100644 index 0000000..3b7bbf0 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Extensions/RewardExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using VirtoCommerce.CoreModule.Core.Common; +using VirtoCommerce.CoreModule.Core.Currency; +using VirtoCommerce.Xapi.Core.Models; +using VirtoCommerce.MarketingModule.Core.Model.Promotions; +using VirtoCommerce.Platform.Core.Common; + +namespace VirtoCommerce.XCatalog.Core.Extensions +{ + public static class RewardExtensions + { + public static void ApplyRewards(this List productPrices, CatalogItemAmountReward[] rewards) + { + if (rewards.IsNullOrEmpty()) + { + return; + } + + var rewardsMap = productPrices + .Select(x => x.Currency) + .Distinct() + .ToDictionary(x => x, x => rewards); + + foreach (var productPrice in productPrices) + { + var mappedRewards = rewardsMap[productPrice.Currency]; + + productPrice.DiscountAmount = new Money(Math.Max(0, (productPrice.ListPrice - productPrice.SalePrice).Amount), productPrice.Currency); + + foreach (var reward in mappedRewards) + { + foreach (var tierPrice in productPrice.TierPrices) + { + tierPrice.DiscountAmount = new Money(Math.Max(0, (productPrice.ListPrice - tierPrice.Price).Amount), productPrice.Currency); + } + + if (!reward.IsValid) + { + continue; + } + + var priceAmount = (productPrice.ListPrice - productPrice.DiscountAmount).Amount; + + var discount = new Discount + { + DiscountAmount = reward.GetRewardAmount(priceAmount, 1), + Description = reward.Promotion.Description, + Coupon = reward.Coupon, + PromotionId = reward.Promotion.Id + }; + + productPrice.Discounts.Add(discount); + + if (discount.DiscountAmount > 0) + { + productPrice.DiscountAmount += discount.DiscountAmount; + + foreach (var tierPrice in productPrice.TierPrices) + { + tierPrice.DiscountAmount += reward.GetRewardAmount(tierPrice.Price.Amount, 1); + } + } + } + } + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/Breadcrumb.cs b/src/VirtoCommerce.XCatalog.Core/Models/Breadcrumb.cs new file mode 100644 index 0000000..f412bd9 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/Breadcrumb.cs @@ -0,0 +1,16 @@ +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class Breadcrumb + { + public Breadcrumb(string type) + { + TypeName = type; + } + + public string ItemId { get; set; } + public string TypeName { get; private set; } + public virtual string Title { get; set; } + public virtual string SeoPath { get; set; } + public virtual string SemanticUrl { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/ChildCategoriesQueryResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/ChildCategoriesQueryResponse.cs new file mode 100644 index 0000000..e9316a7 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/ChildCategoriesQueryResponse.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.XCatalog.Core.Models; + +public class ChildCategoriesQueryResponse +{ + public IList ChildCategories { get; set; } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/ExpAvailabilityData.cs b/src/VirtoCommerce.XCatalog.Core/Models/ExpAvailabilityData.cs new file mode 100644 index 0000000..241a986 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/ExpAvailabilityData.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using VirtoCommerce.InventoryModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class ExpAvailabilityData + { + public bool IsBuyable { get; set; } + public bool IsAvailable { get; set; } + public bool IsInStock { get; set; } + public bool IsActive { get; set; } + public bool IsTrackInventory { get; set; } + + /// + /// This flag is used to indicate whether a offer is estimated or represents an actual value. + /// When set to true, it signifies that the product price and availability is an estimation, + /// often used when unable to get actual price and availability information or when the system is using cached offer information + /// + public bool IsEstimated { get; set; } + + public IEnumerable InventoryAll { get; set; } = Enumerable.Empty(); + + public long AvailableQuantity { get; set; } + + public virtual ExpAvailabilityData FromProduct(ExpProduct product) + { + AvailableQuantity = product.AvailableQuantity; + InventoryAll = product.AllInventories; + IsBuyable = product.IsBuyable; + IsAvailable = product.IsAvailable; + IsInStock = product.IsInStock; + IsActive = product.IndexedProduct?.IsActive ?? false; + IsTrackInventory = product.IndexedProduct?.TrackInventory ?? false; + return this; + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/ExpCategory.cs b/src/VirtoCommerce.XCatalog.Core/Models/ExpCategory.cs new file mode 100644 index 0000000..c6722e5 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/ExpCategory.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.Xapi.Core.Binding; +using VirtoCommerce.XCatalog.Core.Binding; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class ExpCategory + { + public string Id => Category?.Id; + + [BindIndexField(FieldName = "__object", BinderType = typeof(CategoryBinder))] + public virtual Category Category { get; set; } + + [BindIndexField(BinderType = typeof(KeyBinder))] + public virtual string Key { get; set; } + + //Level in hierarchy + public int Level => Category?.Outline?.Split("/").Length ?? 0; + + public IList ChildCategories { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/ExpProduct.cs b/src/VirtoCommerce.XCatalog.Core/Models/ExpProduct.cs new file mode 100644 index 0000000..919c39d --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/ExpProduct.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.CoreModule.Core.Common; +using VirtoCommerce.CoreModule.Core.Currency; +using VirtoCommerce.CoreModule.Core.Seo; +using VirtoCommerce.Xapi.Core.Binding; +using VirtoCommerce.Xapi.Core.Models; +using VirtoCommerce.InventoryModule.Core.Model; +using VirtoCommerce.MarketingModule.Core.Model.Promotions; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.PricingModule.Core.Model; +using VirtoCommerce.StoreModule.Core.Model; +using VirtoCommerce.XCatalog.Core.Binding; +using VirtoCommerce.XCatalog.Core.Specifications; +using ProductPrice = VirtoCommerce.Xapi.Core.Models.ProductPrice; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class ExpProduct + { + public string Id => IndexedProduct?.Id; + + [BindIndexField(FieldName = "__object", BinderType = typeof(CatalogProductBinder))] + public virtual CatalogProduct IndexedProduct { get; set; } + + [BindIndexField(FieldName = "__variations", BinderType = typeof(VariationsBinder))] + public virtual IList IndexedVariationIds { get; set; } = new List(); + + [BindIndexField(FieldName = "__minvariationprice", BinderType = typeof(MinVariationPriceBinder))] + public IList IndexedMinVariationPrices { get; set; } = new List(); + + [BindIndexField(BinderType = typeof(KeyBinder))] + public virtual string Key { get; set; } + + public SeoInfo SeoInfo { get; set; } + + public bool IsBuyable + { + get + { + return AbstractTypeFactory.TryCreateInstance().IsSatisfiedBy(this); + } + } + + public bool IsAvailable + { + get + { + return AbstractTypeFactory.TryCreateInstance().IsSatisfiedBy(this); + } + } + + public bool IsInStock + { + get + { + return AbstractTypeFactory.TryCreateInstance().IsSatisfiedBy(this); + } + } + + public ProductPrice MinVariationPrice { get; set; } + + public IList AllPrices { get; set; } = new List(); + + /// + /// Inventory of all fulfillment centers. + /// + public IList AllInventories { get; set; } = new List(); + + /// + /// Inventory for default fulfillment center + /// + public InventoryInfo Inventory { get; private set; } + + public EditorialReview Description { get; set; } + + public ExpVendor Vendor { get; set; } + + public bool InWishlist { get; set; } + + public IList WishlistIds { get; set; } = []; + + public virtual long AvailableQuantity + { + get + { + long result = 0; + + if (IndexedProduct.TrackInventory.GetValueOrDefault(true) && AllInventories != null) + { + foreach (var inventory in AllInventories) + { + result += Math.Max(0, inventory.InStockQuantity - inventory.ReservedQuantity); + } + } + return result; + } + } + + public virtual void ApplyStaticDiscounts() + { + foreach (var productPrice in AllPrices) + { + productPrice.DiscountAmount = new Money(Math.Max(0, (productPrice.ListPrice - productPrice.SalePrice).Amount), productPrice.Currency); + } + } + + public virtual void ApplyRewards(CatalogItemAmountReward[] allRewards) + { + var productRewards = allRewards.Where(r => r.ProductId.IsNullOrEmpty() || r.ProductId.EqualsInvariant(Id)); + if (productRewards == null) + { + return; + } + + var rewardsMap = AllPrices + .Select(x => x.Currency) + .Distinct() + .ToDictionary(x => x, x => productRewards); + + foreach (var productPrice in AllPrices) + { + var mappedRewards = rewardsMap[productPrice.Currency]; + productPrice.Discounts.Clear(); + productPrice.DiscountAmount = new Money(Math.Max(0, (productPrice.ListPrice - productPrice.SalePrice).Amount), productPrice.Currency); + + foreach (var reward in mappedRewards) + { + foreach (var tierPrice in productPrice.TierPrices) + { + tierPrice.DiscountAmount = new Money(Math.Max(0, (productPrice.ListPrice - tierPrice.Price).Amount), productPrice.Currency); + } + + if (!reward.IsValid) + { + continue; + } + + var priceAmount = (productPrice.ListPrice - productPrice.DiscountAmount).Amount; + + var discount = new Discount + { + DiscountAmount = reward.GetRewardAmount(priceAmount, 1), + Description = reward.Promotion.Description, + Coupon = reward.Coupon, + PromotionId = reward.Promotion.Id + }; + + productPrice.Discounts.Add(discount); + + if (discount.DiscountAmount > 0) + { + productPrice.DiscountAmount += discount.DiscountAmount; + + foreach (var tierPrice in productPrice.TierPrices) + { + tierPrice.DiscountAmount += reward.GetRewardAmount(tierPrice.Price.Amount, 1); + } + } + } + } + } + + public virtual void ApplyStoreInventories(IEnumerable inventories, Store store) + { + if (inventories == null) + { + throw new ArgumentNullException(nameof(inventories)); + } + if (store == null) + { + throw new ArgumentNullException(nameof(store)); + } + + var availFullfilmentCentersIds = (store.AdditionalFulfillmentCenterIds ?? Array.Empty()).Concat(new[] { store.MainFulfillmentCenterId }); + + AllInventories.Clear(); + Inventory = null; + AllInventories = inventories.Where(x => x.ProductId == Id && availFullfilmentCentersIds.Contains(x.FulfillmentCenterId)).ToList(); + + Inventory = AllInventories.OrderByDescending(x => Math.Max(0, x.InStockQuantity - x.ReservedQuantity)).FirstOrDefault(); + + if (store.MainFulfillmentCenterId != null) + { + Inventory = AllInventories.FirstOrDefault(x => x.FulfillmentCenterId == store.MainFulfillmentCenterId) ?? Inventory; + } + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/ExpProductResponseGroup.cs b/src/VirtoCommerce.XCatalog.Core/Models/ExpProductResponseGroup.cs new file mode 100644 index 0000000..11101ac --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/ExpProductResponseGroup.cs @@ -0,0 +1,19 @@ +using System; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + [Flags] + public enum ExpProductResponseGroup + { + None = 0, + LoadPrices = 1, + LoadInventories = 1 << 1, + LoadFacets = 1 << 2, + LoadVendors = 1 << 3, + LoadRating = 1 << 4, + LoadWishlists = 1 << 5, + LoadPropertyMetadata = 1 << 6, + LoadVariationPrices = 1 << 7, + Full = LoadPrices | LoadInventories | LoadFacets | LoadVendors | LoadRating | LoadWishlists | LoadPropertyMetadata | LoadVariationPrices + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/ExpVariation.cs b/src/VirtoCommerce.XCatalog.Core/Models/ExpVariation.cs new file mode 100644 index 0000000..88caf6a --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/ExpVariation.cs @@ -0,0 +1,13 @@ +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class ExpVariation : ExpProduct + { + public ExpVariation(ExpProduct expProduct) + { + IndexedProduct = expProduct.IndexedProduct; + AllPrices = expProduct.AllPrices; + AllInventories = expProduct.AllInventories; + Vendor = expProduct.Vendor; + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/ICatalogQuery.cs b/src/VirtoCommerce.XCatalog.Core/Models/ICatalogQuery.cs new file mode 100644 index 0000000..3ed28b3 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/ICatalogQuery.cs @@ -0,0 +1,12 @@ +using VirtoCommerce.Xapi.Core.Index; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public interface ICatalogQuery : IHasIncludeFields + { + string StoreId { get; set; } + string UserId { get; set; } + string CultureName { get; set; } + string CurrencyCode { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/LoadCategoryResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/LoadCategoryResponse.cs new file mode 100644 index 0000000..e149718 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/LoadCategoryResponse.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class LoadCategoryResponse + { + public LoadCategoryResponse(ICollection expCategories) + { + Categories = expCategories; + } + + public ICollection Categories { get; private set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/LoadProductResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/LoadProductResponse.cs new file mode 100644 index 0000000..054309f --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/LoadProductResponse.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class LoadProductResponse + { + public LoadProductResponse(ICollection expProducts) + { + Products = expProducts; + } + + public ICollection Products { get; private set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/LoadPromotionsResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/LoadPromotionsResponse.cs new file mode 100644 index 0000000..afe3cf9 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/LoadPromotionsResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using VirtoCommerce.MarketingModule.Core.Model.Promotions; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class LoadPromotionsResponse + { + public IDictionary Promotions { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/LoadPropertiesResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/LoadPropertiesResponse.cs new file mode 100644 index 0000000..133b5ce --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/LoadPropertiesResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using VirtoCommerce.CatalogModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class LoadPropertiesResponse + { + public IDictionary Properties { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/LoadRelatedCatalogOutlineResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/LoadRelatedCatalogOutlineResponse.cs new file mode 100644 index 0000000..b247ff1 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/LoadRelatedCatalogOutlineResponse.cs @@ -0,0 +1,7 @@ +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class LoadRelatedCatalogOutlineResponse + { + public string Outline { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/LoadRelatedSlugPathResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/LoadRelatedSlugPathResponse.cs new file mode 100644 index 0000000..b0b5f67 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/LoadRelatedSlugPathResponse.cs @@ -0,0 +1,7 @@ +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class LoadRelatedSlugPathResponse + { + public string Slug { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/ProductSuggestionsQueryResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/ProductSuggestionsQueryResponse.cs new file mode 100644 index 0000000..11de417 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/ProductSuggestionsQueryResponse.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace VirtoCommerce.XCatalog.Core.Models; + +public class ProductSuggestionsQueryResponse +{ + public IList Suggestions { get; set; } = new List(); +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/SearchCategoryResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/SearchCategoryResponse.cs new file mode 100644 index 0000000..9bc138f --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/SearchCategoryResponse.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using VirtoCommerce.StoreModule.Core.Model; +using VirtoCommerce.XCatalog.Core.Queries; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class SearchCategoryResponse + { + public SearchCategoryQuery Query { get; set; } + + public int TotalCount { get; set; } + public IList Results { get; set; } + public Store Store { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/SearchProductAssociationsResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/SearchProductAssociationsResponse.cs new file mode 100644 index 0000000..4205f86 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/SearchProductAssociationsResponse.cs @@ -0,0 +1,9 @@ +using VirtoCommerce.CatalogModule.Core.Model.Search; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class SearchProductAssociationsResponse + { + public ProductAssociationSearchResult Result { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/SearchProductResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/SearchProductResponse.cs new file mode 100644 index 0000000..21ecaa6 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/SearchProductResponse.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using VirtoCommerce.CoreModule.Core.Currency; +using VirtoCommerce.Xapi.Core.Models.Facets; +using VirtoCommerce.StoreModule.Core.Model; +using VirtoCommerce.XDigitalCatalog.Queries; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class SearchProductResponse + { + public SearchProductQuery Query { get; set; } + + public int TotalCount { get; set; } + public IList Results { get; set; } + public IList Facets { get; set; } + + public IEnumerable AllStoreCurrencies { get; set; } + public Currency Currency { get; set; } + public Store Store { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/SearchPropertiesResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/SearchPropertiesResponse.cs new file mode 100644 index 0000000..6047695 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/SearchPropertiesResponse.cs @@ -0,0 +1,9 @@ +using VirtoCommerce.CatalogModule.Core.Model.Search; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class SearchPropertiesResponse + { + public PropertySearchResult Result { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/SearchPropertyDictionaryItemResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/SearchPropertyDictionaryItemResponse.cs new file mode 100644 index 0000000..35746d5 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/SearchPropertyDictionaryItemResponse.cs @@ -0,0 +1,9 @@ +using VirtoCommerce.CatalogModule.Core.Model.Search; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class SearchPropertyDictionaryItemResponse + { + public PropertyDictionaryItemSearchResult Result { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Models/SearchVideoQueryResponse.cs b/src/VirtoCommerce.XCatalog.Core/Models/SearchVideoQueryResponse.cs new file mode 100644 index 0000000..f58ac86 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Models/SearchVideoQueryResponse.cs @@ -0,0 +1,9 @@ +using VirtoCommerce.CatalogModule.Core.Model.Search; + +namespace VirtoCommerce.XCatalog.Core.Models +{ + public class SearchVideoQueryResponse + { + public VideoSearchResult Result { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/ModuleConstants.cs b/src/VirtoCommerce.XCatalog.Core/ModuleConstants.cs new file mode 100644 index 0000000..cecf55a --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/ModuleConstants.cs @@ -0,0 +1,7 @@ +namespace VirtoCommerce.XCatalog.Core +{ + public class ModuleConstants + { + public const string KeyProperty = "KeyProperty"; + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/CatalogQueryBase.cs b/src/VirtoCommerce.XCatalog.Core/Queries/CatalogQueryBase.cs new file mode 100644 index 0000000..3d4099b --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/CatalogQueryBase.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using GraphQL; +using GraphQL.Types; +using VirtoCommerce.Xapi.Core.BaseQueries; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.StoreModule.Core.Model; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class CatalogQueryBase : Query, ICatalogQuery + { + public string StoreId { get; set; } + public string UserId { get; set; } + public string CultureName { get; set; } + public string CurrencyCode { get; set; } + + public Store Store { get; set; } + public IList IncludeFields { get; set; } = Array.Empty(); + + public override IEnumerable GetArguments() + { + yield return Argument>(nameof(StoreId), description: "Store Id"); + yield return Argument(nameof(UserId), description: "User Id"); + yield return Argument(nameof(CultureName), description: "Currency code (\"USD\")"); + yield return Argument(nameof(CurrencyCode), description: "Culture name (\"en-US\")"); + } + + public override void Map(IResolveFieldContext context) + { + StoreId = context.GetArgument(nameof(StoreId)); + UserId = context.GetArgument(nameof(UserId)) ?? context.GetCurrentUserId(); + CultureName = context.GetArgument(nameof(CultureName)); + CurrencyCode = context.GetArgument(nameof(CurrencyCode)); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/ChildCategoriesQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/ChildCategoriesQuery.cs new file mode 100644 index 0000000..8fdde19 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/ChildCategoriesQuery.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using GraphQL; +using GraphQL.Types; +using VirtoCommerce.XCatalog.Core.Models; +using VirtoCommerce.XCatalog.Core.Queries; + +namespace VirtoCommerce.XDigitalCatalog.Queries; + +public class ChildCategoriesQuery : CatalogQueryBase +{ + public string CategoryId { get; set; } + public int MaxLevel { get; set; } + public bool OnlyActive { get; set; } + public string ProductFilter { get; set; } + + public override IEnumerable GetArguments() + { + foreach (var argument in base.GetArguments()) + { + yield return argument; + } + + yield return Argument(nameof(CategoryId)); + yield return Argument(nameof(MaxLevel)); + yield return Argument(nameof(OnlyActive)); + yield return Argument(nameof(ProductFilter)); + } + + public override void Map(IResolveFieldContext context) + { + base.Map(context); + + CategoryId = context.GetArgument(nameof(CategoryId)); + MaxLevel = context.GetArgument(nameof(MaxLevel)); + OnlyActive = context.GetArgument(nameof(OnlyActive)); + ProductFilter = context.GetArgument(nameof(ProductFilter)); + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/GetFulfillmentCenterQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/GetFulfillmentCenterQuery.cs new file mode 100644 index 0000000..316a58b --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/GetFulfillmentCenterQuery.cs @@ -0,0 +1,12 @@ +using VirtoCommerce.Xapi.Core.Infrastructure; +using VirtoCommerce.InventoryModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class GetFulfillmentCenterQuery : IQuery + { + public string Id { get; set; } + + public string StoreId { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/LoadCategoryQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/LoadCategoryQuery.cs new file mode 100644 index 0000000..14342e8 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/LoadCategoryQuery.cs @@ -0,0 +1,9 @@ +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class LoadCategoryQuery : CatalogQueryBase + { + public string[] ObjectIds { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/LoadProductsQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/LoadProductsQuery.cs new file mode 100644 index 0000000..56fc1ea --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/LoadProductsQuery.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class LoadProductsQuery : CatalogQueryBase + { + public IList ObjectIds { get; set; } + public bool EvaluatePromotions { get; set; } = true; + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/LoadPromotionsQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/LoadPromotionsQuery.cs new file mode 100644 index 0000000..c134b78 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/LoadPromotionsQuery.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using VirtoCommerce.Xapi.Core.Infrastructure; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class LoadPromotionsQuery : IQuery + { + public IEnumerable Ids { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/LoadPropertiesQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/LoadPropertiesQuery.cs new file mode 100644 index 0000000..1cbce0a --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/LoadPropertiesQuery.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using VirtoCommerce.Xapi.Core.Infrastructure; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class LoadPropertiesQuery : IQuery + { + public IEnumerable Ids { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/LoadRelatedCatalogOutlineQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/LoadRelatedCatalogOutlineQuery.cs new file mode 100644 index 0000000..5a370f5 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/LoadRelatedCatalogOutlineQuery.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using VirtoCommerce.CoreModule.Core.Outlines; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class LoadRelatedCatalogOutlineQuery : CatalogQueryBase + { + public IList Outlines { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/LoadRelatedSlugPathQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/LoadRelatedSlugPathQuery.cs new file mode 100644 index 0000000..37e19c1 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/LoadRelatedSlugPathQuery.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using VirtoCommerce.CoreModule.Core.Outlines; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class LoadRelatedSlugPathQuery : CatalogQueryBase + { + public IList Outlines { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/ProductSuggestionsQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/ProductSuggestionsQuery.cs new file mode 100644 index 0000000..31bca9f --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/ProductSuggestionsQuery.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using GraphQL; +using GraphQL.Types; +using VirtoCommerce.Xapi.Core.BaseQueries; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries; + +public class ProductSuggestionsQuery : Query +{ + public string StoreId { get; set; } + public string Query { get; set; } + public int Size { get; set; } + + public override IEnumerable GetArguments() + { + yield return Argument>(nameof(StoreId)); + yield return Argument(nameof(Query)); + yield return Argument(nameof(Size)); + } + + public override void Map(IResolveFieldContext context) + { + StoreId = context.GetArgument(nameof(StoreId)); + Query = context.GetArgument(nameof(Query)); + Size = context.GetArgument(nameof(Size)); + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/SearchCategoryQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/SearchCategoryQuery.cs new file mode 100644 index 0000000..5678808 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/SearchCategoryQuery.cs @@ -0,0 +1,66 @@ +using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class SearchCategoryQuery : CatalogQueryBase + { + public string Query { get; set; } + public bool Fuzzy { get; set; } + public int? FuzzyLevel { get; set; } + public string Filter { get; set; } + public string Facet { get; set; } + public string Sort { get; set; } + public int Skip { get; set; } + public int Take { get; set; } + public string[] ObjectIds { get; set; } + + public virtual string GetCategoryResponseGroup() + { + var result = CategoryResponseGroup.None; + + if (IncludeFields.ContainsAny("assets", "images", "imgSrc")) + { + result |= CategoryResponseGroup.WithImages; + } + + if (IncludeFields.ContainsAny("properties")) + { + result |= CategoryResponseGroup.WithProperties; + } + + if (IncludeFields.ContainsAny("seoInfo")) + { + result |= CategoryResponseGroup.WithSeo; + } + + if (IncludeFields.ContainsAny("slug")) + { + result |= CategoryResponseGroup.WithLinks; + } + + if (IncludeFields.ContainsAny("outline", "outlines", "slug", "level", "hasParent", "parent")) + { + result |= CategoryResponseGroup.WithOutlines; + } + + if (IncludeFields.ContainsAny("breadcrumbs")) + { + result |= CategoryResponseGroup.WithParents | CategoryResponseGroup.WithOutlines; + } + + if (IncludeFields.ContainsAny("description", "descriptions")) + { + result |= CategoryResponseGroup.Full; + } + + return result.ToString(); + } + + public bool GetLoadChildCategories() + { + return IncludeFields.ContainsAny("childCategories"); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/SearchFulfillmentCentersQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/SearchFulfillmentCentersQuery.cs new file mode 100644 index 0000000..86347e8 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/SearchFulfillmentCentersQuery.cs @@ -0,0 +1,17 @@ +using VirtoCommerce.Xapi.Core.Infrastructure; +using VirtoCommerce.InventoryModule.Core.Model.Search; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class SearchFulfillmentCentersQuery : IQuery + { + public int Skip { get; set; } + public int Take { get; set; } + + public string StoreId { get; set; } + public string Query { get; set; } + public string Sort { get; set; } + + public string[] FulfillmentCenterIds { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/SearchProductAssociationsQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/SearchProductAssociationsQuery.cs new file mode 100644 index 0000000..bb0e371 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/SearchProductAssociationsQuery.cs @@ -0,0 +1,14 @@ +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class SearchProductAssociationsQuery : CatalogQueryBase + { + public string[] ObjectIds { get; set; } + public string Keyword { get; set; } + public string Group { get; set; } + public string Sort { get; set; } + public int Skip { get; set; } + public int Take { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/SearchProductQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/SearchProductQuery.cs new file mode 100644 index 0000000..07b5cdf --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/SearchProductQuery.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQL; +using GraphQL.Builders; +using GraphQL.Types; +using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.Xapi.Core.Infrastructure; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.XCatalog.Core.Models; +using VirtoCommerce.XCatalog.Core.Queries; + +namespace VirtoCommerce.XDigitalCatalog.Queries +{ + public class SearchProductQuery : CatalogQueryBase, ISearchQuery + { + public string Keyword { get => Query; set => Query = value; } + public string Query { get; set; } + public bool Fuzzy { get; set; } + public int? FuzzyLevel { get; set; } + public string Filter { get; set; } + public string Facet { get; set; } + public string Sort { get; set; } + public int Skip { get; set; } + public int Take { get; set; } + public string[] ObjectIds { get; set; } + public bool EvaluatePromotions { get; set; } = true; + + public override IEnumerable GetArguments() + { + yield return Argument>(nameof(StoreId), "The store id where products are searched"); + yield return Argument(nameof(UserId), "The customer id for search result impersonation"); + yield return Argument(nameof(CurrencyCode), "The currency for which all prices data will be returned"); + yield return Argument(nameof(CultureName), "The culture name for cart context product"); + + yield return Argument(nameof(Query), "The query parameter performs the full-text search"); + yield return Argument(nameof(Filter), "This parameter applies a filter to the query results"); + yield return Argument(nameof(Facet), "Facets calculate statistical counts to aid in faceted navigation."); + yield return Argument(nameof(Fuzzy), "When the fuzzy query parameter is set to true the search endpoint will also return products that contain slight differences to the search text."); + yield return Argument(nameof(FuzzyLevel), "The fuzziness level is quantified in terms of the Damerau-Levenshtein distance, this distance being the number of operations needed to transform one word into another."); + yield return Argument(nameof(Sort), "The sort expression"); + + yield return Argument>("productIds", "Product Ids"); + + yield return Argument("custom", "Can be used for custom query parameters"); + } + + public override void Map(IResolveFieldContext context) + { + base.Map(context); + + var productIds = context.GetArgument>("productIds"); + if (!productIds.IsNullOrEmpty()) + { + ObjectIds = productIds.ToArray(); + Take = productIds.Count; + } + else + { + Query = context.GetArgument(nameof(Query)); + Filter = context.GetArgument(nameof(Filter)); + Facet = context.GetArgument(nameof(Facet)); + Fuzzy = context.GetArgument(nameof(Fuzzy)); + FuzzyLevel = context.GetArgument(nameof(FuzzyLevel)); + Sort = context.GetArgument(nameof(Sort)); + + if (context is IResolveConnectionContext connectionContext) + { + Skip = int.TryParse(connectionContext.After, out var skip) ? skip : 0; + Take = connectionContext.First ?? connectionContext.PageSize ?? 20; + } + } + } + + public virtual string GetResponseGroup() + { + var result = ExpProductResponseGroup.None; + if (IncludeFields.Any(x => x.Contains("price"))) + { + result |= ExpProductResponseGroup.LoadPrices; + } + if (IncludeFields.Any(x => x.Contains("minVariationPrice"))) + { + result |= ExpProductResponseGroup.LoadVariationPrices; + } + if (IncludeFields.Any(x => x.Contains("availabilityData"))) + { + result |= ExpProductResponseGroup.LoadInventories; + result |= ExpProductResponseGroup.LoadPrices; + } + if (IncludeFields.Any(x => x.Contains("vendor"))) + { + result |= ExpProductResponseGroup.LoadVendors; + } + if (IncludeFields.Any(x => x.Contains("rating"))) + { + result |= ExpProductResponseGroup.LoadRating; + } + if (IncludeFields.Any(x => x.Contains("_facets"))) + { + result |= ExpProductResponseGroup.LoadFacets; + } + if (IncludeFields.ContainsAny("inWishlist", "wishlistIds")) + { + result |= ExpProductResponseGroup.LoadWishlists; + } + if (IncludeFields.ContainsAny("properties", "keyProperties")) + { + result |= ExpProductResponseGroup.LoadPropertyMetadata; + } + return result.ToString(); + } + + public virtual string GetItemResponseGroup() + { + var result = ItemResponseGroup.None; + + if (IncludeFields.ContainsAny("assets", "images", "imgSrc")) + { + result |= ItemResponseGroup.WithImages; + } + + if (IncludeFields.ContainsAny("properties", "keyProperties", "brandName")) + { + result |= ItemResponseGroup.WithProperties; + } + + // "descriptions" could look redundant, but better to check it explicitly - clear approach for possible modification or different "Contains" logic + if (IncludeFields.ContainsAny("description", "descriptions")) + { + result |= ItemResponseGroup.ItemEditorialReviews; + } + + if (IncludeFields.ContainsAny("seoInfo")) + { + result |= ItemResponseGroup.WithSeo; + } + + if (IncludeFields.ContainsAny("slug")) + { + result |= ItemResponseGroup.WithLinks; + result |= ItemResponseGroup.WithSeo; + } + + if (IncludeFields.ContainsAny("outline", "outlines", "slug", "level", "breadcrumbs")) + { + result |= ItemResponseGroup.WithOutlines; + result |= ItemResponseGroup.WithSeo; + } + + if (IncludeFields.ContainsAny("availabilityData")) + { + result |= ItemResponseGroup.Inventory; + } + + return result.ToString(); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/SearchPropertiesQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/SearchPropertiesQuery.cs new file mode 100644 index 0000000..5094a66 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/SearchPropertiesQuery.cs @@ -0,0 +1,14 @@ +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class SearchPropertiesQuery : CatalogQueryBase + { + public string CatalogId { get; set; } + public object[] Types { get; set; } + public string Filter { get; set; } + public int Skip { get; set; } + public int Take { get; set; } + + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/SearchPropertyDictionaryItemQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/SearchPropertyDictionaryItemQuery.cs new file mode 100644 index 0000000..d8cf945 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/SearchPropertyDictionaryItemQuery.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class SearchPropertyDictionaryItemQuery : CatalogQueryBase + { + public int Skip { get; set; } + public int Take { get; set; } + + public IList PropertyIds { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Queries/SearchVideoQuery.cs b/src/VirtoCommerce.XCatalog.Core/Queries/SearchVideoQuery.cs new file mode 100644 index 0000000..c83b97b --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Queries/SearchVideoQuery.cs @@ -0,0 +1,14 @@ +using VirtoCommerce.Xapi.Core.Infrastructure; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Queries +{ + public class SearchVideoQuery : IQuery + { + public int Skip { get; set; } + public int Take { get; set; } + public string CultureName { get; set; } + public string OwnerId { get; set; } + public string OwnerType { get; set; } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/AssetType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/AssetType.cs new file mode 100644 index 0000000..beec3e0 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/AssetType.cs @@ -0,0 +1,25 @@ +using GraphQL.Types; +using VirtoCommerce.CatalogModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class AssetType : ObjectGraphType + { + public AssetType() + { + Name = "Asset"; + + Field(x => x.Id, nullable: false).Description("The unique ID of the asset."); + Field(x => x.Name, nullable: true).Description("The name of the asset."); + Field(x => x.MimeType, nullable: true).Description("MimeType of the asset."); + Field(x => x.Size, nullable: false).Description("Size of the asset."); + Field(x => x.Url, nullable: false).Description("Url of the asset."); + Field(x => x.RelativeUrl, nullable: true).Description("RelativeUrl of the asset."); + Field(x => x.TypeId, nullable: false).Description("Type id of the asset."); + Field(x => x.Group, nullable: true).Description("Group of the asset."); + Field("cultureName", + "Culture name", + resolve: context => context.Source.LanguageCode); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/AvailabilityDataType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/AvailabilityDataType.cs new file mode 100644 index 0000000..f7777ce --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/AvailabilityDataType.cs @@ -0,0 +1,37 @@ +using GraphQL.Types; +using VirtoCommerce.Xapi.Core.Schemas; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class AvailabilityDataType : ExtendableGraphType + { + public AvailabilityDataType() + { + Name = "AvailabilityData"; + + Field(x => x.AvailableQuantity, nullable: false).Description("Available quantity"); + Field>("IsBuyable", + "Is buyable", + resolve: context => context.Source.IsBuyable); + Field>("IsAvailable", + "Is available", + resolve: context => context.Source.IsAvailable); + Field>("IsInStock", + "Is in stock", + resolve: context => context.Source.IsInStock); + Field>("IsActive", + "Is active", + resolve: context => context.Source.IsActive); + Field>("IsTrackInventory", + "Is track inventory", + resolve: context => context.Source.IsTrackInventory); + ExtendableField>>>("inventories", + "Inventories", + resolve: context => context.Source.InventoryAll); + Field>("IsEstimated", + "Is estimated", + resolve: context => context.Source.IsEstimated); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/BreadcrumbsType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/BreadcrumbsType.cs new file mode 100644 index 0000000..6e2f5e4 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/BreadcrumbsType.cs @@ -0,0 +1,18 @@ +using GraphQL.Types; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class BreadcrumbType : ObjectGraphType + { + public BreadcrumbType() + { + Name = "Breadcrumb"; + Field(x => x.ItemId, nullable: false).Description("Id of item the breadcrumb calculated for"); + Field(x => x.Title, nullable: false).Description("Name of current breadcrumb"); + Field(x => x.TypeName, nullable: false).Description("Catalog, category or product"); + Field(x => x.SeoPath, nullable: true).Description("Full path from catalog"); + Field(x => x.SemanticUrl, nullable: true).Description("Semantic URL keyword"); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/CatalogDiscountType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/CatalogDiscountType.cs new file mode 100644 index 0000000..7291dba --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/CatalogDiscountType.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using GraphQL.DataLoader; +using GraphQL.Resolvers; +using GraphQL.Types; +using MediatR; +using VirtoCommerce.CoreModule.Core.Common; +using VirtoCommerce.Xapi.Core.Helpers; +using VirtoCommerce.Xapi.Core.Schemas; +using VirtoCommerce.MarketingModule.Core.Model.Promotions; +using VirtoCommerce.XCatalog.Core.Queries; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class CatalogDiscountType : DiscountType + { + public CatalogDiscountType(IMediator mediator, IDataLoaderContextAccessor dataLoader) + { + var promotion = new EventStreamFieldType + { + Name = "promotion", + Type = GraphTypeExtenstionHelper.GetActualType(), + Arguments = new QueryArguments(), + Resolver = new FuncFieldResolver>(context => + { + var loader = dataLoader.Context.GetOrAddBatchLoader("promotionsLoader", (ids) => LoadPromotionsAsync(mediator, ids)); + return loader.LoadAsync(context.Source.PromotionId); + }) + }; + AddField(promotion); + } + + protected virtual async Task> LoadPromotionsAsync(IMediator mediator, IEnumerable ids) + { + var result = await mediator.Send(new LoadPromotionsQuery { Ids = ids }); + + return result.Promotions; + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/CategoryDescriptionType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/CategoryDescriptionType.cs new file mode 100644 index 0000000..b4cae2b --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/CategoryDescriptionType.cs @@ -0,0 +1,16 @@ +using GraphQL.Types; +using VirtoCommerce.CatalogModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class CategoryDescriptionType : ObjectGraphType + { + public CategoryDescriptionType() + { + Field(x => x.Id, nullable: false).Description("Description ID."); + Field(x => x.DescriptionType, nullable: true).Description("Description type."); + Field(x => x.Content, nullable: true).Description("Description text."); + Field(x => x.LanguageCode, nullable: true).Description("Description language code."); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/CategoryType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/CategoryType.cs new file mode 100644 index 0000000..09211da --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/CategoryType.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GraphQL; +using GraphQL.DataLoader; +using GraphQL.Types; +using MediatR; +using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.CoreModule.Core.Outlines; +using VirtoCommerce.CoreModule.Core.Seo; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.Xapi.Core.Schemas; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.StoreModule.Core.Model; +using VirtoCommerce.XCatalog.Core.Extensions; +using VirtoCommerce.XCatalog.Core.Models; +using VirtoCommerce.XCatalog.Core.Queries; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class CategoryType : ExtendableGraphType + { + public CategoryType(IMediator mediator, IDataLoaderContextAccessor dataLoader) + { + Name = "Category"; + + Field(x => x.Id, nullable: false).Description("Id of category."); + Field(x => x.Category.ImgSrc, nullable: true).Description("The category image."); + Field(x => x.Category.Code, nullable: false).Description("SKU of category."); + Field(x => x.Category.Name, nullable: false).Description("Name of category."); + Field(x => x.Level, nullable: false).Description(@"Level in hierarchy"); + Field(x => x.Category.Priority, nullable: false).Description(@"The category priority."); + + FieldAsync("outline", resolve: async context => + { + var outlines = context.Source.Category?.Outlines; + if (outlines.IsNullOrEmpty()) + { + return null; + } + + var loadRelatedCatalogOutlineQuery = context.GetCatalogQuery(); + loadRelatedCatalogOutlineQuery.Outlines = outlines; + + var response = await mediator.Send(loadRelatedCatalogOutlineQuery); + return response.Outline; + }, description: @"All parent categories ids relative to the requested catalog and concatenated with \ . E.g. (1/21/344)"); + + FieldAsync("slug", resolve: async context => + { + var outlines = context.Source.Category?.Outlines; + if (outlines.IsNullOrEmpty()) + { + return null; + } + + var loadRelatedSlugPathQuery = context.GetCatalogQuery(); + loadRelatedSlugPathQuery.Outlines = outlines; + + var response = await mediator.Send(loadRelatedSlugPathQuery); + return response.Slug; + }, description: @"Request related slug for category"); + + Field(x => x.Category.Path, nullable: true).Description("Category path in to the requested catalog (all parent categories names concatenated. E.g. (parent1/parent2))"); + + Field>("seoInfo", resolve: context => + { + var source = context.Source; + var storeId = context.GetArgumentOrValue("storeId"); + var cultureName = context.GetArgumentOrValue("cultureName"); + + SeoInfo seoInfo = null; + + if (!source.Category.SeoInfos.IsNullOrEmpty()) + { + seoInfo = source.Category.SeoInfos.GetBestMatchingSeoInfo(storeId, cultureName); + } + + return seoInfo ?? SeoInfosExtensions.GetFallbackSeoInfo(source.Id, source.Category.Name, cultureName); + }, description: "Request related SEO info"); + + Field>>>("descriptions", + arguments: new QueryArguments(new QueryArgument { Name = "type" }), + resolve: context => + { + var descriptions = context.Source.Category.Descriptions; + var cultureName = context.GetArgumentOrValue("cultureName"); + var type = context.GetArgumentOrValue("type"); + if (cultureName != null) + { + descriptions = descriptions.Where(x => string.IsNullOrEmpty(x.LanguageCode) || x.LanguageCode.EqualsInvariant(cultureName)).ToList(); + } + if (type != null) + { + descriptions = descriptions.Where(x => x.DescriptionType?.EqualsInvariant(type) ?? true).ToList(); + } + return descriptions; + }); + + Field("description", + arguments: new QueryArguments(new QueryArgument { Name = "type" }), + resolve: context => + { + var descriptions = context.Source.Category.Descriptions; + var type = context.GetArgumentOrValue("type"); + var cultureName = context.GetArgumentOrValue("cultureName"); + + if (!descriptions.IsNullOrEmpty()) + { + return descriptions.Where(x => x.DescriptionType.EqualsInvariant(type ?? "FullReview")).FirstBestMatchForLanguage(cultureName) as CategoryDescription + ?? descriptions.FirstBestMatchForLanguage(cultureName) as CategoryDescription; + } + + return null; + }); + + Field("parent").ResolveAsync(ctx => + { + var loader = dataLoader.Context.GetOrAddBatchLoader("parentsCategoryLoader", (ids) => LoadCategoriesAsync(mediator, ids, ctx)); + + return TryGetCategoryParentId(ctx, out var parentCategoryId) + ? loader.LoadAsync(parentCategoryId) + : new DataLoaderResult(Task.FromResult(null)); + }); + + Field>("hasParent", + "Have a parent", + resolve: context => TryGetCategoryParentId(context, out _)); + Field>>>("outlines", + "Outlines", + resolve: context => context.Source.Category.Outlines ?? Array.Empty()); + Field>>>("images", + "Images", + resolve: context => context.Source.Category.Images ?? Array.Empty()); + Field>>>("breadcrumbs", + "Breadcrumbs", + resolve: context => + { + + var store = context.GetArgumentOrValue("store"); + var cultureName = context.GetValue("cultureName"); + + return context.Source.Category.Outlines.GetBreadcrumbsFromOutLine(store, cultureName); + + }); + + ExtendableField>>>("properties", + arguments: new QueryArguments(new QueryArgument> { Name = "names" }), + resolve: context => + { + var names = context.GetArgument("names"); + var cultureName = context.GetValue("cultureName"); + var result = context.Source.Category.Properties.ExpandByValues(cultureName); + if (!names.IsNullOrEmpty()) + { + result = result.Where(x => names.Contains(x.Name, StringComparer.InvariantCultureIgnoreCase)).ToList(); + } + return result; + }); + + Field>>>( + nameof(ExpCategory.ChildCategories), + resolve: context => context.Source.ChildCategories ?? Array.Empty()); + } + + protected virtual bool TryGetCategoryParentId(IResolveFieldContext context, out string parentId) + { + parentId = null; + var outlines = context.Source.Category?.Outlines; + if (outlines.IsNullOrEmpty()) + { + return false; + } + + var store = context.GetArgumentOrValue("store"); + + foreach (var outline in outlines.Where(outline => outline.Items.Any(x => x.Id.Equals(store.Catalog)))) + { + parentId = outline.Items.Take(outline.Items.Count - 1).Select(x => x.Id).LastOrDefault(); + + //parentId should be a category id, not a catalog id + if (parentId != null && parentId != store.Catalog) + { + return true; + } + } + return false; + } + + private static async Task> LoadCategoriesAsync(IMediator mediator, IEnumerable ids, IResolveFieldContext context) + { + var loadCategoryQuery = context.GetCatalogQuery(); + loadCategoryQuery.ObjectIds = ids.Where(x => x != null).ToArray(); + loadCategoryQuery.IncludeFields = context.SubFields.Values.GetAllNodesPaths(context).ToArray(); + + var response = await mediator.Send(loadCategoryQuery); + return response.Categories.ToDictionary(x => x.Id); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/ChildCategoriesQueryResponseType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/ChildCategoriesQueryResponseType.cs new file mode 100644 index 0000000..4a158b3 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/ChildCategoriesQueryResponseType.cs @@ -0,0 +1,15 @@ +using GraphQL.Types; +using VirtoCommerce.Xapi.Core.Schemas; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Schemas; + +public class ChildCategoriesQueryResponseType : ExtendableGraphType +{ + public ChildCategoriesQueryResponseType() + { + Field>( + nameof(ChildCategoriesQueryResponse.ChildCategories), + resolve: context => context.Source.ChildCategories); + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/DescriptionType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/DescriptionType.cs new file mode 100644 index 0000000..db3491b --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/DescriptionType.cs @@ -0,0 +1,16 @@ +using GraphQL.Types; +using VirtoCommerce.CatalogModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class DescriptionType : ObjectGraphType + { + public DescriptionType() + { + Field(x => x.Id, nullable: false).Description("Description ID."); + Field(x => x.ReviewType, true).Description("Description type."); + Field(x => x.Content, true).Description("Description text."); + Field(x => x.LanguageCode, true).Description("Description language code."); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/FulfillmentCenterAddressType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/FulfillmentCenterAddressType.cs new file mode 100644 index 0000000..d5057db --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/FulfillmentCenterAddressType.cs @@ -0,0 +1,34 @@ +using GraphQL.Types; +using VirtoCommerce.InventoryModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class FulfillmentCenterAddressType : ObjectGraphType
+ { + public FulfillmentCenterAddressType() + { + Field("id", resolve: context => context.Source.Key, description: "Id"); + Field(x => x.Key, true).Description("Id"); + Field(x => x.City, nullable: true).Description("City"); + Field(x => x.CountryCode, nullable: true).Description("Country code"); + Field(x => x.CountryName, nullable: true).Description("Country name"); + Field(x => x.Email, nullable: true).Description("Email"); + Field(x => x.FirstName, nullable: true).Description("First name"); + Field(x => x.MiddleName, nullable: true).Description("Middle name"); + Field(x => x.LastName, nullable: true).Description("Last name"); + Field(x => x.Line1, nullable: true).Description("Line1"); + Field(x => x.Line2, nullable: true).Description("Line2"); + Field(x => x.Name, nullable: true).Description("Name"); + Field(x => x.Organization, nullable: true).Description("Company name"); + Field(x => x.Phone, nullable: true).Description("Phone"); + Field(x => x.PostalCode, nullable: false).Description("Postal code"); + Field(x => x.RegionId, nullable: true).Description("Region id"); + Field(x => x.RegionName, nullable: true).Description("Region name"); + Field(x => x.Zip, nullable: true).Description("Zip"); + Field(x => x.OuterId, nullable: true).Description("Outer id"); + Field(nameof(Address.AddressType), + "Address type", + resolve: context => (int)context.Source.AddressType); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/FulfillmentCenterType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/FulfillmentCenterType.cs new file mode 100644 index 0000000..7f01ca0 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/FulfillmentCenterType.cs @@ -0,0 +1,35 @@ +using GraphQL; +using GraphQL.Types; +using VirtoCommerce.InventoryModule.Core.Model; +using VirtoCommerce.InventoryModule.Core.Services; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class FulfillmentCenterType : ObjectGraphType + { + public FulfillmentCenterType(IFulfillmentCenterGeoService fulfillmentCenterGeoService) + { + Field(x => x.Id).Description("Fulfillment Center ID."); + Field(x => x.Name, nullable: true).Description("Fulfillment Center name."); + Field(x => x.Description, nullable: true).Description("Fulfillment Center descripion."); + Field(x => x.OuterId, nullable: true).Description("Fulfillment Center outerId."); + Field(x => x.GeoLocation, nullable: true).Description("Fulfillment Center geo location."); + Field(x => x.ShortDescription, nullable: true).Description("Fulfillment Center short description."); + Field(nameof(FulfillmentCenter.Address).ToCamelCase(), + description: "Fulfillment Center address.", + resolve: x => x.Source.Address); + + FieldAsync>( + "nearest", + arguments: new QueryArguments(new QueryArgument { Name = "take" }), + description: "Nearest Fulfillment Centers", + resolve: async context => + { + var take = context.GetArgument("take", 10); + + var result = await fulfillmentCenterGeoService.GetNearestAsync(context.Source.Id, take); + return result; + }); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/ImageType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/ImageType.cs new file mode 100644 index 0000000..630a0f9 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/ImageType.cs @@ -0,0 +1,51 @@ +using GraphQL.Types; +using VirtoCommerce.CatalogModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class ImageType : ObjectGraphType + { + /// + /// + /// + /// + /// "sortOrder":0, + /// "binaryData":null, + /// "relativeUrl":"catalog/LG55EG9600/1431446771000_1119832.jpg", + /// "url":"http://localhost:10645/assets/catalog/LG55EG9600/1431446771000_1119832.jpg", + /// "typeId":"Image", + /// "group":"images", + /// "name":"1431446771000_1119832.jpg", + /// "outerId":null, + /// "languageCode":null, + /// "isInherited":false, + /// "seoObjectType":"Image", + /// "seoInfos":null, + /// "id":"a40b05e231ba4be0893bd4bbcfb92376" + /// + public ImageType() + { + Field>("id", + "Image ID", + resolve: context => context.Source.Id); + Field("name", + "Image name", + resolve: context => context.Source.Name); + Field("group", + "Image group", + resolve: context => context.Source.Group); + Field>("url", + "Image URL", + resolve: context => context.Source.Url); + Field("relativeUrl", + "Image relative URL", + resolve: context => context.Source.RelativeUrl); + Field>("sortOrder", + "Sort order", + resolve: context => context.Source.SortOrder); + Field("cultureName", + "Culture name", + resolve: context => context.Source.LanguageCode); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/InventoryInfoType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/InventoryInfoType.cs new file mode 100644 index 0000000..f40c7e5 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/InventoryInfoType.cs @@ -0,0 +1,34 @@ +using GraphQL.Types; +using VirtoCommerce.InventoryModule.Core.Model; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class InventoryInfoType : ObjectGraphType + { + public InventoryInfoType() + { + Name = "InventoryInfo"; + Description = ""; + Field>("inStockQuantity", + "Inventory in stock quantity", + resolve: context => context.Source.InStockQuantity); + Field>("reservedQuantity", + "Inventory reserved quantity", + resolve: context => context.Source.ReservedQuantity); + Field(d => d.FulfillmentCenterId); + Field(d => d.FulfillmentCenterName); + Field>("allowPreorder", + "Allow preorder", + resolve: context => context.Source.AllowPreorder); + Field>("allowBackorder", + "Allow backorder", + resolve: context => context.Source.AllowBackorder); + Field("preorderAvailabilityDate", + "Preorder availability date", + resolve: context => context.Source.PreorderAvailabilityDate); + Field("backorderAvailabilityDate", + "Backorder availability date", + resolve: context => context.Source.BackorderAvailabilityDate); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/OutlineItemType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/OutlineItemType.cs new file mode 100644 index 0000000..a04203a --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/OutlineItemType.cs @@ -0,0 +1,19 @@ +using GraphQL.Types; +using VirtoCommerce.CoreModule.Core.Outlines; +using VirtoCommerce.Xapi.Core.Schemas; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class OutlineItemType : ObjectGraphType + { + public OutlineItemType() + { + Field(x => x.Id, nullable: false); + Field(x => x.Name, nullable: false); + Field(x => x.SeoObjectType, nullable: false); + Field>>("seoInfos", + "SEO info", + resolve: context => context.Source.SeoInfos); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/OutlineType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/OutlineType.cs new file mode 100644 index 0000000..c2fb479 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/OutlineType.cs @@ -0,0 +1,15 @@ +using GraphQL.Types; +using VirtoCommerce.CoreModule.Core.Outlines; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class OutlineType : ObjectGraphType + { + public OutlineType() + { + Field>>("items", + "Outline items", + resolve: context => context.Source.Items); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/PriceType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/PriceType.cs new file mode 100644 index 0000000..8c6d226 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/PriceType.cs @@ -0,0 +1,62 @@ +using GraphQL.Types; +using VirtoCommerce.Xapi.Core.Models; +using VirtoCommerce.Xapi.Core.Schemas; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class PriceType : ObjectGraphType + { + public PriceType() + { + Field>("list", + "Price list", + resolve: context => context.Source.ListPrice); + Field>("listWithTax", + "Price list with tax", + resolve: context => context.Source.ListPriceWithTax); + Field>("sale", + "Sale price", + resolve: context => context.Source.SalePrice); + Field>("saleWithTax", + "Sale price with tax", + resolve: context => context.Source.SalePriceWithTax); + Field>("actual", + "Actual price", + resolve: context => context.Source.ActualPrice); + Field>("actualWithTax", + "Actual price with tax", + resolve: context => context.Source.ActualPriceWithTax); + Field>("discountAmount", + "Discount amount", + resolve: context => context.Source.DiscountAmount); + Field>("discountAmountWithTax", + "Discount amount with tax", + resolve: context => context.Source.DiscountAmountWithTax); + Field(d => d.DiscountPercent, nullable: false); + Field>("currency", + "Currency", + resolve: context => context.Source.Currency.Code); + Field("validFrom", + "Valid from", + resolve: context => context.Source.StartDate, deprecationReason: "startDate"); + Field("startDate", + "Start date", + resolve: context => context.Source.StartDate); + Field("validUntil", + "Valid until", + resolve: context => context.Source.EndDate, deprecationReason: "endDate"); + Field("endDate", + "End date", + resolve: context => context.Source.EndDate); + Field>>>("tierPrices", + "Tier prices", + resolve: context => context.Source.TierPrices); + Field>>>("discounts", + "Discounts", + resolve: context => context.Source.Discounts); + + Field(d => d.PricelistId, nullable: true).Description("The product price list"); + Field(d => d.MinQuantity, nullable: true).Description("The product min qty"); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/ProductAssociationType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/ProductAssociationType.cs new file mode 100644 index 0000000..3e75b6c --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/ProductAssociationType.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GraphQL; +using GraphQL.DataLoader; +using GraphQL.Resolvers; +using GraphQL.Types; +using MediatR; +using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.Xapi.Core.Helpers; +using VirtoCommerce.XCatalog.Core.Extensions; +using VirtoCommerce.XCatalog.Core.Models; +using VirtoCommerce.XCatalog.Core.Queries; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class ProductAssociationType : ObjectGraphType + { + public ProductAssociationType(IDataLoaderContextAccessor dataLoader, IMediator mediator) + { + Name = "ProductAssociation"; + Description = "product association."; + + Field(d => d.Type, nullable: false); + Field(d => d.Priority, nullable: false); + Field("Quantity", x => x.Quantity, nullable: true, type: typeof(IntGraphType)); + Field(d => d.AssociatedObjectId, nullable: true); + Field(d => d.AssociatedObjectType, nullable: true); + Field>>>("tags", resolve: context => context.Source.Tags?.ToList() ?? new List()); + + var productField = new FieldType + { + Name = "product", + Type = GraphTypeExtenstionHelper.GetActualType(), + Resolver = new FuncFieldResolver>(context => + { + var loader = dataLoader.Context.GetOrAddBatchLoader("associatedProductLoader", (ids) => LoadProductsAsync(mediator, ids, context)); + return loader.LoadAsync(context.Source.AssociatedObjectId); + }) + }; + AddField(productField); + } + + public static async Task> LoadProductsAsync(IMediator mediator, IEnumerable ids, IResolveFieldContext context) + { + var query = context.GetCatalogQuery(); + query.ObjectIds = ids.ToArray(); + query.IncludeFields = context.SubFields.Values.GetAllNodesPaths(context).Select(x => x.Replace("associations.items.product", string.Empty)).ToArray(); + + var response = await mediator.Send(query); + return response.Products.ToDictionary(x => x.Id); + } + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/ProductSuggestionsQueryResponseType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/ProductSuggestionsQueryResponseType.cs new file mode 100644 index 0000000..f7287dd --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/ProductSuggestionsQueryResponseType.cs @@ -0,0 +1,15 @@ +using GraphQL.Types; +using VirtoCommerce.Xapi.Core.Schemas; +using VirtoCommerce.XCatalog.Core.Models; + +namespace VirtoCommerce.XCatalog.Core.Schemas; + +public class ProductSuggestionsQueryResponseType : ExtendableGraphType +{ + public ProductSuggestionsQueryResponseType() + { + Field>( + nameof(ProductSuggestionsQueryResponse.Suggestions), + resolve: context => context.Source.Suggestions); + } +} diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/ProductType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/ProductType.cs new file mode 100644 index 0000000..35a5b17 --- /dev/null +++ b/src/VirtoCommerce.XCatalog.Core/Schemas/ProductType.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GraphQL; +using GraphQL.Builders; +using GraphQL.DataLoader; +using GraphQL.Types; +using MediatR; +using VirtoCommerce.CatalogModule.Core.Model; +using VirtoCommerce.CoreModule.Core.Outlines; +using VirtoCommerce.CoreModule.Core.Seo; +using VirtoCommerce.Xapi.Core.Extensions; +using VirtoCommerce.Xapi.Core.Helpers; +using VirtoCommerce.Xapi.Core.Infrastructure; +using VirtoCommerce.Xapi.Core.Models; +using VirtoCommerce.Xapi.Core.Schemas; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.StoreModule.Core.Model; +using VirtoCommerce.XCatalog.Core.Extensions; +using VirtoCommerce.XCatalog.Core.Models; +using VirtoCommerce.XCatalog.Core.Queries; + +namespace VirtoCommerce.XCatalog.Core.Schemas +{ + public class ProductType : ExtendableGraphType + { + /// + ///{ + /// product(id: "f1b26974b7634abaa0900e575a99476f") + /// { + /// id + /// code + /// category{ id code name hasParent slug } + /// name + /// metaTitle + /// metaDescription + /// metaKeywords + /// brandName + /// slug + /// imgSrc + /// productType + /// masterVariation { + /// images{ id url name } + /// assets{ id size url } + /// prices(cultureName: "en-us"){ + /// list { amount } + /// currency + /// } + /// availabilityData{ + /// availableQuantity + /// inventories{ + /// inStockQuantity + /// fulfillmentCenterId + /// fulfillmentCenterName + /// allowPreorder + /// allowBackorder + /// } + /// } + /// properties{ id name valueType value valueId } + /// } + ///} + /// + public ProductType(IMediator mediator, IDataLoaderContextAccessor dataLoader) + { + Name = "Product"; + Description = "Products are the sellable goods in an e-commerce project."; + + Field(d => d.IndexedProduct.Id, nullable: false).Description("The unique ID of the product."); + Field(d => d.IndexedProduct.Code, nullable: false).Description("The product SKU."); + Field("catalogId", + "The unique ID of the catalog", + resolve: context => context.Source.IndexedProduct.CatalogId); + Field(d => d.IndexedProduct.ProductType, nullable: true).Description("The type of product"); + Field(d => d.IndexedProduct.MinQuantity, nullable: true).Description("Min. quantity"); + Field(d => d.IndexedProduct.MaxQuantity, nullable: true).Description("Max. quantity"); + + FieldAsync("outline", resolve: async context => + { + var outlines = context.Source.IndexedProduct.Outlines; + if (outlines.IsNullOrEmpty()) + { + return null; + } + + var loadRelatedCatalogOutlineQuery = context.GetCatalogQuery(); + loadRelatedCatalogOutlineQuery.Outlines = outlines; + + var response = await mediator.Send(loadRelatedCatalogOutlineQuery); + return response.Outline; + }, description: @"All parent categories ids relative to the requested catalog and concatenated with \ . E.g. (1/21/344)"); + + FieldAsync("slug", resolve: async context => + { + var outlines = context.Source.IndexedProduct.Outlines; + if (outlines.IsNullOrEmpty()) + { + return null; + } + + var loadRelatedSlugPathQuery = context.GetCatalogQuery(); + loadRelatedSlugPathQuery.Outlines = outlines; + + var response = await mediator.Send(loadRelatedSlugPathQuery); + return response.Slug; + }, description: "Request related slug for product"); + + Field(d => d.IndexedProduct.Name, nullable: false).Description("The name of the product."); + + Field>("seoInfo", resolve: context => + { + var source = context.Source; + var storeId = context.GetArgumentOrValue("storeId"); + var cultureName = context.GetArgumentOrValue("cultureName"); + + SeoInfo seoInfo = null; + + if (!source.IndexedProduct.SeoInfos.IsNullOrEmpty()) + { + seoInfo = source.IndexedProduct.SeoInfos.GetBestMatchingSeoInfo(storeId, cultureName); + } + + return seoInfo ?? SeoInfosExtensions.GetFallbackSeoInfo(source.Id, source.IndexedProduct.Name, cultureName); + }, description: "Request related SEO info"); + + Field>>>("descriptions", + arguments: new QueryArguments(new QueryArgument { Name = "type" }), + resolve: context => + { + var reviews = context.Source.IndexedProduct.Reviews; + var cultureName = context.GetArgumentOrValue("cultureName"); + var type = context.GetArgumentOrValue("type"); + if (cultureName != null) + { + reviews = reviews.Where(x => string.IsNullOrEmpty(x.LanguageCode) || x.LanguageCode.EqualsInvariant(cultureName)).ToList(); + } + if (type != null) + { + reviews = reviews.Where(x => x.ReviewType?.EqualsInvariant(type) ?? true).ToList(); + } + return reviews; + }); + + Field("description", + arguments: new QueryArguments(new QueryArgument { Name = "type" }), + resolve: context => + { + var reviews = context.Source.IndexedProduct.Reviews; + var type = context.GetArgumentOrValue("type"); + var cultureName = context.GetArgumentOrValue("cultureName"); + + if (!reviews.IsNullOrEmpty()) + { + return reviews.Where(x => x.ReviewType.EqualsInvariant(type ?? "FullReview")).FirstBestMatchForLanguage(cultureName) as EditorialReview + ?? reviews.FirstBestMatchForLanguage(cultureName) as EditorialReview; + } + + return null; + }); + + FieldAsync( + "category", + resolve: async context => + { + var categoryId = context.Source.IndexedProduct.CategoryId; + + var loadCategoryQuery = context.GetCatalogQuery(); + loadCategoryQuery.ObjectIds = new[] { categoryId }; + loadCategoryQuery.IncludeFields = context.SubFields.Values.GetAllNodesPaths(context).ToArray(); + + var response = await mediator.Send(loadCategoryQuery); + + return response.Categories.FirstOrDefault(); + }); + + Field( + "imgSrc", + description: "The product main image URL.", + resolve: context => context.Source.IndexedProduct.ImgSrc); + + Field(d => d.IndexedProduct.OuterId, nullable: true).Description("The outer identifier"); + + Field(d => d.IndexedProduct.Gtin, nullable: true).Description("Global Trade Item Number"); + + Field( + "brandName", + description: "Get brandName for product.", + resolve: context => + { + var brandName = context.Source.IndexedProduct.Properties + ?.FirstOrDefault(x => x.Name.EqualsInvariant("Brand")) + ?.Values + ?.FirstOrDefault(x => x.Value != null) + ?.Value; + + return brandName?.ToString(); + }); + + FieldAsync( + "masterVariation", + resolve: async context => + { + if (string.IsNullOrEmpty(context.Source.IndexedProduct.MainProductId)) + { + return null; + } + + var query = context.GetCatalogQuery(); + query.ObjectIds = new[] { context.Source.IndexedProduct.MainProductId }; + query.IncludeFields = context.SubFields.Values.GetAllNodesPaths(context).ToArray(); + + var response = await mediator.Send(query); + + return response.Products.Select(expProduct => new ExpVariation(expProduct)).FirstOrDefault(); + }); + + FieldAsync>>>( + "variations", + resolve: async context => await ResolveVariationsFieldAsync(mediator, context)); + + Field>( + "hasVariations", + resolve: context => + { + var result = context.Source.IndexedVariationIds?.Any() ?? false; + return result; + }); + + Field( + GraphTypeExtenstionHelper.GetActualComplexType>(), + "availabilityData", + "Product availability data", + resolve: context => AbstractTypeFactory.TryCreateInstance().FromProduct(context.Source)); + + Field>>>( + "images", + "Product images", + resolve: context => + { + var images = context.Source.IndexedProduct.Images ?? Array.Empty(); + + return context.GetValue("cultureName") switch + { + // Get images with null or current cultureName value if cultureName is passed + string languageCode => images.Where(x => string.IsNullOrEmpty(x.LanguageCode) || x.LanguageCode.EqualsInvariant(languageCode)).ToList(), + + // CultureName is null + _ => images + }; + }); + + Field>( + "price", + "Product price", + resolve: context => context.Source.AllPrices.FirstOrDefault() ?? new ProductPrice(context.GetCurrencyByCode(context.GetValue("currencyCode")))); + + Field>>>( + "prices", + "Product prices", + resolve: context => context.Source.AllPrices); + + Field( + "minVariationPrice", + "Minimim product variation price", + resolve: context => context.Source.MinVariationPrice); + + ExtendableField>>>("properties", + arguments: new QueryArguments(new QueryArgument> { Name = "names" }), + resolve: context => + { + var names = context.GetArgument("names"); + var cultureName = context.GetValue("cultureName"); + var result = context.Source.IndexedProduct.Properties.ExpandOrderedByValues(cultureName); + if (!names.IsNullOrEmpty()) + { + result = result.Where(x => names.Contains(x.Name, StringComparer.InvariantCultureIgnoreCase)).ToList(); + } + return result; + }); + + ExtendableField>>>("keyProperties", + arguments: new QueryArguments(new QueryArgument { Name = "take" }), + resolve: context => + { + var take = context.GetArgument("take"); + var cultureName = context.GetValue("cultureName"); + + var result = context.Source.IndexedProduct.Properties.ExpandKeyPropertiesByValues(cultureName, take); + + return result; + }); + + Field>>>( + "assets", + "Assets", + resolve: context => + { + var assets = context.Source.IndexedProduct.Assets ?? Array.Empty(); + + return context.GetValue("cultureName") switch + { + // Get assets with null or current cultureName value if cultureName is passed + string languageCode => assets.Where(x => string.IsNullOrEmpty(x.LanguageCode) || x.LanguageCode.EqualsInvariant(languageCode)).ToList(), + + // CultureName is null + _ => assets + }; + }); + + Field>>>("outlines", "Outlines", resolve: context => context.Source.IndexedProduct.Outlines ?? Array.Empty());//.RootAlias("__object.outlines"); + + Field>>>("breadcrumbs", "Breadcrumbs", resolve: context => + { + var store = context.GetArgumentOrValue("store"); + var cultureName = context.GetValue("cultureName"); + + return context.Source.IndexedProduct.Outlines.GetBreadcrumbsFromOutLine(store, cultureName); + }); + + Field( + GraphTypeExtenstionHelper.GetActualType(), + "vendor", + "Product vendor", + resolve: context => context.Source.Vendor); + + Field(x => x.InWishlist, nullable: false).Description("Product added at least in one wishlist"); + + Field(x => x.WishlistIds, nullable: false).Description("List of wishlist ID with this product"); + + Connection() + .Name("associations") + .Argument("query", "the search phrase") + .Argument("group", "association group (Accessories, RelatedItem)") + .PageSize(20) + .ResolveAsync(async context => await ResolveAssociationConnectionAsync(mediator, context)); + + + Connection() + .Name("videos") + .PageSize(20) + .ResolveAsync(async context => await ResolveVideosConnectionAsync(mediator, context)); + } + + protected virtual async Task ResolveVariationsFieldAsync(IMediator mediator, IResolveFieldContext context) + { + if (context.Source.IndexedVariationIds.IsNullOrEmpty()) + { + return new List(); + } + + var query = context.GetCatalogQuery(); + query.ObjectIds = context.Source.IndexedVariationIds; + query.IncludeFields = context.SubFields.Values.GetAllNodesPaths(context).ToArray(); + + var response = await mediator.Send(query); + + return response.Products.Where(x => x.IndexedProduct?.IsActive == true).Select(expProduct => new ExpVariation(expProduct)); + } + + private static async Task ResolveAssociationConnectionAsync(IMediator mediator, IResolveConnectionContext context) + { + var first = context.First; + + int.TryParse(context.After, out var skip); + + var query = new SearchProductAssociationsQuery + { + Skip = skip, + Take = first ?? context.PageSize ?? 10, + + Keyword = context.GetArgument("query"), + Group = context.GetArgument("group"), + ObjectIds = new[] { context.Source.IndexedProduct.Id } + }; + + var response = await mediator.Send(query); + + return new PagedConnection(response.Result.Results, query.Skip, query.Take, response.Result.TotalCount); + } + + private static async Task ResolveVideosConnectionAsync(IMediator mediator, IResolveConnectionContext context) + { + var first = context.First; + + int.TryParse(context.After, out var skip); + + var query = new SearchVideoQuery + { + Skip = skip, + Take = first ?? context.PageSize ?? 10, + OwnerType = "Product", + OwnerId = context.Source.Id, + CultureName = context.GetArgumentOrValue("cultureName") + }; + + var response = await mediator.Send(query); + + return new PagedConnection