From 921b5219b36be2abcbfe2592b7b6355f27e4954d Mon Sep 17 00:00:00 2001 From: Alexander Kovalenko Date: Mon, 10 Jun 2024 17:54:20 +0300 Subject: [PATCH 1/9] Added mocks. --- apps/storefront/src/app.ts | 9 +- .../domain/cart/services/src/adapter/index.ts | 4 +- .../services/src/adapter/mock/cart-totals.ts | 72 ++ .../cart/services/src/adapter/mock/index.ts | 3 + .../src/adapter/mock/mock-cart.adapter.ts | 250 +++++++ .../services/src/adapter/mock/mock-cart.ts | 241 +++++++ .../spryker-glue/glue-cart.adapter.spec.ts | 628 ++++++++++++++++++ .../adapter/spryker-glue/glue-cart.adapter.ts | 328 +++++++++ .../src/adapter/spryker-glue/index.ts | 2 + .../normalizers/cart.normalizer.spec.ts | 62 ++ .../normalizers/cart.normalizer.ts | 26 + .../normalizers/carts.normalizer.spec.ts | 64 ++ .../normalizers/carts.normalizer.ts | 15 + .../adapter/spryker-glue/normalizers/index.ts | 3 + .../adapter/spryker-glue/normalizers/model.ts | 19 + libs/domain/cart/src/feature.ts | 26 +- libs/domain/cart/src/index.ts | 1 - libs/domain/cart/src/services-reexports.ts | 2 +- .../cart/src/services/cart.providers.ts | 179 ++--- .../checkout/services/src/adapter/index.ts | 4 +- .../services/src/adapter/mock/index.ts | 2 + .../src/adapter/mock/mock-checkout.adapter.ts | 33 + .../src/adapter/mock/mock-checkout.ts | 308 +++++++++ .../glue-checkout.adapter.spec.ts | 173 +++++ .../spryker-glue/glue-checkout.adapter.ts | 81 +++ .../src/adapter/spryker-glue/index.ts | 3 + .../checkout-response.normalizer.spec.ts | 15 + .../checkout-response.normalizer.ts | 7 + .../normalizers/checkout.normalizer.spec.ts | 173 +++++ .../normalizers/checkout.normalizer.ts | 56 ++ .../adapter/spryker-glue/normalizers/index.ts | 5 + .../adapter/spryker-glue/normalizers/model.ts | 27 + .../normalizers/payments.normalizer.spec.ts | 30 + .../normalizers/payments.normalizer.ts | 30 + .../normalizers/shipments.normalizer.spec.ts | 99 +++ .../normalizers/shipments.normalizer.ts | 35 + .../checkout-data.serializer.spec.ts | 24 + .../serializers/checkout-data.serializer.ts | 11 + .../serializers/checkout.serializer.spec.ts | 12 + .../serializers/checkout.serializer.ts | 25 + .../adapter/spryker-glue/serializers/index.ts | 2 + .../domain/checkout/src/checkout.providers.ts | 2 +- .../domain/checkout/src/services-reexports.ts | 2 +- libs/domain/product/src/feature.ts | 16 +- .../src/mocks/src/mock-product.providers.ts | 6 +- .../product-list/mock-product-list.adapter.ts | 2 +- .../mock-product-relations-list.service.ts | 2 +- .../product/src/services/adapter/index.ts | 6 +- .../src/services/adapter/mock/index.ts | 2 + .../adapter/mock/mock-category.adapter.ts | 29 + .../services/adapter/mock/mock-category.ts | 11 + .../adapter/mock/mock-product.adapter.ts | 18 + .../src/services/adapter/mock/mock-product.ts | 324 +++++++++ .../adapter/mock/product-list/index.ts | 3 + .../mock/product-list/mock-facet.generator.ts | 65 ++ .../product-list/mock-product-list.adapter.ts | 89 +++ .../mock-product-list.generator.ts | 24 + .../adapter/mock/product-relations/index.ts | 1 + .../mock-product-relations-list.adapter.ts | 19 + .../mock-product-relations-list.service.ts | 19 + .../spryker-glue/glue-product.adapter.spec.ts | 166 +++++ .../spryker-glue/glue-product.adapter.ts | 78 +++ .../services/adapter/spryker-glue/index.ts | 4 + .../availability.normalizer.spec.ts | 23 + .../availability/availability.normalizer.ts | 26 + .../normalizers/availability/index.ts | 2 + .../normalizers/availability/model.ts | 8 + .../category-id.normalizer.spec.ts | 20 + .../category-id/category-id.normalizer.ts | 30 + .../normalizers/category-id/index.ts | 2 + .../normalizers/category-id/model.ts | 1 + .../concrete-products.normalizer.spec.ts | 64 ++ .../concrete-products.normalizer.ts | 44 ++ .../normalizers/concrete-products/index.ts | 2 + .../normalizers/concrete-products/model.ts | 10 + .../facet-category.normalizer.spec.ts | 67 ++ .../facet-category.normalizer.ts | 50 ++ .../normalizers/facet-category/index.ts | 1 + .../facet-range.normalizer.spec.ts | 51 ++ .../facet-range/facet-range.normalizer.ts | 45 ++ .../normalizers/facet-range/index.ts | 1 + .../facet-rating/facet-rating.normalizer.ts | 44 ++ .../normalizers/facet-rating/index.ts | 1 + .../facet/facet.normalizer.spec.ts | 52 ++ .../normalizers/facet/facet.normalizer.ts | 88 +++ .../spryker-glue/normalizers/facet/index.ts | 1 + .../adapter/spryker-glue/normalizers/index.ts | 13 + .../spryker-glue/normalizers/labels/index.ts | 1 + .../labels/labels.normalizer.spec.ts | 55 ++ .../normalizers/labels/labels.normalizer.ts | 35 + .../spryker-glue/normalizers/media/index.ts | 2 + .../media/media-set.normalizer.spec.ts | 73 ++ .../normalizers/media/media-set.normalizer.ts | 27 + .../media/media.normalizer.spec.ts | 46 ++ .../normalizers/media/media.normalizer.ts | 30 + .../adapter/spryker-glue/normalizers/model.ts | 44 ++ .../normalizers/pagination/index.ts | 1 + .../pagination/pagination.normalizer.spec.ts | 27 + .../pagination/pagination.normalizer.ts | 23 + .../spryker-glue/normalizers/price/index.ts | 1 + .../price/price.normalizer.spec.ts | 50 ++ .../normalizers/price/price.normalizer.ts | 41 ++ .../normalizers/product-list/index.ts | 2 + .../normalizers/product-list/model.ts | 13 + .../product-list.normalizer.spec.ts | 30 + .../product-list/product-list.normalizer.ts | 121 ++++ .../spryker-glue/normalizers/product/index.ts | 2 + .../spryker-glue/normalizers/product/model.ts | 13 + .../product/product.normalizer.spec.ts | 194 ++++++ .../normalizers/product/product.normalizer.ts | 171 +++++ .../normalizers/relations-list/index.ts | 1 + .../relations-list.normalizer.spec.ts | 33 + .../relations-list.normalizer.ts | 30 + .../spryker-glue/normalizers/sort/index.ts | 1 + .../normalizers/sort/sort.normalizer.spec.ts | 60 ++ .../normalizers/sort/sort.normalizer.ts | 28 + .../adapter/spryker-glue/product-includes.ts | 22 + .../adapter/spryker-glue/product.adapter.ts | 15 + .../glue-product-category.adapter.spec.ts | 98 +++ .../adapter/glue-product-category.adapter.ts | 48 ++ .../src/services/category/adapter/index.ts | 2 +- .../services/default-product.service.spec.ts | 2 +- libs/domain/product/src/services/index.ts | 3 +- .../adapter/glue-product-list.adapter.spec.ts | 149 +++++ .../list/adapter/glue-product-list.adapter.ts | 113 ++++ .../src/services/list/adapter/index.ts | 2 +- .../product/src/services/product.providers.ts | 152 +++-- ...lue-product-relations-list.adapter.spec.ts | 101 +++ .../glue-product-relations-list.adapter.ts | 32 + .../src/services/related/adapter/index.ts | 2 +- .../product/src/services/state/queries.ts | 2 +- libs/domain/search/src/feature.ts | 16 +- .../src/mocks/src/mock-search.providers.ts | 8 +- .../search/src/services/adapter/index.ts | 6 +- .../search/src/services/adapter/mock/index.ts | 3 + .../services/adapter/mock/mock-completion.ts | 15 + .../adapter/mock/mock-suggestion.adapter.ts | 17 + .../adapter/mock/mock-suggestion.generator.ts | 80 +++ .../content-suggestion.adapter.spec.ts | 74 +++ .../content-suggestion.adapter.ts | 34 + .../glue-suggestion.adapter.spec.ts | 125 ++++ .../spryker-glue/glue-suggestion.adapter.ts | 83 +++ .../services/adapter/spryker-glue/index.ts | 4 + .../adapter/spryker-glue/normalizers/index.ts | 1 + .../normalizers/suggestion/index.ts | 2 + .../normalizers/suggestion/model.ts | 13 + .../suggestion/suggestion.normalizer.spec.ts | 72 ++ .../suggestion/suggestion.normalizer.ts | 67 ++ .../spryker-glue/suggestion.adapter.ts | 27 + .../products-experience-data.revealer.spec.ts | 2 +- .../products-experience-data.revealer.ts | 2 +- ...uggestion-experience-data.revealer.spec.ts | 2 +- .../search/src/services/search.providers.ts | 29 +- .../default-suggestion.service.spec.ts | 4 +- .../suggestion/default-suggestion.service.ts | 2 +- .../default-suggestion-renderer.service.ts | 2 +- .../renderer/suggestion-renderer.service.ts | 2 +- libs/domain/site/src/feature.ts | 16 +- .../domain/site/src/services/adapter/index.ts | 5 +- .../site/src/services/adapter/mock/index.ts | 2 + .../adapter/mock/mock-store.adapter.ts | 9 + .../src/services/adapter/mock/mock-store.ts | 47 ++ .../spryker-glue/glue-store.adapter.ts | 20 + .../adapter/spryker-glue/glue.adapter.spec.ts | 78 +++ .../services/adapter/spryker-glue/index.ts | 3 + .../adapter/spryker-glue/normalizers/index.ts | 2 + .../adapter/spryker-glue/normalizers/model.ts | 3 + .../spryker-glue/normalizers/store/index.ts | 1 + .../normalizers/store/store.normalizer.ts | 34 + .../adapter/spryker-glue/store.adapter.ts | 14 + .../site/src/services/site.providers.ts | 37 +- .../store/default-store.service.spec.ts | 2 +- .../services/store/default-store.service.ts | 2 +- libs/template/presets/storefront/app.ts | 41 +- 174 files changed, 7292 insertions(+), 237 deletions(-) create mode 100644 libs/domain/cart/services/src/adapter/mock/cart-totals.ts create mode 100644 libs/domain/cart/services/src/adapter/mock/index.ts create mode 100644 libs/domain/cart/services/src/adapter/mock/mock-cart.adapter.ts create mode 100644 libs/domain/cart/services/src/adapter/mock/mock-cart.ts create mode 100644 libs/domain/cart/services/src/adapter/spryker-glue/glue-cart.adapter.spec.ts create mode 100644 libs/domain/cart/services/src/adapter/spryker-glue/glue-cart.adapter.ts create mode 100644 libs/domain/cart/services/src/adapter/spryker-glue/index.ts create mode 100644 libs/domain/cart/services/src/adapter/spryker-glue/normalizers/cart.normalizer.spec.ts create mode 100644 libs/domain/cart/services/src/adapter/spryker-glue/normalizers/cart.normalizer.ts create mode 100644 libs/domain/cart/services/src/adapter/spryker-glue/normalizers/carts.normalizer.spec.ts create mode 100644 libs/domain/cart/services/src/adapter/spryker-glue/normalizers/carts.normalizer.ts create mode 100644 libs/domain/cart/services/src/adapter/spryker-glue/normalizers/index.ts create mode 100644 libs/domain/cart/services/src/adapter/spryker-glue/normalizers/model.ts create mode 100644 libs/domain/checkout/services/src/adapter/mock/index.ts create mode 100644 libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts create mode 100644 libs/domain/checkout/services/src/adapter/mock/mock-checkout.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/glue-checkout.adapter.spec.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/glue-checkout.adapter.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/index.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout-response.normalizer.spec.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout-response.normalizer.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout.normalizer.spec.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout.normalizer.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/index.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/model.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/payments.normalizer.spec.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/payments.normalizer.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/shipments.normalizer.spec.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/shipments.normalizer.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout-data.serializer.spec.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout-data.serializer.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout.serializer.spec.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout.serializer.ts create mode 100644 libs/domain/checkout/services/src/adapter/spryker-glue/serializers/index.ts create mode 100644 libs/domain/product/src/services/adapter/mock/index.ts create mode 100644 libs/domain/product/src/services/adapter/mock/mock-category.adapter.ts create mode 100644 libs/domain/product/src/services/adapter/mock/mock-category.ts create mode 100644 libs/domain/product/src/services/adapter/mock/mock-product.adapter.ts create mode 100644 libs/domain/product/src/services/adapter/mock/mock-product.ts create mode 100644 libs/domain/product/src/services/adapter/mock/product-list/index.ts create mode 100644 libs/domain/product/src/services/adapter/mock/product-list/mock-facet.generator.ts create mode 100644 libs/domain/product/src/services/adapter/mock/product-list/mock-product-list.adapter.ts create mode 100644 libs/domain/product/src/services/adapter/mock/product-list/mock-product-list.generator.ts create mode 100644 libs/domain/product/src/services/adapter/mock/product-relations/index.ts create mode 100644 libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.adapter.ts create mode 100644 libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.service.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/availability.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/availability.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/model.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/category-id.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/category-id.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/model.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/concrete-products.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/concrete-products.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/model.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/facet-category.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/facet-category.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/facet-range.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/facet-range.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-rating/facet-rating.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-rating/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/facet.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/facet.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/labels.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/labels.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media-set.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media-set.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/model.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/pagination.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/pagination.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/price.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/price.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/model.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/product-list.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/product-list.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/model.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/product.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/product.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/relations-list.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/relations-list.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/index.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/sort.normalizer.spec.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/sort.normalizer.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/product-includes.ts create mode 100644 libs/domain/product/src/services/adapter/spryker-glue/product.adapter.ts create mode 100644 libs/domain/product/src/services/category/adapter/glue-product-category.adapter.spec.ts create mode 100644 libs/domain/product/src/services/category/adapter/glue-product-category.adapter.ts create mode 100644 libs/domain/product/src/services/list/adapter/glue-product-list.adapter.spec.ts create mode 100644 libs/domain/product/src/services/list/adapter/glue-product-list.adapter.ts create mode 100644 libs/domain/product/src/services/related/adapter/glue-product-relations-list.adapter.spec.ts create mode 100644 libs/domain/product/src/services/related/adapter/glue-product-relations-list.adapter.ts create mode 100644 libs/domain/search/src/services/adapter/mock/index.ts create mode 100644 libs/domain/search/src/services/adapter/mock/mock-completion.ts create mode 100644 libs/domain/search/src/services/adapter/mock/mock-suggestion.adapter.ts create mode 100644 libs/domain/search/src/services/adapter/mock/mock-suggestion.generator.ts create mode 100644 libs/domain/search/src/services/adapter/spryker-glue/content-suggestion.adapter.spec.ts create mode 100644 libs/domain/search/src/services/adapter/spryker-glue/content-suggestion.adapter.ts create mode 100644 libs/domain/search/src/services/adapter/spryker-glue/glue-suggestion.adapter.spec.ts create mode 100644 libs/domain/search/src/services/adapter/spryker-glue/glue-suggestion.adapter.ts create mode 100644 libs/domain/search/src/services/adapter/spryker-glue/index.ts create mode 100644 libs/domain/search/src/services/adapter/spryker-glue/normalizers/index.ts create mode 100644 libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/index.ts create mode 100644 libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/model.ts create mode 100644 libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/suggestion.normalizer.spec.ts create mode 100644 libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/suggestion.normalizer.ts create mode 100644 libs/domain/search/src/services/adapter/spryker-glue/suggestion.adapter.ts create mode 100644 libs/domain/site/src/services/adapter/mock/index.ts create mode 100644 libs/domain/site/src/services/adapter/mock/mock-store.adapter.ts create mode 100644 libs/domain/site/src/services/adapter/mock/mock-store.ts create mode 100644 libs/domain/site/src/services/adapter/spryker-glue/glue-store.adapter.ts create mode 100644 libs/domain/site/src/services/adapter/spryker-glue/glue.adapter.spec.ts create mode 100644 libs/domain/site/src/services/adapter/spryker-glue/index.ts create mode 100644 libs/domain/site/src/services/adapter/spryker-glue/normalizers/index.ts create mode 100644 libs/domain/site/src/services/adapter/spryker-glue/normalizers/model.ts create mode 100644 libs/domain/site/src/services/adapter/spryker-glue/normalizers/store/index.ts create mode 100644 libs/domain/site/src/services/adapter/spryker-glue/normalizers/store/store.normalizer.ts create mode 100644 libs/domain/site/src/services/adapter/spryker-glue/store.adapter.ts diff --git a/apps/storefront/src/app.ts b/apps/storefront/src/app.ts index 8930a1c74..e81f9e293 100644 --- a/apps/storefront/src/app.ts +++ b/apps/storefront/src/app.ts @@ -3,7 +3,10 @@ import { multiCartFeature } from '@spryker-oryx/cart'; import { labsFeatures } from '@spryker-oryx/labs'; import { merchantFeature } from '@spryker-oryx/merchant'; import { b2bStorefrontFeatures } from '@spryker-oryx/presets/b2b-storefront'; -import { storefrontFeatures } from '@spryker-oryx/presets/storefront'; +import { + storefrontFeatures, + storefrontMockFeatures, +} from '@spryker-oryx/presets/storefront'; import { storefrontTheme } from '@spryker-oryx/themes'; const env = import.meta.env; @@ -19,8 +22,10 @@ const features = [ ...(env.ORYX_LABS ? labsFeatures : []), ]; +const mockFeatures = [...storefrontMockFeatures]; + export const app = appBuilder() - .withFeature(features) + .withFeature(mockFeatures) .withTheme([storefrontTheme]) .withEnvironment(env) .create(); diff --git a/libs/domain/cart/services/src/adapter/index.ts b/libs/domain/cart/services/src/adapter/index.ts index 66a32ddab..75f2d6239 100644 --- a/libs/domain/cart/services/src/adapter/index.ts +++ b/libs/domain/cart/services/src/adapter/index.ts @@ -1,2 +1,2 @@ -export * from './default-cart.adapter'; -export * from './normalizers'; +export * from './mock'; +export * from './spryker-glue'; diff --git a/libs/domain/cart/services/src/adapter/mock/cart-totals.ts b/libs/domain/cart/services/src/adapter/mock/cart-totals.ts new file mode 100644 index 000000000..3e174c474 --- /dev/null +++ b/libs/domain/cart/services/src/adapter/mock/cart-totals.ts @@ -0,0 +1,72 @@ +import { NormalizedTotals, TotalsResolver } from '@spryker-oryx/cart'; +import { Observable, of } from 'rxjs'; + +const types = [ + 'subtotal', + 'discount', + 'tax', + 'expense', + 'delivery', + 'total', +].map((type) => ({ + id: `only ${type}`, + components: [{ type: `oryx-cart-totals-${type}` }], +})); + +const discountVariations = ['expanded', 'collapsed', 'inline', 'none'].map( + (type) => ({ + id: `discount (${type})`, + components: [ + { type: 'oryx-cart-totals-subtotal' }, + { + type: `oryx-cart-totals-discount`, + options: { + data: { discountRowsAppearance: type }, + }, + }, + { type: 'oryx-cart-totals-total' }, + ], + }) +); + +export const mockedTotals = (totals: NormalizedTotals): any => { + return class implements TotalsResolver { + getTotals(): Observable { + return of(totals); + } + }; +}; + +export const cartTotalsStaticData = [ + { + id: 'all', + components: [ + { type: 'oryx-cart-totals-subtotal' }, + { type: 'oryx-cart-totals-discount' }, + { type: 'oryx-cart-totals-tax' }, + { type: 'oryx-cart-totals-delivery' }, + { type: 'oryx-cart-totals-total' }, + ], + }, + { + id: 'small', + components: [ + { type: 'oryx-cart-totals-subtotal' }, + { type: 'oryx-cart-totals-discount' }, + { type: 'oryx-cart-totals-total' }, + ], + }, + ...types, + ...discountVariations, + { + id: 'small (without message)', + components: [ + { type: 'oryx-cart-totals-subtotal' }, + { type: 'oryx-cart-totals-discount' }, + { + type: 'oryx-cart-totals-total', + options: { data: { enableTaxMessage: false } }, + }, + ], + }, +]; diff --git a/libs/domain/cart/services/src/adapter/mock/index.ts b/libs/domain/cart/services/src/adapter/mock/index.ts new file mode 100644 index 000000000..ff4269cf6 --- /dev/null +++ b/libs/domain/cart/services/src/adapter/mock/index.ts @@ -0,0 +1,3 @@ +export * from './cart-totals'; +export * from './mock-cart'; +export * from './mock-cart.adapter'; diff --git a/libs/domain/cart/services/src/adapter/mock/mock-cart.adapter.ts b/libs/domain/cart/services/src/adapter/mock/mock-cart.adapter.ts new file mode 100644 index 000000000..2a1b97192 --- /dev/null +++ b/libs/domain/cart/services/src/adapter/mock/mock-cart.adapter.ts @@ -0,0 +1,250 @@ +import { + AddCartEntryQualifier, + Cart, + CartAdapter, + CartCalculations, + CartEntry, + CartEntryQualifier, + CartQualifier, + ProductOption, + UpdateCartEntryQualifier, +} from '@spryker-oryx/cart'; +import { MockProductService } from '@spryker-oryx/product/mocks'; +import { Observable, delay, mapTo, of, take, tap, timer } from 'rxjs'; +import { + mockCartEntry, + mockCartLarge, + mockCartWithDiscount, + mockCartWithExpense, + mockCartWithMultipleDiscount, + mockCartWithMultipleProducts, + mockCartWithTax, + mockDefaultCart, + mockEmptyCart, + mockNetCart, +} from './mock-cart'; + +export class MockCartAdapter implements Partial { + private carts: Cart[] = [ + mockDefaultCart, + mockNetCart, + mockEmptyCart as Cart, + mockCartWithExpense, + mockCartWithMultipleDiscount, + mockCartWithDiscount, + mockCartLarge, + mockCartWithTax, + mockCartWithMultipleProducts, + ]; + + private responseDelay = 300; + + private selectedProductOptions = [ + { + optionGroupName: 'Three (3) year limited warranty', + sku: 'OP_3_year_warranty', + optionName: 'Three (3) year limited warranty', + price: 2000, + }, + { + optionGroupName: 'Two (2) year insurance coverage', + sku: 'OP_insurance', + optionName: 'Two (2) year insurance coverage', + price: 10000, + }, + { + optionGroupName: 'Gift wrapping', + sku: 'OP_gift_wrapping', + optionName: 'Gift wrapping', + price: 500, + }, + ]; + + getAll(): Observable { + return of(this.carts).pipe(take(1)); + } + + get(data: CartQualifier): Observable { + const cart = this.findCart(data.cartId!); + + return of(cart).pipe(delay(this.responseDelay)); + } + + addEntry(data: AddCartEntryQualifier): Observable { + const { cart, products, productIndex } = this.getInfo( + data.cartId!, + data.sku! + ); + + if (products.length && productIndex >= 0) { + const productFromCart = products[productIndex]; + const quantity = Number(productFromCart.quantity) + Number(data.quantity); + + products.splice(productIndex, 1, { + ...productFromCart, + quantity, + calculations: this.createCalculations( + data.sku!, + quantity, + this.selectedProductOptions + ), + }); + } else { + products.push({ + ...mockCartEntry, + sku: data.sku!, + groupKey: data.sku!, + quantity: data.quantity ?? 1, + selectedProductOptions: this.selectedProductOptions, + calculations: this.createCalculations( + data.sku!, + data.quantity ?? 1, + this.selectedProductOptions + ), + }); + } + + const mappedCart = { + ...cart, + totals: { + ...cart.totals, + grandTotal: this.calculateCartTotal(products), + }, + products, + }; + + return this.applyCartChanges(mappedCart); + } + + updateEntry(data: UpdateCartEntryQualifier): Observable { + const { cart, products, productIndex } = this.getInfo( + data.cartId!, + data.groupKey! + ); + + products.splice(productIndex, 1, { + ...products[productIndex], + quantity: data.quantity, + calculations: this.createCalculations( + products[productIndex].sku, + data.quantity, + this.selectedProductOptions + ), + }); + + const mappedCart = { + ...cart, + totals: { + ...cart.totals, + grandTotal: this.calculateCartTotal(products), + }, + products, + }; + + return this.applyCartChanges(mappedCart); + } + + deleteEntry(data: CartEntryQualifier): Observable { + return timer(this.responseDelay).pipe( + tap(() => { + const { cart, products, productIndex } = this.getInfo( + data.cartId!, + data.groupKey! + ); + + products.splice(productIndex, 1); + + const cartIndex = this.carts.findIndex((item) => item.id === cart.id); + + this.carts.splice(cartIndex, 1, { + ...cart, + products, + }); + }), + mapTo(null) + ); + } + + update(): Observable { + return of(); + } + + create(): Observable { + return of(); + } + + delete(): Observable { + return of(); + } + + protected findCart(id: string): Cart { + const cart = this.carts.find((item) => item.id === id); + + if (!cart) { + throw new Error(`Cart with id ${id} is not exist`); + } + + return cart; + } + + protected getInfo( + id: string, + checkValue: string + ): { + cart: Cart; + products: CartEntry[]; + productIndex: number; + } { + const cart = this.findCart(id); + const products = cart.products ? [...cart.products] : []; + const productIndex = products.findIndex( + (product) => product.sku === checkValue || product.groupKey === checkValue + ); + + return { cart, products, productIndex }; + } + + protected applyCartChanges(mappedCart: Cart): Observable { + const cartIndex = this.carts.findIndex((cart) => cart.id === mappedCart.id); + + this.carts.splice(cartIndex, 1, mappedCart); + + return of(mappedCart).pipe(delay(this.responseDelay)); + } + + protected calculateCartTotal(products: CartEntry[]): number { + let total = 0; + + for (let i = 0; i < products.length; i++) { + total = + total + + Number(products[i].quantity) * + (products[i]?.calculations?.unitPrice || + Math.floor(Math.random() * 10 + 1)); + } + + return total; + } + + protected createCalculations( + sku: string, + quantity: number, + options: ProductOption[] + ): CartCalculations { + const unitPrice = + MockProductService.mockProducts.find((product) => product.sku === sku) + ?.price?.defaultPrice?.value ?? 0; + const sumPrice = unitPrice * quantity; + const sumPriceToPayAggregation = + options.reduce((sum, { price = 0 }) => sum + price, 0) + sumPrice; + + return { + unitPrice, + sumPrice, + unitGrossPrice: unitPrice, + sumGrossPrice: sumPrice, + sumPriceToPayAggregation, + sumSubtotalAggregation: sumPriceToPayAggregation, + }; + } +} diff --git a/libs/domain/cart/services/src/adapter/mock/mock-cart.ts b/libs/domain/cart/services/src/adapter/mock/mock-cart.ts new file mode 100644 index 000000000..da6429415 --- /dev/null +++ b/libs/domain/cart/services/src/adapter/mock/mock-cart.ts @@ -0,0 +1,241 @@ +import { + ApiCartModel, + Cart, + CartDiscount, + CartEntry, + CartTotals, + Coupon, + NormalizedTotals, + PriceMode, +} from '@spryker-oryx/cart'; + +export const mockCartEntry: CartEntry = { + sku: '1', + groupKey: '1', + abstractSku: 'abstractSku', + quantity: 1, + calculations: { + unitPrice: 100, + sumPrice: 200, + sumPriceToPayAggregation: 200, + }, +}; + +export const mockCartEntry2: CartEntry = { + sku: '2', + groupKey: '2', + abstractSku: 'abstractSku', + quantity: 1, + calculations: { + unitPrice: 595, + sumPrice: 595, + sumPriceToPayAggregation: 595, + }, +}; + +export const mockCartEntry3: CartEntry = { + sku: '3', + groupKey: '3', + abstractSku: 'abstractSku', + quantity: 7, + calculations: { + unitPrice: 195, + sumPrice: 1365, + sumPriceToPayAggregation: 1365, + }, +}; + +export const mockCartEntry4: CartEntry = { + sku: '4', + groupKey: '4', + abstractSku: 'abstractSku', + quantity: 150, + calculations: { + unitPrice: 1, + sumPrice: 150, + sumPriceToPayAggregation: 150, + }, +}; + +export const mockCartCoupon1: Coupon = { + id: '1', + code: 'couponCode_1', + amount: 100, + displayName: 'Coupon 1', + expirationDateTime: '2021-12-31T23:59:59+00:00', + discountType: 'percentage', +}; + +export const mockCartCoupon2: Coupon = { + id: '2', + code: 'couponCode_2', + amount: 200, + displayName: 'Coupon 2', + expirationDateTime: '2021-12-31T23:59:59+00:00', + discountType: 'percentage', +}; + +const mockFullCartTotals: CartTotals = { + subtotal: 161942, + grandTotal: 149867, + priceToPay: 150867, + taxTotal: 6386, + discountTotal: 12075, + expenseTotal: 1000, + shipmentTotal: 1000, +}; + +const mockCartTotals: CartTotals = { + subtotal: 161942, + grandTotal: 149867, + priceToPay: 150867, + taxTotal: 1000, + expenseTotal: 100, +}; + +const mockCartTotalsWithTax: CartTotals = { + ...mockCartTotals, + taxTotal: 6386, +}; + +const mockCartTotalsWithDiscount: CartTotals = { + ...mockCartTotals, + discountTotal: 12075, +}; + +const mockCartTotalsWithExpense: CartTotals = { + ...mockCartTotals, + expenseTotal: 1000, +}; + +const mockDiscounts: CartDiscount[] = [ + { + displayName: '€5 every tuesday and wednesday for buying 5 items', + amount: 12075, + }, +]; + +const mockMultipleDiscounts: CartDiscount[] = [ + { + displayName: '€5 every tuesday and wednesday for buying 5 items', + amount: 12075, + }, + { + displayName: 'Happy birthday!', + amount: 1000, + }, +]; +/** + * The base cart is in gross mode and contains a single product. + */ +export const mockBaseCart: Cart = { + id: 'cart', + name: 'Shopping cart', + isDefault: false, + totals: mockCartTotals, + priceMode: PriceMode.GrossMode, + products: [mockCartEntry], +}; + +export const mockDefaultCart: Cart = { + id: 'default', + name: 'Shopping cart', + isDefault: true, + totals: mockFullCartTotals, + priceMode: PriceMode.GrossMode, + products: [mockCartEntry], + coupons: [mockCartCoupon1], + discounts: mockDiscounts, +}; + +// TODO: the Cart model doesn't fit empty carts, either the model is wrong or +// the logic inside cart totals is expecting the wrong data +export const mockEmptyCart: Partial = { + id: 'empty', +}; + +export const mockNetCart: Cart = { + ...mockBaseCart, + id: 'net', + priceMode: PriceMode.NetMode, + totals: mockFullCartTotals, +}; + +export const mockCartWithTax: Cart = { + ...mockBaseCart, + id: 'tax', + totals: mockCartTotalsWithTax, +}; + +export const mockCartWithExpense: Cart = { + ...mockBaseCart, + id: 'expense', + totals: mockCartTotalsWithExpense, +}; + +export const mockCartWithDiscount: Cart = { + ...mockBaseCart, + id: 'discount', + totals: mockCartTotalsWithDiscount, + discounts: mockDiscounts, +}; + +export const mockCartWithMultipleDiscount: Cart = { + ...mockBaseCart, + id: 'discount-multi-rows', + totals: mockCartTotalsWithDiscount, + discounts: mockMultipleDiscounts, +}; + +export const mockCartWithoutDiscount: Cart = { + ...mockBaseCart, + id: 'discount-no-rows', + discounts: [], +}; + +export const mockCartWithMultipleProducts: Cart = { + ...mockBaseCart, + id: 'multiple', + products: [mockCartEntry, mockCartEntry2, mockCartEntry3], + coupons: [mockCartCoupon1, mockCartCoupon2], +}; + +export const mockCartLarge: Cart = { + ...mockBaseCart, + id: 'large', + products: [mockCartEntry, mockCartEntry2, mockCartEntry3, mockCartEntry4], +}; + +export const mockGetCartsResponse: ApiCartModel.Response = { + data: { + attributes: mockBaseCart, + }, + included: [ + { + type: ApiCartModel.Includes.Items, + id: 'entry', + attributes: mockCartEntry, + }, + ], +}; + +export const mockNormalizedCartTotals: NormalizedTotals = { + ...mockFullCartTotals, + discounts: [...mockMultipleDiscounts], + currency: 'EUR', + priceMode: PriceMode.GrossMode, +}; + +export const mockNormalizedCartTotalsSingleDiscount: NormalizedTotals = { + ...mockFullCartTotals, + discounts: [...mockDiscounts], + currency: 'EUR', + priceMode: PriceMode.GrossMode, + shipmentTotal: 0, +}; + +export const mockNormalizedCartTotalsNetMode: NormalizedTotals = { + ...mockNormalizedCartTotals, + priceMode: PriceMode.NetMode, + shipmentTotal: undefined, +}; diff --git a/libs/domain/cart/services/src/adapter/spryker-glue/glue-cart.adapter.spec.ts b/libs/domain/cart/services/src/adapter/spryker-glue/glue-cart.adapter.spec.ts new file mode 100644 index 000000000..2697acbdb --- /dev/null +++ b/libs/domain/cart/services/src/adapter/spryker-glue/glue-cart.adapter.spec.ts @@ -0,0 +1,628 @@ +import { AuthIdentity, IdentityService } from '@spryker-oryx/auth'; +import { + ApiCartModel, + CartAdapter, + CartNormalizer, + CartsNormalizer, +} from '@spryker-oryx/cart'; +import { mockGetCartsResponse } from '@spryker-oryx/cart/mocks'; +import { + FeatureOptionsService, + HttpService, + JsonAPITransformerService, +} from '@spryker-oryx/core'; +import { HttpTestService } from '@spryker-oryx/core/testing'; +import { createInjector, destroyInjector } from '@spryker-oryx/di'; +import { + CurrencyService, + PriceModeService, + Store, + StoreService, +} from '@spryker-oryx/site'; +import { Observable, of } from 'rxjs'; +import { GlueCartAdapter } from './glue-cart.adapter'; + +const mockApiUrl = 'mockApiUrl'; + +const mockAnonymousUser: AuthIdentity = { + userId: 'userId', + isAuthenticated: false, +}; + +const mockUser: AuthIdentity = { + userId: 'userId', + isAuthenticated: true, +}; + +const mockTransformer = { + do: vi.fn().mockReturnValue(() => of(null)), + transform: vi.fn(), +}; + +class MockIdentityService implements Partial { + get = vi + .fn<[], Observable>() + .mockReturnValue(of(mockAnonymousUser)); +} + +class MockStoreService implements Partial { + get = vi + .fn<[], Observable>() + .mockReturnValue(of({ id: 'DE' } as Store)); +} + +class MockCurrencyService implements Partial { + get = vi.fn<[], Observable>().mockReturnValue(of('EUR')); +} + +class MockPriceModeService implements Partial { + get = vi.fn<[], Observable>().mockReturnValue(of('GROSS_MODE')); +} + +class MockFeatureOptionsService implements Partial { + getFeatureOptions = vi.fn().mockReturnValue({ multi: true }); +} + +describe('DefaultCartAdapter', () => { + let adapter: CartAdapter; + let identity: MockIdentityService; + let http: HttpTestService; + let storeService: MockStoreService; + let currencyService: MockCurrencyService; + let priceModeService: MockPriceModeService; + let optionsService: MockFeatureOptionsService; + + function requestIncludes(isAuthenticated = false): string { + return `?include=${(isAuthenticated + ? [ApiCartModel.Includes.Items, ApiCartModel.Includes.Coupons] + : [ApiCartModel.Includes.GuestCartItems, ApiCartModel.Includes.Coupons] + ).join(',')}`; + } + + beforeEach(() => { + const testInjector = createInjector({ + providers: [ + { + provide: HttpService, + useClass: HttpTestService, + }, + { + provide: CartAdapter, + useClass: GlueCartAdapter, + }, + { + provide: 'SCOS_BASE_URL', + useValue: mockApiUrl, + }, + { + provide: JsonAPITransformerService, + useValue: mockTransformer, + }, + { + provide: IdentityService, + useClass: MockIdentityService, + }, + { + provide: StoreService, + useClass: MockStoreService, + }, + { + provide: CurrencyService, + useClass: MockCurrencyService, + }, + { + provide: PriceModeService, + useClass: MockPriceModeService, + }, + { + provide: FeatureOptionsService, + useClass: MockFeatureOptionsService, + }, + ], + }); + + adapter = testInjector.inject(CartAdapter); + http = testInjector.inject(HttpService); + identity = testInjector.inject(IdentityService); + storeService = testInjector.inject(StoreService); + currencyService = testInjector.inject(CurrencyService); + priceModeService = + testInjector.inject(PriceModeService); + optionsService = testInjector.inject( + FeatureOptionsService + ); + http.flush(mockGetCartsResponse); + }); + + afterEach(() => { + vi.clearAllMocks(); + destroyInjector(); + }); + + it('should be provided', () => { + expect(adapter).toBeInstanceOf(GlueCartAdapter); + }); + + describe('getAll should send `get` request', () => { + beforeEach(() => { + http.flush(mockGetCartsResponse); + }); + + describe('guest user', () => { + it('should build url', () => { + adapter.getAll().subscribe(); + + expect(http.url).toBe(`${mockApiUrl}/guest-carts${requestIncludes()}`); + }); + }); + + describe('loggedIn user', () => { + beforeEach(() => { + identity.get.mockReturnValue(of(mockUser)); + }); + + it('should build url', async () => { + adapter.getAll().subscribe(); + + expect(http.url).toBe( + `${mockApiUrl}/customers/${mockUser.userId}/carts${requestIncludes( + true + )}` + ); + }); + }); + + describe('data transforming', () => { + it('should call transformer with proper normalizer', () => { + adapter.getAll().subscribe(); + + expect(mockTransformer.do).toHaveBeenCalledWith(CartsNormalizer); + }); + + it('should return transformed data', () => { + const mockTransformerData = 'mockTransformerData'; + const callback = vi.fn(); + mockTransformer.do.mockReturnValue(() => of(mockTransformerData)); + + adapter.getAll().subscribe(callback); + + expect(callback).toHaveBeenCalledWith(mockTransformerData); + }); + }); + }); + + describe('get should send `get` request', () => { + const mockGuestGetCartQualifier = { + cartId: 'test', + }; + const mockGetCartQualifier = { + ...mockGuestGetCartQualifier, + }; + + describe('guest user', () => { + it('should build url', () => { + adapter.get(mockGuestGetCartQualifier).subscribe(); + + expect(http.url).toBe( + `${mockApiUrl}/guest-carts/${ + mockGuestGetCartQualifier.cartId + }${requestIncludes()}` + ); + }); + }); + + describe('loggedIn user', () => { + beforeEach(() => { + identity.get.mockReturnValue(of(mockUser)); + }); + + it('should build url', () => { + adapter.get(mockGetCartQualifier).subscribe(); + + expect(http.url).toBe( + `${mockApiUrl}/carts/${mockGetCartQualifier.cartId}${requestIncludes( + true + )}` + ); + }); + }); + + describe('data transforming', () => { + it('should call transformer data with proper normalizer', () => { + adapter.get(mockGuestGetCartQualifier).subscribe(); + + expect(mockTransformer.do).toHaveBeenCalledWith(CartNormalizer); + }); + + it('should return transformed data', () => { + const mockTransformerData = 'mockTransformerData'; + const callback = vi.fn(); + mockTransformer.do.mockReturnValue(() => of(mockTransformerData)); + + adapter.get(mockGuestGetCartQualifier).subscribe(callback); + + expect(callback).toHaveBeenCalledWith(mockTransformerData); + }); + }); + }); + + describe('addEntry should send `post` request', () => { + const mockGuestAddEntryQualifier = { + cartId: 'test', + sku: 'sku', + quantity: 1, + }; + const mockAddEntryQualifier = { + ...mockGuestAddEntryQualifier, + }; + + describe('guest user', () => { + it('should build url', () => { + adapter.addEntry(mockGuestAddEntryQualifier).subscribe(); + + expect(http.url).toBe( + `${mockApiUrl}/guest-carts/${ + mockGuestAddEntryQualifier.cartId + }/guest-cart-items${requestIncludes()}` + ); + }); + + it('should provide body', () => { + adapter.addEntry(mockGuestAddEntryQualifier).subscribe(); + + expect(http.body).toEqual({ + data: { + type: 'guest-cart-items', + attributes: { + sku: mockGuestAddEntryQualifier.sku, + quantity: mockGuestAddEntryQualifier.quantity, + }, + }, + }); + }); + }); + + describe('loggedIn user', () => { + beforeEach(() => { + identity.get.mockReturnValue(of(mockUser)); + }); + + it('should build url', () => { + adapter.addEntry(mockAddEntryQualifier).subscribe(); + + expect(http.url).toBe( + `${mockApiUrl}/carts/${ + mockAddEntryQualifier.cartId + }/items${requestIncludes(true)}` + ); + }); + + it('should provide body', () => { + adapter.addEntry(mockAddEntryQualifier).subscribe(); + + expect(http.body).toEqual({ + data: { + type: 'items', + attributes: { + sku: mockAddEntryQualifier.sku, + quantity: mockAddEntryQualifier.quantity, + }, + }, + }); + }); + }); + + describe('transforming data', () => { + it('should call transformer with proper normalizer', () => { + adapter.addEntry(mockGuestAddEntryQualifier).subscribe(); + + expect(mockTransformer.do).toHaveBeenCalledWith(CartNormalizer); + }); + + it('should return transformed data', () => { + const mockTransformerData = 'mockTransformerData'; + const callback = vi.fn(); + mockTransformer.do.mockReturnValue(() => of(mockTransformerData)); + + adapter.addEntry(mockGuestAddEntryQualifier).subscribe(callback); + + expect(callback).toHaveBeenCalledWith(mockTransformerData); + }); + }); + }); + + describe('updateEntry should send `patch` request', () => { + const mockGuestUpdateEntryQualifier = { + cartId: 'test', + groupKey: 'groupKey', + sku: 'sku', + quantity: 1, + }; + const mockUpdateEntryQualifier = { + ...mockGuestUpdateEntryQualifier, + }; + + describe('guest user', () => { + it('should build url', () => { + adapter.updateEntry(mockGuestUpdateEntryQualifier).subscribe(); + + expect(http.url).toBe( + `${mockApiUrl}/guest-carts/${ + mockGuestUpdateEntryQualifier.cartId + }/guest-cart-items/${ + mockGuestUpdateEntryQualifier.groupKey + }${requestIncludes()}` + ); + }); + + it('should provide body', () => { + adapter.updateEntry(mockGuestUpdateEntryQualifier).subscribe(); + + expect(http.body).toEqual({ + data: { + type: 'guest-cart-items', + attributes: { + quantity: mockGuestUpdateEntryQualifier.quantity, + }, + }, + }); + }); + }); + + describe('loggedIn user', () => { + beforeEach(() => { + identity.get.mockReturnValue(of(mockUser)); + }); + + it('should build url', () => { + adapter.updateEntry(mockUpdateEntryQualifier).subscribe(); + + expect(http.url).toBe( + `${mockApiUrl}/carts/${mockUpdateEntryQualifier.cartId}/items/${ + mockUpdateEntryQualifier.groupKey + }${requestIncludes(true)}` + ); + }); + + it('should provide body', () => { + adapter.updateEntry(mockUpdateEntryQualifier).subscribe(); + + expect(http.body).toEqual({ + data: { + type: 'items', + attributes: { quantity: mockUpdateEntryQualifier.quantity }, + }, + }); + }); + }); + + describe('transforming data', () => { + it('should call transformer with proper normalizer', () => { + adapter.addEntry(mockUpdateEntryQualifier).subscribe(); + + expect(mockTransformer.do).toHaveBeenCalledWith(CartNormalizer); + }); + + it('should return transformed data', () => { + const mockTransformerData = 'mockTransformerData'; + const callback = vi.fn(); + mockTransformer.do.mockReturnValue(() => of(mockTransformerData)); + + adapter.addEntry(mockUpdateEntryQualifier).subscribe(callback); + + expect(callback).toHaveBeenCalledWith(mockTransformerData); + }); + }); + }); + + describe('deleteEntry should send `delete` request', () => { + const mockGuestDeleteEntryQualifier = { + cartId: 'test', + groupKey: 'groupKey', + }; + const mockDeleteEntryQualifier = { + ...mockGuestDeleteEntryQualifier, + }; + + beforeEach(() => { + http.flush(null); + }); + + describe('guest user', () => { + it('should build url', () => { + adapter.deleteEntry(mockGuestDeleteEntryQualifier).subscribe(); + + expect(http.url).toBe( + `${mockApiUrl}/guest-carts/${ + mockGuestDeleteEntryQualifier.cartId + }/guest-cart-items/${ + mockGuestDeleteEntryQualifier.groupKey + }${requestIncludes()}` + ); + }); + }); + + describe('loggedIn user', () => { + beforeEach(() => { + identity.get.mockReturnValue(of(mockUser)); + }); + + it('should build url', () => { + adapter.deleteEntry(mockDeleteEntryQualifier).subscribe(); + + expect(http.url).toBe( + `${mockApiUrl}/carts/${mockDeleteEntryQualifier.cartId}/items/${ + mockDeleteEntryQualifier.groupKey + }${requestIncludes(true)}` + ); + }); + }); + + it('should do emission', () => { + const callback = vi.fn(); + + adapter.deleteEntry(mockDeleteEntryQualifier).subscribe(callback); + + expect(callback).toHaveBeenCalled(); + }); + }); + + describe('addEntry should create cart if needed for registered user', () => { + beforeEach(() => { + identity.get.mockReturnValue(of(mockUser)); + }); + + it('should create cart if cartId is undefined', () => { + const mockRegisteredAddEntryQualifier = { + cartId: undefined, + sku: 'sku', + quantity: 1, + }; + + const mockResponse = { id: 'newCartId' }; + const callback = vi.fn(); + + http.flush(mockResponse); + mockTransformer.do.mockReturnValue((x: any) => x); + + adapter.addEntry(mockRegisteredAddEntryQualifier).subscribe(callback); + + expect(http.urls).toStrictEqual([ + `${mockApiUrl}/carts`, + `${mockApiUrl}/carts/newCartId/items?include=items,vouchers`, + ]); + + http.flush(mockResponse); + + expect(callback).toHaveBeenCalledWith(mockResponse); + }); + + it('should not create cart if cartId is defined', () => { + const mockRegisteredAddEntryQualifier = { + cartId: 'testCartId', + sku: 'sku', + quantity: 1, + }; + + const mockResponse = { id: 'existingCartId' }; + const callback = vi.fn(); + + http.flush(mockResponse); + mockTransformer.do.mockReturnValue((x: any) => x); + + adapter.addEntry(mockRegisteredAddEntryQualifier).subscribe(callback); + + expect(http.url).toBe( + `${mockApiUrl}/carts/${ + mockRegisteredAddEntryQualifier.cartId + }/items${requestIncludes(true)}` + ); + + expect(http.urls).toStrictEqual([ + `${mockApiUrl}/carts/testCartId/items?include=items,vouchers`, + ]); + expect(callback).toHaveBeenCalledWith(mockResponse); + }); + }); + + describe('create', () => { + const qualifier = { + name: 'test', + currency: 'EUR', + priceMode: 'GROSS_MODE', + }; + + beforeEach(() => { + http.flush(null); + vi.spyOn(http, 'post'); + adapter.create(qualifier).subscribe(); + }); + + it('should get the store', () => { + expect(storeService.get).toHaveBeenCalled(); + }); + + it('should make a post request', () => { + expect(http.post).toHaveBeenCalledWith(`${mockApiUrl}/carts`, { + data: { + type: 'carts', + attributes: { + ...qualifier, + store: 'DE', + }, + }, + }); + }); + + it('should normalize the response', () => { + expect(mockTransformer.do).toHaveBeenCalledWith(CartNormalizer); + }); + + describe('when qualifier is not provided', () => { + beforeEach(() => { + adapter.create().subscribe(); + }); + + it('should get the currency', () => { + expect(currencyService.get).toHaveBeenCalled(); + }); + + it('should get the price mode', () => { + expect(priceModeService.get).toHaveBeenCalled(); + }); + + it('should not provide the name', () => { + expect(http.post).toHaveBeenCalledWith(`${mockApiUrl}/carts`, { + data: { + type: 'carts', + attributes: { + priceMode: 'GROSS_MODE', + currency: 'EUR', + store: 'DE', + }, + }, + }); + }); + }); + + describe('when single cart setup', () => { + beforeEach(() => { + optionsService.getFeatureOptions = vi + .fn() + .mockReturnValue({ multi: false }); + adapter.create(qualifier).subscribe(); + }); + + it('should not provide the name', () => { + expect(http.post).toHaveBeenCalledWith(`${mockApiUrl}/carts`, { + data: { + type: 'carts', + attributes: { + priceMode: 'GROSS_MODE', + currency: 'EUR', + store: 'DE', + }, + }, + }); + }); + }); + }); + + describe('delete', () => { + const qualifier = { + cartId: 'test', + }; + + beforeEach(() => { + http.flush(null); + vi.spyOn(http, 'delete'); + adapter.delete(qualifier).subscribe(); + }); + + it('should make a delete request', () => { + expect(http.delete).toHaveBeenCalledWith( + `${mockApiUrl}/carts/${qualifier.cartId}` + ); + }); + }); +}); diff --git a/libs/domain/cart/services/src/adapter/spryker-glue/glue-cart.adapter.ts b/libs/domain/cart/services/src/adapter/spryker-glue/glue-cart.adapter.ts new file mode 100644 index 000000000..e8962135c --- /dev/null +++ b/libs/domain/cart/services/src/adapter/spryker-glue/glue-cart.adapter.ts @@ -0,0 +1,328 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { AuthIdentity, IdentityService } from '@spryker-oryx/auth'; +import { + AddCartEntryQualifier, + ApiCartModel, + Cart, + CartAdapter, + CartEntryQualifier, + CartFeatureOptionsKey, + CartNormalizer, + CartQualifier, + CartsNormalizer, + CouponQualifier, + CreateCartQualifier, + UpdateCartEntryQualifier, + UpdateCartQualifier, +} from '@spryker-oryx/cart'; +import { + FeatureOptionsService, + HttpService, + JsonAPITransformerService, +} from '@spryker-oryx/core'; +import { inject } from '@spryker-oryx/di'; +import { + CurrencyService, + PriceModeService, + StoreService, +} from '@spryker-oryx/site'; +import { + Observable, + catchError, + combineLatest, + map, + of, + switchMap, + take, + throwError, +} from 'rxjs'; + +export class GlueCartAdapter implements CartAdapter { + constructor( + protected http = inject(HttpService), + protected SCOS_BASE_URL = inject('SCOS_BASE_URL'), + protected transformer = inject(JsonAPITransformerService), + protected identity = inject(IdentityService), + protected store = inject(StoreService), + protected currency = inject(CurrencyService), + protected priceMode = inject(PriceModeService), + protected optionsService = inject(FeatureOptionsService) + ) {} + + getAll(): Observable { + return this.identity.get().pipe( + take(1), + switchMap((identity) => { + if (!identity.isAuthenticated && !identity.userId) return of([]); + + const url = this.generateUrl( + identity.isAuthenticated + ? `${ApiCartModel.UrlParts.Customers}/${identity.userId}/${ApiCartModel.UrlParts.Carts}` + : ApiCartModel.UrlParts.GuestCarts, + !identity.isAuthenticated + ); + + return this.http.get(url).pipe( + this.transformer.do(CartsNormalizer), + catchError(() => of([])) + ); + }) + ); + } + + get(data: CartQualifier): Observable { + if (!data.cartId) return throwError(() => new Error('Cart ID is required')); + + return this.identity.get().pipe( + take(1), + switchMap((identity) => { + const url = this.generateUrl( + `${ + identity.isAuthenticated + ? ApiCartModel.UrlParts.Carts + : ApiCartModel.UrlParts.GuestCarts + }/${data.cartId}`, + !identity.isAuthenticated + ); + + return this.http + .get(url) + .pipe(this.transformer.do(CartNormalizer)); + }) + ); + } + + update(data: UpdateCartQualifier): Observable { + return this.identity.get().pipe( + take(1), + switchMap((identity) => { + const url = this.generateUrl( + `${ + identity.isAuthenticated + ? ApiCartModel.UrlParts.Carts + : ApiCartModel.UrlParts.GuestCarts + }/${data.cartId}`, + !identity.isAuthenticated + ); + + const body = { + data: { + type: identity.isAuthenticated + ? ApiCartModel.UrlParts.Carts + : ApiCartModel.UrlParts.GuestCarts, + attributes: { ...data }, + }, + }; + + const options = { + headers: { 'If-Match': '*' }, + }; + + return this.http + .patch(url, body, options) + .pipe(this.transformer.do(CartNormalizer)); + }) + ); + } + + delete(data: CartQualifier): Observable { + if (!data.cartId) return throwError(() => new Error('Cart ID is required')); + + return this.http.delete( + `${this.SCOS_BASE_URL}/${ApiCartModel.UrlParts.Carts}/${data.cartId}` + ); + } + + addCoupon(data: CouponQualifier): Observable { + return this.identity.get().pipe( + take(1), + switchMap((identity) => this.createCartIfNeeded(identity, data.cartId)), + switchMap(([identity]) => { + const url = this.generateUrl( + identity.isAuthenticated + ? `${ApiCartModel.UrlParts.Carts}/${data.cartId}/${ApiCartModel.UrlParts.Coupons}` + : `${ApiCartModel.UrlParts.GuestCarts}/${data.cartId}/${ApiCartModel.UrlParts.Coupons}`, + !identity.isAuthenticated + ); + + const body = { + data: { + type: ApiCartModel.UrlParts.Coupons, + attributes: { + code: data.code, + }, + }, + }; + + return this.http + .post(url, body) + .pipe(this.transformer.do(CartNormalizer)); + }) + ); + } + + deleteCoupon(data: CouponQualifier): Observable { + return this.identity.get().pipe( + take(1), + switchMap((identity) => { + const requestType = identity.isAuthenticated + ? ApiCartModel.UrlParts.Carts + : ApiCartModel.UrlParts.GuestCarts; + + const url = this.generateUrl( + `${requestType}/${data.cartId}/${ApiCartModel.UrlParts.Coupons}/${data.code}`, + !identity.isAuthenticated + ); + return this.http.delete(url); + }) + ); + } + + addEntry(data: AddCartEntryQualifier): Observable { + const attributes = { + sku: data.sku, + quantity: data.quantity, + productOfferReference: data.offer, + }; + + return this.identity.get({ requireGuest: true }).pipe( + take(1), + switchMap((identity) => this.createCartIfNeeded(identity, data.cartId)), + switchMap(([identity, cartId]) => { + const url = cartId + ? this.generateUrl( + identity.isAuthenticated + ? `${ApiCartModel.UrlParts.Carts}/${cartId}/${ApiCartModel.UrlParts.Items}` + : `${ApiCartModel.UrlParts.GuestCarts}/${cartId}/${ApiCartModel.UrlParts.GuestCartItems}`, + !identity.isAuthenticated + ) + : this.generateUrl( + ApiCartModel.UrlParts.GuestCartItems, + !identity.isAuthenticated + ); + + const body = { + data: { + type: identity.isAuthenticated + ? ApiCartModel.UrlParts.Items + : ApiCartModel.UrlParts.GuestCartItems, + attributes, + }, + }; + + return this.http + .post(url, body) + .pipe(this.transformer.do(CartNormalizer)); + }) + ); + } + + create(qualifier?: CreateCartQualifier): Observable { + const request = (attributes: CreateCartQualifier) => + this.http.post( + `${this.SCOS_BASE_URL}/${ApiCartModel.UrlParts.Carts}`, + { + data: { + type: 'carts', + attributes, + }, + } + ); + + return this.store.get().pipe( + take(1), + switchMap((store) => + qualifier + ? request({ + store: store?.id, + ...qualifier, + name: this.ensureCartName(qualifier), + }) + : combineLatest([this.currency.get(), this.priceMode.get()]).pipe( + take(1), + switchMap(([currency, priceMode]) => + request({ store: store?.id, currency, priceMode }) + ) + ) + ), + this.transformer.do(CartNormalizer) + ); + } + + protected createCartIfNeeded( + identity: AuthIdentity, + cartId: string | undefined + ): Observable<[AuthIdentity, string | undefined]> { + if (!identity.isAuthenticated || cartId) { + return of([identity, cartId]); + } + + // if we are a registered user and we do not have a cartId, we need to create a cart first + return this.create().pipe(map(({ id }) => [identity, id])); + } + + updateEntry(data: UpdateCartEntryQualifier): Observable { + const attributes = { + quantity: data.quantity, + }; + + return this.identity.get().pipe( + take(1), + switchMap((identity) => { + const url = this.generateUrl( + identity.isAuthenticated + ? `${ApiCartModel.UrlParts.Carts}/${data.cartId}/${ApiCartModel.UrlParts.Items}/${data.groupKey}` + : `${ApiCartModel.UrlParts.GuestCarts}/${data.cartId}/${ApiCartModel.UrlParts.GuestCartItems}/${data.groupKey}`, + !identity.isAuthenticated + ); + const body = { + data: { + type: identity.isAuthenticated + ? ApiCartModel.UrlParts.Items + : ApiCartModel.UrlParts.GuestCartItems, + attributes, + }, + }; + + return this.http + .patch(url, body) + .pipe(this.transformer.do(CartNormalizer)); + }) + ); + } + + deleteEntry(data: CartEntryQualifier): Observable { + return this.identity.get().pipe( + take(1), + switchMap((identity) => { + const url = this.generateUrl( + identity.isAuthenticated + ? `${ApiCartModel.UrlParts.Carts}/${data.cartId}/${ApiCartModel.UrlParts.Items}/${data.groupKey}` + : `${ApiCartModel.UrlParts.GuestCarts}/${data.cartId}/${ApiCartModel.UrlParts.GuestCartItems}/${data.groupKey}`, + !identity.isAuthenticated + ); + + return this.http.delete(url); + }) + ); + } + + protected generateUrl(path: string, isAnonymous?: boolean): string { + const includes = isAnonymous + ? [ApiCartModel.Includes.GuestCartItems, ApiCartModel.Includes.Coupons] + : [ApiCartModel.Includes.Items, ApiCartModel.Includes.Coupons]; + + return `${this.SCOS_BASE_URL}/${path}${`?include=${includes.join(',')}`}`; + } + + protected ensureCartName( + qualifier?: CreateCartQualifier + ): string | undefined { + return this.isMultiCart ? qualifier?.name : undefined; + } + + protected get isMultiCart(): boolean { + return !!this.optionsService.getFeatureOptions(CartFeatureOptionsKey) + ?.multi; + } +} diff --git a/libs/domain/cart/services/src/adapter/spryker-glue/index.ts b/libs/domain/cart/services/src/adapter/spryker-glue/index.ts new file mode 100644 index 000000000..79d3315b3 --- /dev/null +++ b/libs/domain/cart/services/src/adapter/spryker-glue/index.ts @@ -0,0 +1,2 @@ +export * from './glue-cart.adapter'; +export * from './normalizers'; diff --git a/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/cart.normalizer.spec.ts b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/cart.normalizer.spec.ts new file mode 100644 index 000000000..0de9a4690 --- /dev/null +++ b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/cart.normalizer.spec.ts @@ -0,0 +1,62 @@ +import { cartAttributesNormalizer } from './cart.normalizer'; +import { DeserializedCart } from './model'; + +const mockGuestDeserializedCart = { + currency: 'EUR', + discounts: [{ discount: 'discount' }], + id: '51bc9ed7-89e6-5967-ac56-a03f92dce54d', + priceMode: 'GROSS_MODE', + guestCartItems: [ + { + item: 'itemA', + }, + { + item: 'itemB', + }, + ], + store: 'DE', + thresholds: [], + totals: { expenseTotal: 0 }, +} as unknown as DeserializedCart; + +const mockDeserializedCart = { + currency: 'EUR', + discounts: [{ discount: 'discount' }], + id: '51bc9ed7-89e6-5967-ac56-a03f92dce54d', + priceMode: 'GROSS_MODE', + items: [ + { + item: 'itemA', + }, + { + item: 'itemB', + }, + ], + store: 'DE', + thresholds: [], + totals: { expenseTotal: 0 }, +} as unknown as DeserializedCart; + +describe('Cart Normalizers', () => { + describe('Cart Attributes Normalizer', () => { + it('should transform guest DeserializedCart into Cart', () => { + const mockResult = { + ...mockGuestDeserializedCart, + products: mockGuestDeserializedCart.guestCartItems, + }; + delete mockResult.guestCartItems; + const normalized = cartAttributesNormalizer(mockGuestDeserializedCart); + expect(normalized).toEqual(mockResult); + }); + + it('should transform loggedIn user DeserializedCart into Cart', () => { + const mockResult = { + ...mockDeserializedCart, + products: mockDeserializedCart.items, + }; + delete mockResult.items; + const normalized = cartAttributesNormalizer(mockDeserializedCart); + expect(normalized).toEqual(mockResult); + }); + }); +}); diff --git a/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/cart.normalizer.ts b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/cart.normalizer.ts new file mode 100644 index 000000000..321f348fc --- /dev/null +++ b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/cart.normalizer.ts @@ -0,0 +1,26 @@ +import { ApiCartModel, Cart } from '@spryker-oryx/cart'; +import { camelize } from '@spryker-oryx/core/utilities'; +import { DeserializedCart } from './model'; + +export function cartAttributesNormalizer(data: DeserializedCart): Cart { + const guestItemsKey = camelize(ApiCartModel.Includes.GuestCartItems); + const itemsKey = camelize(ApiCartModel.Includes.Items); + const products = data[itemsKey] ?? data[guestItemsKey]; + const coupons = data[camelize(ApiCartModel.Includes.Coupons)]; + + delete data[itemsKey]; + delete data[guestItemsKey]; + + // Workaround for shipmentTotals = 0 + // we don't want to have shipment and expense totals in the default cart response + // as we can't recognize if shipment is calculated or not in case if it's zero + if (!data.totals?.shipmentTotal) { + delete data.totals?.shipmentTotal; + } + + return { + ...data, + products, + coupons, + }; +} diff --git a/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/carts.normalizer.spec.ts b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/carts.normalizer.spec.ts new file mode 100644 index 000000000..a4b1c143e --- /dev/null +++ b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/carts.normalizer.spec.ts @@ -0,0 +1,64 @@ +import { CartNormalizer } from '@spryker-oryx/cart'; +import { of, take } from 'rxjs'; +import { cartsItemsNormalizer } from './carts.normalizer'; +import { DeserializedCart } from './model'; + +const mockDeserializedCarts = [ + { + currency: 'EUR', + discounts: [{ discount: 'discount' }], + id: 'id-1', + priceMode: 'GROSS_MODE', + guestCartItems: [ + { + item: 'itemA', + }, + ], + store: 'DE', + thresholds: [], + totals: { expenseTotal: 0 }, + }, + { + currency: 'EUR', + discounts: [{ discount: 'discount' }], + id: 'id-2', + priceMode: 'GROSS_MODE', + guestCartItems: [ + { + item: 'itemA', + }, + { + item: 'itemB', + }, + ], + store: 'DE', + thresholds: [], + totals: { expenseTotal: 0 }, + }, +] as unknown as DeserializedCart[]; + +describe('Cart Normalizers', () => { + describe('Cart Attributes Normalizer', () => { + it('should transform DeserializedCart array into Cart array', () => { + const mockTransformed = 'mockTransformed'; + const mockTransformer = { + do: vi.fn().mockReturnValue(() => of(mockTransformed)), + transform: vi.fn().mockReturnValue(of(mockTransformed)), + }; + + cartsItemsNormalizer(mockDeserializedCarts, mockTransformer) + .pipe(take(1)) + .subscribe((normalized) => { + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockDeserializedCarts[0], + CartNormalizer + ); + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockDeserializedCarts[1], + CartNormalizer + ); + expect(normalized).toEqual([mockTransformed, mockTransformed]); + }); + }); + }); +}); diff --git a/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/carts.normalizer.ts b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/carts.normalizer.ts new file mode 100644 index 000000000..ec85c7ec9 --- /dev/null +++ b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/carts.normalizer.ts @@ -0,0 +1,15 @@ +import { Cart, CartNormalizer } from '@spryker-oryx/cart'; +import { TransformerService } from '@spryker-oryx/core'; +import { Observable, combineLatest, of } from 'rxjs'; +import { DeserializedCart } from './model'; + +export function cartsItemsNormalizer( + data: DeserializedCart[], + transformer: TransformerService +): Observable { + return data.length + ? combineLatest( + data.map((cart) => transformer.transform(cart, CartNormalizer)) + ) + : of([]); +} diff --git a/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/index.ts b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/index.ts new file mode 100644 index 000000000..010c3fbba --- /dev/null +++ b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/index.ts @@ -0,0 +1,3 @@ +export * from './cart.normalizer'; +export * from './carts.normalizer'; +export * from './model'; diff --git a/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/model.ts b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/model.ts new file mode 100644 index 000000000..a89cea218 --- /dev/null +++ b/libs/domain/cart/services/src/adapter/spryker-glue/normalizers/model.ts @@ -0,0 +1,19 @@ +import { ApiCartModel, CartId } from '@spryker-oryx/cart'; +import { CamelCase } from '@spryker-oryx/core/utilities'; + +export type DeserializedCartIncludes = { + [P in ApiCartModel.Includes as `${CamelCase

}`]?: P extends + | ApiCartModel.Includes.Items + | ApiCartModel.Includes.GuestCartItems + ? ApiCartModel.Entry[] + : never; +}; + +export type DeserializedCart = ApiCartModel.Attributes & + Pick< + DeserializedCartIncludes, + CamelCase< + ApiCartModel.Includes.Items | ApiCartModel.Includes.GuestCartItems + > + > & + CartId; diff --git a/libs/domain/cart/src/feature.ts b/libs/domain/cart/src/feature.ts index 98319aed2..2f480d2eb 100644 --- a/libs/domain/cart/src/feature.ts +++ b/libs/domain/cart/src/feature.ts @@ -1,11 +1,35 @@ import { AppFeature } from '@spryker-oryx/core'; import * as components from './components'; -import { cartProviders } from './services'; +import { + cartProviders, + glueCartProviders, + mockCartProviders, +} from './services'; export * from './components'; export const cartComponents = Object.values(components); +/** + * Barebone Cart feature, without connector layer (adapters), + * designed to work with custom adapters + */ export const cartFeature: AppFeature = { providers: cartProviders, components: cartComponents, }; + +/** + * Fully functional Cart feature with Glue connectors included. + */ +export const glueCartFeature: AppFeature = { + providers: glueCartProviders, + components: cartComponents, +}; + +/** + * Cart feature with mock connectors for testing and quick start + */ +export const mockCartFeature: AppFeature = { + providers: mockCartProviders, + components: cartComponents, +}; diff --git a/libs/domain/cart/src/index.ts b/libs/domain/cart/src/index.ts index d1f37648a..e82891044 100644 --- a/libs/domain/cart/src/index.ts +++ b/libs/domain/cart/src/index.ts @@ -8,4 +8,3 @@ export * from './mixins'; export * from './models'; export * from './presets'; export * from './services'; -export * from './services-reexports'; diff --git a/libs/domain/cart/src/services-reexports.ts b/libs/domain/cart/src/services-reexports.ts index 5cafddf44..c91be0461 100644 --- a/libs/domain/cart/src/services-reexports.ts +++ b/libs/domain/cart/src/services-reexports.ts @@ -7,7 +7,7 @@ const reexports: typeof services = /** @deprecated since 1.2, use DefaultCartService from @spryker-oryx/cart/services */ export const DefaultCartService = reexports?.DefaultCartService; /** @deprecated since 1.2, use DefaultCartAdapter from @spryker-oryx/cart/services */ -export const DefaultCartAdapter = reexports?.DefaultCartAdapter; +export const DefaultCartAdapter = reexports?.GlueCartAdapter; /** @deprecated since 1.2, use DefaultTotalsService from @spryker-oryx/cart/services */ export const DefaultTotalsService = reexports?.DefaultTotalsService; /** @deprecated since 1.2, use cartAttributesNormalizer from @spryker-oryx/cart/services */ diff --git a/libs/domain/cart/src/services/cart.providers.ts b/libs/domain/cart/src/services/cart.providers.ts index 54ca90595..3693c5e49 100644 --- a/libs/domain/cart/src/services/cart.providers.ts +++ b/libs/domain/cart/src/services/cart.providers.ts @@ -1,17 +1,7 @@ import { TokenResourceResolvers } from '@spryker-oryx/core'; import { Provider } from '@spryker-oryx/di'; import { provideExperienceData } from '@spryker-oryx/experience'; -import { featureVersion } from '@spryker-oryx/utilities'; import { cartCreatePage, cartsPage } from '../presets'; -import { - CartResolver, - CartTotalsResolver, - DefaultCartAdapter, - DefaultCartService, - DefaultTotalsService, - cartAttributesNormalizer, - cartsItemsNormalizer, -} from '../services-reexports'; import { CartAdapter, CartNormalizer, CartsNormalizer } from './adapter'; import { cartContextProviders } from './cart-context'; import { CartService } from './cart.service'; @@ -20,110 +10,73 @@ import { TotalsResolver, TotalsService } from './totals'; export const TotalsResolverCartToken = `${TotalsResolver}CART`; export const CartTokenResourceResolverToken = `${TokenResourceResolvers}CART`; -export const cartNormalizer: Provider[] = - featureVersion < '1.2' - ? [ - { - provide: CartNormalizer, - useValue: cartAttributesNormalizer, - }, - ] - : [ - { - provide: CartNormalizer, - useValue: () => - import('@spryker-oryx/cart/services').then( - (m) => m.cartAttributesNormalizer - ), - }, - ]; +export const cartNormalizer: Provider[] = [ + { + provide: CartNormalizer, + useValue: () => + import('@spryker-oryx/cart/services').then( + (m) => m.cartAttributesNormalizer + ), + }, +]; -export const cartsNormalizer: Provider[] = - featureVersion < '1.2' - ? [ - { - provide: CartsNormalizer, - useValue: cartsItemsNormalizer, - }, - ] - : [ - { - provide: CartsNormalizer, - useValue: () => - import('@spryker-oryx/cart/services').then( - (m) => m.cartsItemsNormalizer - ), - }, - ]; +export const cartsNormalizer: Provider[] = [ + { + provide: CartsNormalizer, + useValue: () => + import('@spryker-oryx/cart/services').then((m) => m.cartsItemsNormalizer), + }, +]; -/** @deprecated since 1.2 */ -export const CartResourceResolver: Provider = { - provide: CartTokenResourceResolverToken, - useClass: CartResolver, -}; +export const glueCartConnectors: Provider[] = [ + { + provide: CartAdapter, + asyncClass: () => + import('@spryker-oryx/cart/services').then((m) => m.GlueCartAdapter), + }, + ...cartNormalizer, + ...cartsNormalizer, +]; -/** @deprecated since 1.2 */ -export const CartTotalsProvider: Provider = { - provide: TotalsResolverCartToken, - useClass: CartTotalsResolver, -}; +export const mockCartConnectors: Provider[] = [ + { + provide: CartAdapter, + asyncClass: () => + import('@spryker-oryx/cart/services').then((m) => m.MockCartAdapter), + }, +]; -export const cartProviders: Provider[] = - featureVersion < '1.2' - ? [ - { - provide: CartAdapter, - useClass: DefaultCartAdapter, - }, - { - provide: CartService, - useClass: DefaultCartService, - }, - { - provide: TotalsService, - useClass: DefaultTotalsService, - }, - CartResourceResolver, - CartTotalsProvider, - ...cartNormalizer, - ...cartsNormalizer, - ] - : [ - { - provide: CartAdapter, - asyncClass: () => - import('@spryker-oryx/cart/services').then( - (m) => m.DefaultCartAdapter - ), - }, - { - provide: CartService, - asyncClass: () => - import('@spryker-oryx/cart/services').then( - (m) => m.DefaultCartService - ), - }, - ...cartNormalizer, - ...cartsNormalizer, - { - provide: TotalsService, - asyncClass: () => - import('@spryker-oryx/cart/services').then( - (m) => m.DefaultTotalsService - ), - }, - { - provide: CartTokenResourceResolverToken, - asyncClass: () => - import('@spryker-oryx/cart/services').then((m) => m.CartResolver), - }, - { - provide: TotalsResolverCartToken, - asyncClass: () => - import('@spryker-oryx/cart/services').then( - (m) => m.CartTotalsResolver - ), - }, - ...cartContextProviders, - provideExperienceData([cartsPage, cartCreatePage]), - ]; +export const cartProviders: Provider[] = [ + { + provide: CartService, + asyncClass: () => + import('@spryker-oryx/cart/services').then((m) => m.DefaultCartService), + }, + { + provide: TotalsService, + asyncClass: () => + import('@spryker-oryx/cart/services').then((m) => m.DefaultTotalsService), + }, + { + provide: CartTokenResourceResolverToken, + asyncClass: () => + import('@spryker-oryx/cart/services').then((m) => m.CartResolver), + }, + { + provide: TotalsResolverCartToken, + asyncClass: () => + import('@spryker-oryx/cart/services').then((m) => m.CartTotalsResolver), + }, + ...cartContextProviders, + provideExperienceData([cartsPage, cartCreatePage]), +]; + +export const glueCartProviders: Provider[] = [ + ...glueCartConnectors, + ...cartProviders, +]; + +export const mockCartProviders: Provider[] = [ + ...mockCartConnectors, + ...cartProviders, +]; diff --git a/libs/domain/checkout/services/src/adapter/index.ts b/libs/domain/checkout/services/src/adapter/index.ts index d46b727fd..6ccc5579a 100644 --- a/libs/domain/checkout/services/src/adapter/index.ts +++ b/libs/domain/checkout/services/src/adapter/index.ts @@ -1,3 +1 @@ -export * from './default-checkout.adapter'; -export * from './normalizers'; -export * from './serializers'; +export * from './spryker-glue'; diff --git a/libs/domain/checkout/services/src/adapter/mock/index.ts b/libs/domain/checkout/services/src/adapter/mock/index.ts new file mode 100644 index 000000000..adf0c0b83 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/mock/index.ts @@ -0,0 +1,2 @@ +export * from './mock-checkout'; +export * from './mock-checkout.adapter'; diff --git a/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts new file mode 100644 index 000000000..4893a82d4 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts @@ -0,0 +1,33 @@ +import { mockDefaultCart } from '@spryker-oryx/cart/mocks'; +import { + CheckoutAdapter, + CheckoutData, + CheckoutResponse, + PlaceOrderData, +} from '@spryker-oryx/checkout'; +import { Observable, of } from 'rxjs'; + +export class MockCheckoutAdapter implements CheckoutAdapter { + get(props: PlaceOrderData): Observable { + const checkoutData = { + addresses: [props.billingAddress, props.shippingAddress], + paymentProviders: 1, + selectedShipmentMethods: 1, + selectedPaymentMethods: 1, + paymentMethods: [...props.payments], + shipments: [...props.shipments], + carriers: 1, + shipment: props.shipment, + carts: { + id: props.cartId, + ...mockDefaultCart, + }, + }; + + return of(checkoutData); + } + + placeOrder(data: PlaceOrderData): Observable { + return undefined; + } +} diff --git a/libs/domain/checkout/services/src/adapter/mock/mock-checkout.ts b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.ts new file mode 100644 index 000000000..3f0b487a2 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.ts @@ -0,0 +1,308 @@ +import { PlaceOrderData } from '../../models'; + +export const mockSelectedShipmentMethod = { + selectedShipmentMethod: { + id: '2', + name: 'mock shipment method', + carrierName: 'mock carrier', + price: 590, + taxRate: '19.00', + deliveryTime: null, + currencyIsoCode: 'EUR', + }, +}; + +export const mockShipmentMethods = [ + { + type: 'shipment-methods', + id: '4', + attributes: { + name: 'Mock Express', + carrierName: 'Mock Drone Carrier', + deliveryTime: null, + price: 500, + currencyIsoCode: 'EUR', + }, + }, + { + type: 'shipment-methods', + id: '3', + attributes: { + name: 'Mock Method', + carrierName: 'Mock Drone Carrier', + deliveryTime: 1668528630013, + price: 300, + currencyIsoCode: 'EUR', + }, + }, + { + type: 'shipment-methods', + id: '1', + attributes: { + name: 'Standard Mock', + carrierName: 'Mock Dummy Carrier', + deliveryTime: 1602367200000, + price: 490, + currencyIsoCode: 'EUR', + }, + }, + { + type: 'shipment-methods', + id: '2', + attributes: { + name: 'Express Mock', + carrierName: 'Mock Dummy Carrier', + deliveryTime: null, + price: 500, + currencyIsoCode: 'EUR', + }, + }, +]; + +export const mockShipmentAttributes = { + items: ['mockitem', 'mockitem2'], + requestedDeliveryDate: null, + shippingAddress: { + id: null, + salutation: null, + firstName: null, + lastName: null, + address1: null, + address2: null, + address3: null, + zipCode: null, + city: null, + country: null, + iso2Code: null, + company: null, + phone: null, + isDefaultBilling: null, + isDefaultShipping: null, + idCompanyBusinessUnitAddress: null, + }, + selectedShipmentMethod: mockSelectedShipmentMethod.selectedShipmentMethod, +}; + +export const mockGetShipmentResponse = { + included: [ + { + type: 'shipments', + id: 'shipment', + attributes: { + ...mockShipmentAttributes, + ...mockSelectedShipmentMethod, + }, + }, + ], +}; + +export const mockPlaceOrderResponse = { + data: { + type: 'checkout', + id: 'mock', + attributes: { + orderReference: '123', + redirectUrl: 'url', + isExternalRedirect: true, + }, + links: { + self: 'http://glue.spryker.local/checkout', + }, + relationships: { + orders: { + data: [ + { + id: 'mock', + type: 'order', + }, + ], + }, + }, + }, + links: { + self: 'http://glue.spryker.local/checkout', + }, + included: [ + { + type: 'order', + id: 'mock', + attributes: { + createdAt: '123', + totals: { + expenseTotal: 0, + discountTotal: 0, + taxTotal: 0, + subtotal: 0, + grandTotal: 0, + canceledTotal: 0, + remunerationTotal: 0, + }, + currencyIsoCode: 'EUR', + priceMode: 'GROSS_MODE', + }, + links: { + self: 'http://glue.spryker.local/checkout', + }, + relationships: { + 'order-shipments': { + data: [ + { + id: 'mock', + type: 'order', + }, + ], + }, + }, + }, + ], +}; + +export const mockFilteredShipmentMethods = [ + { + name: 'Mock Dummy Carrier', + shipmentMethods: [ + { ...mockShipmentMethods[2].attributes, id: mockShipmentMethods[2].id }, + { ...mockShipmentMethods[3].attributes, id: mockShipmentMethods[3].id }, + ], + }, + { + name: 'Mock Drone Carrier', + shipmentMethods: [ + { ...mockShipmentMethods[0].attributes, id: mockShipmentMethods[0].id }, + { ...mockShipmentMethods[1].attributes, id: mockShipmentMethods[1].id }, + ], + }, +]; + +export const mockSerializedShipmentMethods = [ + { ...mockShipmentMethods[2].attributes, id: mockShipmentMethods[2].id }, + { ...mockShipmentMethods[3].attributes, id: mockShipmentMethods[3].id }, + { ...mockShipmentMethods[0].attributes, id: mockShipmentMethods[0].id }, + { ...mockShipmentMethods[1].attributes, id: mockShipmentMethods[1].id }, +]; + +export const mockDeliveryTimeShipmentMethod = [ + { + name: 'Mock Dummy Carrier', + shipmentMethods: [ + { ...mockShipmentMethods[2].attributes, id: mockShipmentMethods[2].id }, + ], + }, +]; + +export const mockPaymentMethods = [ + { + paymentMethodName: 'Invoice', + paymentProviderName: 'DummyPayment', + priority: 1, + requiredRequestData: [ + 'paymentMethod', + 'paymentProvider', + 'dummyPaymentInvoice.dateOfBirth', + ], + id: '1', + }, + { + paymentMethodName: 'Stripe', + paymentProviderName: 'mockProvider', + priority: 1, + requiredRequestData: [ + 'paymentMethod', + 'paymentProvider', + 'dummyPaymentInvoice.dateOfBirth', + ], + id: '3', + }, + { + paymentMethodName: 'Paypal', + paymentProviderName: 'mockProvider', + priority: 2, + requiredRequestData: [ + 'paymentMethod', + 'paymentProvider', + 'dummyPaymentInvoice.dateOfBirth', + ], + id: '4', + }, + { + paymentMethodName: 'Credit Card', + paymentProviderName: 'DummyPayment', + priority: 2, + requiredRequestData: [ + 'paymentMethod', + 'paymentProvider', + 'dummyPaymentCreditCard.cardType', + 'dummyPaymentCreditCard.cardNumber', + 'dummyPaymentCreditCard.nameOnCard', + 'dummyPaymentCreditCard.cardExpiresMonth', + 'dummyPaymentCreditCard.cardExpiresYear', + 'dummyPaymentCreditCard.cardSecurityCode', + ], + id: '2', + }, +]; + +const mockCustomer = { + email: 'mock', + salutation: 'Mr', + firstName: 'first', + lastName: 'last', +}; + +export const mockPlaceOrderData: PlaceOrderData = { + cartId: 'mockcart', + payments: [{ provider: 'mockProvider', name: 'mock', id: '1' }], + shipments: [mockShipmentAttributes], + customer: mockCustomer, + billingAddress: mockShipmentAttributes.shippingAddress, +}; + +export const mockCheckout = { + type: 'checkout', + attributes: { + idCart: 'mockcart', + payments: [ + { + id: '1', + paymentProviderName: 'mockProvider', + paymentMethodName: 'mock', + }, + ], + shipments: [mockShipmentAttributes], + customer: mockCustomer, + billingAddress: mockShipmentAttributes.shippingAddress, + }, +}; + +export const mockNormalizedPaymentMethods = mockPaymentMethods + .map((payment) => { + const { paymentMethodName, paymentProviderName, ...paymentData } = payment; + return { + ...paymentData, + name: paymentMethodName, + provider: paymentProviderName, + }; + }) + .sort((a, b) => + a.provider.toLowerCase() < b.provider.toLowerCase() + ? -1 + : a.provider.toLowerCase() > b.provider.toLowerCase() + ? 1 + : 0 + ); + +export const mockNormalizedShipmentAttributes = { + ...mockShipmentAttributes, + carriers: mockFilteredShipmentMethods, +}; + +export const mockNormalizedCheckoutData = { + shipments: [mockNormalizedShipmentAttributes], + paymentMethods: mockNormalizedPaymentMethods, +}; + +export const mockNormalizedUpdatedCheckoutData = { + shipments: [{ ...mockShipmentAttributes, ...mockSelectedShipmentMethod }], + paymentMethods: mockNormalizedPaymentMethods, + carriers: mockFilteredShipmentMethods, +}; diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/glue-checkout.adapter.spec.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/glue-checkout.adapter.spec.ts new file mode 100644 index 000000000..fe8ebbba4 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/glue-checkout.adapter.spec.ts @@ -0,0 +1,173 @@ +import { AuthIdentity, IdentityService } from '@spryker-oryx/auth'; +import { + CheckoutAdapter, + CheckoutNormalizer, + CheckoutResponseNormalizer, +} from '@spryker-oryx/checkout'; +import { + mockCheckout, + mockGetShipmentResponse, + mockPlaceOrderData, + mockPlaceOrderResponse, +} from '@spryker-oryx/checkout/mocks'; +import { HttpService, JsonAPITransformerService } from '@spryker-oryx/core'; +import { HttpTestService } from '@spryker-oryx/core/testing'; +import { createInjector, destroyInjector } from '@spryker-oryx/di'; +import { Observable, of } from 'rxjs'; +import { GlueCheckoutAdapter } from './glue-checkout.adapter'; + +const mockApiUrl = 'mockApiUrl'; +const cartId = 'mockid'; + +const mockTransformer = { + transform: vi.fn().mockReturnValue(of(null)), + serialize: vi.fn().mockReturnValue(of(null)), + do: vi.fn().mockReturnValue(() => of(null)), +}; + +const mockAnonymousUser: AuthIdentity = { + userId: 'anonymousUserId', + isAuthenticated: false, +}; + +class MockIdentityService implements Partial { + get = vi + .fn<[], Observable>() + .mockReturnValue(of(mockAnonymousUser)); +} + +describe('GlueCheckoutService', () => { + let service: CheckoutAdapter; + let identity: MockIdentityService; + let http: HttpTestService; + const mockTransformerData = 'mockTransformerData'; + const callback = vi.fn(); + + beforeEach(() => { + const testInjector = createInjector({ + providers: [ + { + provide: HttpService, + useClass: HttpTestService, + }, + { + provide: CheckoutAdapter, + useClass: GlueCheckoutAdapter, + }, + { + provide: 'SCOS_BASE_URL', + useValue: mockApiUrl, + }, + { + provide: JsonAPITransformerService, + useValue: mockTransformer, + }, + { + provide: IdentityService, + useClass: MockIdentityService, + }, + ], + }); + + service = testInjector.inject(CheckoutAdapter); + + http = testInjector.inject(HttpService) as unknown as HttpTestService; + identity = testInjector.inject(IdentityService) as MockIdentityService; + }); + + afterEach(() => { + vi.clearAllMocks(); + destroyInjector(); + }); + + it('should be provided', () => { + expect(service).toBeInstanceOf(GlueCheckoutAdapter); + }); + + describe('get should send `post` request', () => { + describe('when an include is not provided', () => { + const mockSerializedGetCheckoutDataProps = { + data: { + type: 'checkout-data', + attributes: { cartId }, + }, + }; + + it('should build the url with standard includes', () => { + service.get({ cartId }).subscribe(() => { + expect(http.url).toBe( + `${mockApiUrl}/checkout-data?include=shipments,shipment-methods,payment-methods,carts,guest-carts` + ); + }); + }); + + it('should provide body', () => { + mockTransformer.serialize.mockReturnValue( + of(mockSerializedGetCheckoutDataProps) + ); + service.get({ cartId }).subscribe(() => { + expect(http.body).toEqual(mockSerializedGetCheckoutDataProps); + }); + }); + + it('should call transformer with proper normalizer', () => { + http.flush(mockGetShipmentResponse); + service.get({ cartId }).subscribe(); + + expect(mockTransformer.do).toHaveBeenCalledWith(CheckoutNormalizer); + }); + + it('should return transformed data', () => { + mockTransformer.do.mockReturnValue(() => of(mockTransformerData)); + service.get({ cartId }).subscribe(callback); + + expect(callback).toHaveBeenCalledWith(mockTransformerData); + }); + }); + }); + + describe('when placing an order', () => { + describe('and user is not logged in', () => { + it('should build url', () => { + service.placeOrder(mockPlaceOrderData).subscribe(() => { + expect(http.url).toBe(`${mockApiUrl}/checkout?include=orders`); + }); + }); + }); + + describe('and user is logged in', () => { + it('should build url', () => { + identity.get.mockReturnValue( + of({ userId: 'mockUser', isAuthenticated: true }) + ); + service.placeOrder(mockPlaceOrderData).subscribe(() => { + expect(http.url).toBe(`${mockApiUrl}/checkout`); + }); + }); + }); + + it('should provide body', () => { + mockTransformer.serialize.mockReturnValue(of(mockCheckout)); + service.placeOrder(mockPlaceOrderData).subscribe(() => { + expect(http.body).toEqual(mockCheckout); + }); + }); + + it('should call transformer with proper normalizer', () => { + http.flush(mockPlaceOrderResponse); + service.placeOrder(mockPlaceOrderData).subscribe(); + + expect(mockTransformer.do).toHaveBeenCalledWith( + CheckoutResponseNormalizer + ); + }); + + it('should return transformed data', () => { + mockTransformer.do.mockReturnValue(() => of(mockTransformerData)); + + service.placeOrder(mockPlaceOrderData).subscribe(callback); + + expect(callback).toHaveBeenCalledWith(mockTransformerData); + }); + }); +}); diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/glue-checkout.adapter.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/glue-checkout.adapter.ts new file mode 100644 index 000000000..f59b3df45 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/glue-checkout.adapter.ts @@ -0,0 +1,81 @@ +import { IdentityService } from '@spryker-oryx/auth'; +import { + ApiCheckoutModel, + CheckoutAdapter, + CheckoutData, + CheckoutDataSerializer, + CheckoutNormalizer, + CheckoutResponse, + CheckoutResponseNormalizer, + CheckoutSerializer, + PlaceOrderData, +} from '@spryker-oryx/checkout'; +import { HttpService, JsonAPITransformerService } from '@spryker-oryx/core'; +import { inject } from '@spryker-oryx/di'; +import { Observable, combineLatest, map, switchMap, take } from 'rxjs'; + +export class GlueCheckoutAdapter implements CheckoutAdapter { + constructor( + protected http = inject(HttpService), + protected SCOS_BASE_URL = inject('SCOS_BASE_URL'), + protected transformer = inject(JsonAPITransformerService), + protected identity = inject(IdentityService) + ) {} + + get(props: PlaceOrderData): Observable { + return this.transformer.serialize(props, CheckoutDataSerializer).pipe( + switchMap((data) => + this.http + .post(this.generateUrl(), data) + .pipe( + this.transformer.do(CheckoutNormalizer), + map((data) => { + // Workaround for shipmentTotals = 0 + // if we do not submit shipment method, we don't want to have shipment totals in the response + // as we can't recognize if shipment is calculated or not in case if it's zero + if ( + !props.shipment?.idShipmentMethod && + !data?.carts?.[0]?.totals?.shipmentTotal + ) { + delete data?.carts?.[0]?.totals?.shipmentTotal; + } + return data; + }) + ) + ) + ); + } + + placeOrder(data: PlaceOrderData): Observable { + return combineLatest([ + this.identity.get(), + this.transformer.serialize(data, CheckoutSerializer), + ]).pipe( + take(1), + switchMap(([user, data]) => + this.http + .post( + `${this.SCOS_BASE_URL}/checkout${ + !user.isAuthenticated ? '?include=orders' : '' + }`, + data + ) + .pipe(this.transformer.do(CheckoutResponseNormalizer)) + ) + ); + } + + protected generateUrl( + include: ApiCheckoutModel.Includes[] = [ + ApiCheckoutModel.Includes.Shipments, + ApiCheckoutModel.Includes.ShipmentMethods, + ApiCheckoutModel.Includes.PaymentMethods, + ApiCheckoutModel.Includes.Carts, + ApiCheckoutModel.Includes.GuestCarts, + ] + ): string { + return `${this.SCOS_BASE_URL}/checkout-data${ + include ? `?include=${include.join(',')}` : '' + }`; + } +} diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/index.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/index.ts new file mode 100644 index 000000000..6bbdaeddd --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/index.ts @@ -0,0 +1,3 @@ +export * from './glue-checkout.adapter'; +export * from './normalizers'; +export * from './serializers'; diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout-response.normalizer.spec.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout-response.normalizer.spec.ts new file mode 100644 index 000000000..e60e1ef50 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout-response.normalizer.spec.ts @@ -0,0 +1,15 @@ +import { CheckoutResponse } from '@spryker-oryx/checkout'; +import { checkoutResponseAttributesNormalizer } from './checkout-response.normalizer'; + +describe('Checkout Response Normalizer', () => { + describe('Checkout Response Attributes Normalizer', () => { + it('should pass through the data', () => { + const mockResult = { + orderReference: 'test', + } as CheckoutResponse; + + const normalized = checkoutResponseAttributesNormalizer(mockResult); + expect(normalized).toEqual(mockResult); + }); + }); +}); diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout-response.normalizer.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout-response.normalizer.ts new file mode 100644 index 000000000..a942a1bd6 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout-response.normalizer.ts @@ -0,0 +1,7 @@ +import { CheckoutResponse } from '@spryker-oryx/checkout'; + +export function checkoutResponseAttributesNormalizer( + data: CheckoutResponse +): Partial { + return data ?? {}; +} diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout.normalizer.spec.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout.normalizer.spec.ts new file mode 100644 index 000000000..726b4364e --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout.normalizer.spec.ts @@ -0,0 +1,173 @@ +import { CartsNormalizer } from '@spryker-oryx/cart'; +import { + PaymentsNormalizer, + ShipmentsNormalizer, +} from '@spryker-oryx/checkout'; +import { mockPaymentMethods } from '@spryker-oryx/checkout/mocks'; +import { of, take } from 'rxjs'; +import { + checkoutAttributesNormalizer, + checkoutCartsNormalizer, + checkoutPaymentsNormalizer, + checkoutShipmentsNormalizer, +} from './checkout.normalizer'; +import { DeserializedCheckout } from './model'; + +const mockDeserializedCheckout = { + addresses: [], + id: null, + paymentProviders: [], + selectedPaymentMethods: [], + selectedShipmentMethods: [], + paymentMethods: mockPaymentMethods, + shipmentMethods: [], + shipments: [ + { + id: 'mockid', + items: ['mock-product', 'mock-product2'], + requestedDeliveryDate: null, + selectedShipmentMethod: { + id: 1, + name: 'mock shipment method', + carrierName: 'mock carrier', + price: 123, + taxRate: '19.00', + }, + shipmentMethods: [ + { + id: 2, + name: 'mock shipment method 2', + carrierName: 'mock carrier', + price: 123, + }, + { + id: 1, + name: 'mock shipment method', + carrierName: 'mock carrier', + price: 456, + }, + { + id: 3, + name: 'mock shipment method3', + carrierName: 'mock carrier 2', + price: 100, + }, + ], + shippingAddress: { + address1: null, + address2: null, + address3: null, + city: null, + company: null, + country: null, + firstName: null, + id: null, + idCompanyBusinessUnitAddress: null, + isDefaultBilling: null, + isDefaultShipping: null, + iso2Code: null, + lastName: null, + phone: null, + salutation: null, + zipCode: null, + }, + carts: [ + { + priceMode: 'GROSS_MODE', + currency: 'EUR', + store: 'DE', + name: 'Cart 1687950630732', + isDefault: true, + totals: { + expenseTotal: 0, + discountTotal: 0, + taxTotal: 0, + subtotal: 5699, + grandTotal: 5699, + priceToPay: 5699, + shipmentTotal: 0, + }, + id: 'f7fa58ad-50de-5649-b6a8-c82ee4f71986', + }, + ], + }, + ], +} as unknown as DeserializedCheckout; + +const mockTransformer = { + transform: vi.fn(), + do: vi.fn(), +}; + +describe('Checkout Normalizers', () => { + describe('Checkout Attributes Normalizer', () => { + it('should transform DeserializedCheckout into CheckoutData', () => { + const mockResult = { + addresses: [], + paymentProviders: [], + selectedShipmentMethods: [], + selectedPaymentMethods: [], + }; + + const normalized = checkoutAttributesNormalizer(mockDeserializedCheckout); + expect(normalized).toEqual(mockResult); + }); + }); + + describe('Checkout Shipments Normalizers', () => { + it('should call shipments transformer', () => { + mockTransformer.transform.mockReturnValue( + of(mockDeserializedCheckout.shipments) + ); + checkoutShipmentsNormalizer(mockDeserializedCheckout, mockTransformer) + .pipe(take(1)) + .subscribe((normalized) => { + expect(normalized).toEqual({ + shipments: mockDeserializedCheckout.shipments, + }); + expect(mockTransformer.transform).toHaveBeenLastCalledWith( + mockDeserializedCheckout, + ShipmentsNormalizer + ); + }); + }); + }); + + describe('When payment methods are included', () => { + it('should call payments transformer', () => { + mockTransformer.transform.mockReturnValue( + of(mockDeserializedCheckout.paymentMethods) + ); + checkoutPaymentsNormalizer(mockDeserializedCheckout, mockTransformer) + .pipe(take(1)) + .subscribe((normalized) => { + expect(normalized).toEqual({ + paymentMethods: mockPaymentMethods, + }); + expect(mockTransformer.transform).toHaveBeenLastCalledWith( + mockDeserializedCheckout, + PaymentsNormalizer + ); + }); + }); + }); + + describe('When carts methods are included', () => { + it('should call carts transformer', () => { + mockTransformer.transform.mockReturnValue( + of(mockDeserializedCheckout.carts) + ); + checkoutCartsNormalizer(mockDeserializedCheckout, mockTransformer) + .pipe(take(1)) + .subscribe((normalized) => { + expect(normalized).toEqual({ + carts: mockDeserializedCheckout.carts, + }); + expect(mockTransformer.transform).toHaveBeenLastCalledWith( + mockDeserializedCheckout, + CartsNormalizer + ); + }); + }); + }); +}); diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout.normalizer.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout.normalizer.ts new file mode 100644 index 000000000..d193efa44 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/checkout.normalizer.ts @@ -0,0 +1,56 @@ +import { CartsNormalizer } from '@spryker-oryx/cart'; +import { + CheckoutData, + PaymentsNormalizer, + ShipmentsNormalizer, +} from '@spryker-oryx/checkout'; +import { TransformerService } from '@spryker-oryx/core'; +import { Observable, map } from 'rxjs'; +import { DeserializedCheckout } from './model'; + +export function checkoutAttributesNormalizer( + data: DeserializedCheckout +): Partial { + const { + addresses, + paymentProviders, + selectedShipmentMethods, + selectedPaymentMethods, + carts, + guestCarts, + } = data; + return { + addresses, + paymentProviders, + selectedShipmentMethods, + selectedPaymentMethods, + carts: carts ?? guestCarts, + }; +} + +export function checkoutShipmentsNormalizer( + data: DeserializedCheckout, + transformer: TransformerService +): Observable> { + return transformer + .transform(data, ShipmentsNormalizer) + .pipe(map((shipments) => ({ shipments }))); +} + +export function checkoutPaymentsNormalizer( + data: DeserializedCheckout, + transformer: TransformerService +): Observable> { + return transformer + .transform(data, PaymentsNormalizer) + .pipe(map((paymentMethods) => ({ paymentMethods }))); +} + +export function checkoutCartsNormalizer( + data: DeserializedCheckout, + transformer: TransformerService +): Observable> { + return transformer + .transform(data, CartsNormalizer) + .pipe(map((carts) => ({ carts }))); +} diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/index.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/index.ts new file mode 100644 index 000000000..5cacf6844 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/index.ts @@ -0,0 +1,5 @@ +export * from './checkout-response.normalizer'; +export * from './checkout.normalizer'; +export * from './model'; +export * from './payments.normalizer'; +export * from './shipments.normalizer'; diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/model.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/model.ts new file mode 100644 index 000000000..6b6e67e6d --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/model.ts @@ -0,0 +1,27 @@ +import { ApiCheckoutModel } from '@spryker-oryx/checkout'; +import { CamelCase } from '@spryker-oryx/core/utilities'; + +export type DeserializedCheckoutIncludes = { + [P in ApiCheckoutModel.Includes as `${CamelCase

}`]?: P extends ApiCheckoutModel.Includes.ShipmentMethods + ? ApiCheckoutModel.ShipmentMethod[] + : P extends ApiCheckoutModel.Includes.PaymentMethods + ? ApiCheckoutModel.PaymentMethod[] + : P extends ApiCheckoutModel.Includes.Shipments + ? (ApiCheckoutModel.Shipment & + Pick< + DeserializedCheckoutIncludes, + CamelCase + >)[] + : never; +}; + +export type DeserializedCheckout = ApiCheckoutModel.Attributes & + Pick< + DeserializedCheckoutIncludes, + CamelCase< + | ApiCheckoutModel.Includes.Shipments + | ApiCheckoutModel.Includes.PaymentMethods + | ApiCheckoutModel.Includes.Carts + | ApiCheckoutModel.Includes.GuestCarts + > + >; diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/payments.normalizer.spec.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/payments.normalizer.spec.ts new file mode 100644 index 000000000..444b7595d --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/payments.normalizer.spec.ts @@ -0,0 +1,30 @@ +import { + mockNormalizedPaymentMethods, + mockPaymentMethods, +} from '@spryker-oryx/checkout/mocks'; +import { DeserializedCheckout } from './model'; +import { paymentsNormalizer } from './payments.normalizer'; + +const mockCheckoutData: DeserializedCheckout = { + addresses: [], + paymentProviders: [], + shipmentMethods: [], + selectedShipmentMethods: [], + selectedPaymentMethods: [], + paymentMethods: mockPaymentMethods, +}; + +describe('paymentsNormalizer', () => { + describe('when no data is provided', () => { + it('should return an empty array', () => { + expect(paymentsNormalizer()).toEqual([]); + }); + }); + + describe('when valid data is provided', () => { + it('should transform ApiCheckoutModel.PaymentMethods Include into CheckoutData', () => { + const normalized = paymentsNormalizer(mockCheckoutData); + expect(normalized).toEqual(mockNormalizedPaymentMethods); + }); + }); +}); diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/payments.normalizer.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/payments.normalizer.ts new file mode 100644 index 000000000..d6076b19a --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/payments.normalizer.ts @@ -0,0 +1,30 @@ +import { ApiCheckoutModel, PaymentMethod } from '@spryker-oryx/checkout'; +import { camelize } from '@spryker-oryx/core/utilities'; +import { DeserializedCheckout } from './model'; + +export function paymentsNormalizer( + data?: DeserializedCheckout +): PaymentMethod[] { + if (!data) return []; + + const paymentsKey = camelize(ApiCheckoutModel.Includes.PaymentMethods); + return ( + data[paymentsKey] + ?.map((payment) => { + const { paymentMethodName, paymentProviderName, ...paymentData } = + payment; + return { + ...paymentData, + name: paymentMethodName, + provider: paymentProviderName, + }; + }) + .sort((a, b) => + a.provider.toLowerCase() < b.provider.toLowerCase() + ? -1 + : a.provider.toLowerCase() > b.provider.toLowerCase() + ? 1 + : 0 + ) ?? [] + ); +} diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/shipments.normalizer.spec.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/shipments.normalizer.spec.ts new file mode 100644 index 000000000..c0cc21460 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/shipments.normalizer.spec.ts @@ -0,0 +1,99 @@ +import { Shipment } from '@spryker-oryx/checkout'; +import { DeserializedCheckout } from './model'; +import { shipmentsNormalizer } from './shipments.normalizer'; + +const mockBaseShipment = { + items: ['mock-product', 'mock-product2'], + requestedDeliveryDate: null, + selectedShipmentMethod: { + id: '1', + name: 'mock shipment method', + carrierName: 'mock carrier', + price: 123, + taxRate: '19.00', + currencyIsoCode: 'EUR', + deliveryTime: null, + }, + shippingAddress: { + address1: null, + address2: null, + address3: null, + city: null, + company: null, + country: null, + firstName: null, + id: null, + idCompanyBusinessUnitAddress: null, + isDefaultBilling: null, + isDefaultShipping: null, + iso2Code: null, + lastName: null, + phone: null, + salutation: null, + zipCode: null, + }, +}; + +const mockShipmentMethods = [ + { + id: '2', + name: 'mock shipment method 2', + carrierName: 'mock carrier', + price: 123, + currencyIsoCode: 'EUR', + deliveryTime: null, + }, + { + id: '1', + name: 'mock shipment method', + carrierName: 'mock carrier', + price: 456, + currencyIsoCode: 'EUR', + deliveryTime: null, + }, + { + id: '3', + name: 'mock shipment method3', + carrierName: 'mock carrier 2', + price: 100, + currencyIsoCode: 'EUR', + deliveryTime: null, + }, +]; + +const mockCheckoutData: DeserializedCheckout = { + addresses: [], + paymentProviders: [], + shipmentMethods: [], + selectedShipmentMethods: [], + selectedPaymentMethods: [], + shipments: [{ ...mockBaseShipment, shipmentMethods: mockShipmentMethods }], +}; + +const mockNormalizedShipment: Shipment[] = [ + { + ...mockBaseShipment, + carriers: [ + { + name: 'mock carrier', + shipmentMethods: [mockShipmentMethods[0], mockShipmentMethods[1]], + }, + { name: 'mock carrier 2', shipmentMethods: [mockShipmentMethods[2]] }, + ], + }, +]; + +describe('shipmentsNormalizer', () => { + describe('when no data is provided', () => { + it('should return an empty array', () => { + expect(shipmentsNormalizer()).toEqual([]); + }); + }); + + describe('when valid data is provided', () => { + it('should transform ApiCheckoutModel.ShipmentInclude into CheckoutData', () => { + const normalized = shipmentsNormalizer(mockCheckoutData); + expect(normalized).toEqual(mockNormalizedShipment); + }); + }); +}); diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/shipments.normalizer.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/shipments.normalizer.ts new file mode 100644 index 000000000..c134f661e --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/normalizers/shipments.normalizer.ts @@ -0,0 +1,35 @@ +import { + ApiCheckoutModel, + Shipment, + ShipmentMethod, +} from '@spryker-oryx/checkout'; +import { camelize } from '@spryker-oryx/core/utilities'; +import { DeserializedCheckout } from './model'; + +export function shipmentsNormalizer(data?: DeserializedCheckout): Shipment[] { + if (!data) return []; + + const shipmentsKey = camelize(ApiCheckoutModel.Includes.Shipments); + const shipments = data[shipmentsKey] ?? []; + + return shipments.map((shipment) => { + const methods: Record = {}; + + if (typeof shipment === 'object' && shipment.shipmentMethods) { + for (let i = 0; i < shipment.shipmentMethods.length; i++) { + const method: ShipmentMethod = shipment.shipmentMethods[i]; + const carrier = method.carrierName; + methods[carrier] = [...(methods[carrier] ?? []), method]; + } + } + + delete shipment.shipmentMethods; + + return { + ...shipment, + carriers: Object.keys(methods).map((carrier) => { + return { name: carrier, shipmentMethods: methods[carrier] }; + }), + }; + }); +} diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout-data.serializer.spec.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout-data.serializer.spec.ts new file mode 100644 index 000000000..bece40b06 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout-data.serializer.spec.ts @@ -0,0 +1,24 @@ +import { checkoutDataAttributesSerializer } from './checkout-data.serializer'; +import { checkoutAttributesSerializer } from './checkout.serializer'; + +const mockUpdateCheckoutDataProps = { + user: { anonymousUserId: 'mockid' }, + cartId: 'mockcart', + shipments: [], +}; + +describe('Checkout Data Serializers', () => { + describe('Checkout Data Attributes Serializer', () => { + it('should reuse checkout AttributesSerializer', () => { + const mockResult = { + ...checkoutAttributesSerializer(mockUpdateCheckoutDataProps), + type: 'checkout-data', + }; + + const serialized = checkoutDataAttributesSerializer( + mockUpdateCheckoutDataProps + ); + expect(serialized).toEqual(mockResult); + }); + }); +}); diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout-data.serializer.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout-data.serializer.ts new file mode 100644 index 000000000..84adf5b14 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout-data.serializer.ts @@ -0,0 +1,11 @@ +import { ApiCheckoutModel, PlaceOrderData } from '@spryker-oryx/checkout'; +import { checkoutAttributesSerializer } from './checkout.serializer'; + +export function checkoutDataAttributesSerializer( + data: PlaceOrderData +): Partial { + return { + ...checkoutAttributesSerializer(data), + type: 'checkout-data', + }; +} diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout.serializer.spec.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout.serializer.spec.ts new file mode 100644 index 000000000..9d5db3992 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout.serializer.spec.ts @@ -0,0 +1,12 @@ +import { mockCheckout, mockPlaceOrderData } from '@spryker-oryx/checkout/mocks'; +import { checkoutAttributesSerializer } from './checkout.serializer'; + +describe('Checkout Serializers', () => { + describe('Checkout Attributes Serializer', () => { + it('should transform PostCheckoutProps into Payload', () => { + const serialized = checkoutAttributesSerializer(mockPlaceOrderData); + + expect(serialized).toEqual(mockCheckout); + }); + }); +}); diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout.serializer.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout.serializer.ts new file mode 100644 index 000000000..cdb090760 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/checkout.serializer.ts @@ -0,0 +1,25 @@ +import { ApiCheckoutModel, PlaceOrderData } from '@spryker-oryx/checkout'; + +export function checkoutAttributesSerializer( + data: PlaceOrderData +): Partial { + const { cartId, payments, ...attributeData } = data; + + const serializedPayments = payments?.map((payment) => { + const { name, provider, ...paymentData } = payment; + return { + ...paymentData, + paymentMethodName: name, + paymentProviderName: provider, + }; + }); + + return { + type: 'checkout', + attributes: { + ...attributeData, + idCart: cartId, + payments: serializedPayments, + }, + }; +} diff --git a/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/index.ts b/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/index.ts new file mode 100644 index 000000000..69e636fb1 --- /dev/null +++ b/libs/domain/checkout/services/src/adapter/spryker-glue/serializers/index.ts @@ -0,0 +1,2 @@ +export * from './checkout-data.serializer'; +export * from './checkout.serializer'; diff --git a/libs/domain/checkout/src/checkout.providers.ts b/libs/domain/checkout/src/checkout.providers.ts index aa2a897d9..a9da753ae 100644 --- a/libs/domain/checkout/src/checkout.providers.ts +++ b/libs/domain/checkout/src/checkout.providers.ts @@ -191,7 +191,7 @@ export const checkoutProviders = provide: CheckoutAdapter, asyncClass: () => import('@spryker-oryx/checkout/services').then( - (m) => m.DefaultCheckoutAdapter + (m) => m.GlueCheckoutAdapter ), }, { diff --git a/libs/domain/checkout/src/services-reexports.ts b/libs/domain/checkout/src/services-reexports.ts index 6b534829f..4fae5af1d 100644 --- a/libs/domain/checkout/src/services-reexports.ts +++ b/libs/domain/checkout/src/services-reexports.ts @@ -7,7 +7,7 @@ const reexports: typeof services = /** @deprecated since 1.2, use DefaultCheckoutService from @spryker-oryx/checkout/services */ export const DefaultCheckoutService = reexports?.DefaultCheckoutService; /** @deprecated since 1.2, use DefaultCheckoutAdapter from @spryker-oryx/checkout/services */ -export const DefaultCheckoutAdapter = reexports?.DefaultCheckoutAdapter; +export const DefaultCheckoutAdapter = reexports?.GlueCheckoutAdapter; /** @deprecated since 1.2, use DefaultCheckoutDataService from @spryker-oryx/checkout/services */ export const DefaultCheckoutDataService = reexports?.DefaultCheckoutDataService; /** @deprecated since 1.2, use DefaultCheckoutStateService from @spryker-oryx/checkout/services */ diff --git a/libs/domain/product/src/feature.ts b/libs/domain/product/src/feature.ts index 2a5aaff05..232d66ac6 100644 --- a/libs/domain/product/src/feature.ts +++ b/libs/domain/product/src/feature.ts @@ -1,6 +1,10 @@ import { AppFeature } from '@spryker-oryx/core'; import * as components from './components'; -import { productProviders } from './services'; +import { + glueProductProviders, + mockProductProviders, + productProviders, +} from './services'; export * from './components'; export const productComponents = Object.values(components); @@ -9,3 +13,13 @@ export const productFeature: AppFeature = { providers: productProviders, components: productComponents, }; + +export const glueProductFeature: AppFeature = { + providers: glueProductProviders, + components: productComponents, +}; + +export const mockProductFeature: AppFeature = { + providers: mockProductProviders, + components: productComponents, +}; diff --git a/libs/domain/product/src/mocks/src/mock-product.providers.ts b/libs/domain/product/src/mocks/src/mock-product.providers.ts index dd1f52e40..37f137cd4 100644 --- a/libs/domain/product/src/mocks/src/mock-product.providers.ts +++ b/libs/domain/product/src/mocks/src/mock-product.providers.ts @@ -1,10 +1,10 @@ import { provideEntity } from '@spryker-oryx/core'; import { Provider } from '@spryker-oryx/di'; import { - DefaultProductAdapter, DefaultProductImageService, DefaultProductListPageService, DefaultProductListService, + GlueProductAdapter, PRODUCT, ProductAdapter, ProductCategoryService, @@ -12,10 +12,10 @@ import { ProductListAdapter, ProductListPageService, ProductListService, - productMediaConfig, ProductMediaConfig, ProductRelationsListService, ProductService, + productMediaConfig, } from '@spryker-oryx/product'; import { MockProductCategoryService } from './mock-category.service'; import { MockProductService } from './mock-product.service'; @@ -25,7 +25,7 @@ import { MockProductRelationsListService } from './product-relations/mock-produc export const mockProductProviders: Provider[] = [ { provide: ProductAdapter, - useClass: DefaultProductAdapter, + useClass: GlueProductAdapter, }, { provide: ProductService, diff --git a/libs/domain/product/src/mocks/src/product-list/mock-product-list.adapter.ts b/libs/domain/product/src/mocks/src/product-list/mock-product-list.adapter.ts index 187b94f0c..37f5e7891 100644 --- a/libs/domain/product/src/mocks/src/product-list/mock-product-list.adapter.ts +++ b/libs/domain/product/src/mocks/src/product-list/mock-product-list.adapter.ts @@ -3,8 +3,8 @@ import { ProductListAdapter, ProductListQualifier, } from '@spryker-oryx/product'; -import { generateFacet, generateRange } from '@spryker-oryx/product/mocks'; import { Observable, of } from 'rxjs'; +import { generateFacet, generateRange } from './mock-facet.generator'; import { createProductListMock } from './mock-product-list.generator'; export class MockProductListAdapter implements ProductListAdapter { diff --git a/libs/domain/product/src/mocks/src/product-relations/mock-product-relations-list.service.ts b/libs/domain/product/src/mocks/src/product-relations/mock-product-relations-list.service.ts index 12f759404..fc5ded86d 100644 --- a/libs/domain/product/src/mocks/src/product-relations/mock-product-relations-list.service.ts +++ b/libs/domain/product/src/mocks/src/product-relations/mock-product-relations-list.service.ts @@ -3,8 +3,8 @@ import { ProductQualifier, ProductRelationsListService, } from '@spryker-oryx/product'; -import { MockProductService } from '@spryker-oryx/product/mocks'; import { Observable, of } from 'rxjs'; +import { MockProductService } from '../mock-product.service'; export class MockProductRelationsListService implements ProductRelationsListService diff --git a/libs/domain/product/src/services/adapter/index.ts b/libs/domain/product/src/services/adapter/index.ts index 8473712ce..c49b6129c 100644 --- a/libs/domain/product/src/services/adapter/index.ts +++ b/libs/domain/product/src/services/adapter/index.ts @@ -1,4 +1,2 @@ -export * from './default-product.adapter'; -export * from './normalizers'; -export * from './product-includes'; -export * from './product.adapter'; +export * from './spryker-glue'; +// export * from './mock'; diff --git a/libs/domain/product/src/services/adapter/mock/index.ts b/libs/domain/product/src/services/adapter/mock/index.ts new file mode 100644 index 000000000..e035441dd --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/index.ts @@ -0,0 +1,2 @@ +export * from './product-list'; +export * from './product-relations'; diff --git a/libs/domain/product/src/services/adapter/mock/mock-category.adapter.ts b/libs/domain/product/src/services/adapter/mock/mock-category.adapter.ts new file mode 100644 index 000000000..158fe3114 --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/mock-category.adapter.ts @@ -0,0 +1,29 @@ +import { + ProductCategory, + ProductCategoryAdapter, + ProductCategoryQualifier, +} from '@spryker-oryx/product'; +import { Observable, of } from 'rxjs'; +import { mockProductsCategories } from './mock-category'; + +export class MockProductCategoryAdapter implements ProductCategoryAdapter { + get( + qualifier: string | ProductCategoryQualifier + ): Observable { + if (typeof qualifier === 'string') return this.get({ id: qualifier }); + const category = mockProductsCategories.find( + (p) => p.id === qualifier.id + ) as ProductCategory; + return of(category); + } + + getTree(qualifier?: ProductCategoryQualifier): Observable { + return of( + mockProductsCategories.filter((category) => { + return !qualifier?.parent + ? !category.parent + : category.parent === qualifier?.parent; + }) + ); + } +} diff --git a/libs/domain/product/src/services/adapter/mock/mock-category.ts b/libs/domain/product/src/services/adapter/mock/mock-category.ts new file mode 100644 index 000000000..ec52749bb --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/mock-category.ts @@ -0,0 +1,11 @@ +import { ProductCategory } from '@spryker-oryx/product'; + +export const mockProductsCategories: ProductCategory[] = [ + { id: '2', name: 'Cameras & Camcorders', order: 0 }, + { id: '5', name: 'Computer', order: 0 }, + { id: '6', name: 'Notebooks', parent: '5', order: 0 }, + { id: '7', name: "Pc's/workstations", parent: '5', order: 0 }, + { id: '8', name: 'Tablets', parent: '5', order: 0 }, + { id: '9', name: 'Smart Wearables', order: 0 }, + { id: '11', name: 'Telecom & Navigation', order: 0 }, +]; diff --git a/libs/domain/product/src/services/adapter/mock/mock-product.adapter.ts b/libs/domain/product/src/services/adapter/mock/mock-product.adapter.ts new file mode 100644 index 000000000..2b596a358 --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/mock-product.adapter.ts @@ -0,0 +1,18 @@ +import { Product, ProductQualifier } from '@spryker-oryx/product'; +import { Observable, of } from 'rxjs'; +import { ProductAdapter } from '../../index'; +import { mockProducts } from './mock-product'; + +export class MockProductAdapter implements ProductAdapter { + getKey(qualifier: ProductQualifier): string { + return (qualifier.sku ?? '') + qualifier.include?.sort()?.join(''); + } + + get(qualifier: ProductQualifier): Observable { + const product = mockProducts.find( + (p) => p.sku === qualifier.sku + ) as Product; + + return of(product); + } +} diff --git a/libs/domain/product/src/services/adapter/mock/mock-product.ts b/libs/domain/product/src/services/adapter/mock/mock-product.ts new file mode 100644 index 000000000..638b72f6b --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/mock-product.ts @@ -0,0 +1,324 @@ +import { Product, ProductLabel, ProductMediaSet } from '@spryker-oryx/product'; + +export const enum ProductLabelAppearance { + Highlight = 'error', + Info = 'info', +} + +const img1 = { + sm: 'https://images.icecat.biz/img/gallery_mediums/29885545_9575.jpg', + lg: 'https://images.icecat.biz/img/gallery/29885545_9575.jpg', +}; +const img2 = { + sm: 'https://images.icecat.biz/img/norm/medium/26138343-5454.jpg', + lg: 'https://images.icecat.biz/img/norm/high/26138343-5454.jpg', +}; + +const img3 = { + sm: 'https://images.icecat.biz/img/gallery_mediums/30663301_9631.jpg', + lg: 'https://images.icecat.biz/img/gallery/30663301_9631.jpg', +}; + +const images = [ + { + ...img1, + xl: img1.lg, + externalUrlLarge: img1.lg, + externalUrlSmall: img1.sm, + }, + { + ...img2, + xl: img2.lg, + externalUrlLarge: img2.lg, + externalUrlSmall: img2.sm, + }, + { + ...img3, + xl: img3.lg, + externalUrlLarge: img3.lg, + externalUrlSmall: img3.sm, + }, +]; + +const mediaSet: ProductMediaSet[] = [ + { + name: 'default', + media: images, + }, +]; + +const newLabel: ProductLabel = { + name: 'New', + appearance: ProductLabelAppearance.Highlight, +}; + +const saleLabel: ProductLabel = { + name: 'sale', + appearance: ProductLabelAppearance.Info, +}; + +export const mockProducts: Product[] = [ + { + sku: '1', + name: 'Sample product', + mediaSet, + description: `Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.`, + price: { + defaultPrice: { + currency: 'EUR', + value: 1879, + isNet: true, + }, + originalPrice: { + currency: 'EUR', + value: 2879, + isNet: true, + }, + }, + averageRating: 2, + reviewCount: 5, + + attributes: { + brand: 'Brand1', + color: 'color1', + }, + attributeNames: { + brand: 'Brand', + color: 'Color', + }, + labels: [newLabel, saleLabel], + availability: { + quantity: 3, + isNeverOutOfStock: false, + availability: true, + }, + categoryIds: ['category id 1'], + }, + { + sku: '2', + name: 'Second sample product', + mediaSet: [], + description: `Lorem ipsum dolor sit amet, consectetur adipisicing elit. + Lorem ipsum dolor sit amet, consectetur adipisicing elit. + + Lorem ipsum dolor sit amet, consectetur adipisicing elit + + Lorem ipsum dolor sit amet, consectetur adipisicing elit + Lorem ipsum dolor sit amet, consectetur adipisicing elit. + `, + price: { + defaultPrice: { + value: 1095, + currency: 'EUR', + isNet: false, + }, + }, + averageRating: 2.5, + reviewCount: 175, + + attributes: { + brand: 'Brand2', + color: 'color2', + }, + attributeNames: { + brand: 'Brand', + color: 'Color', + }, + labels: [newLabel], + availability: { + isNeverOutOfStock: true, + quantity: 0, + availability: false, + }, + categoryIds: ['category id 2'], + }, + { + sku: '3', + name: 'Sample product no. 3', + mediaSet: [ + { + name: 'default', + media: [...images, ...images, ...images], + }, + ], + averageRating: undefined, + + attributes: { + brand: 'Brand3', + color: 'color3', + }, + attributeNames: { + brand: 'Brand', + color: 'Color', + }, + availability: { + isNeverOutOfStock: false, + quantity: 10, + availability: true, + }, + }, + { + sku: '4', + name: 'Sample product no. 4', + description: 'Lorem', + price: { + defaultPrice: { + value: 1700, + isNet: false, + currency: 'EUR', + }, + originalPrice: { + value: 1900, + isNet: false, + currency: 'EUR', + }, + }, + averageRating: 1, + reviewCount: undefined, + mediaSet, + + attributes: { + brand: 'Brand4', + color: 'color4', + }, + attributeNames: { + brand: 'Brand', + color: 'Color', + }, + }, + { + sku: '5', + name: 'Sample product no. 5', + description: 'Lorem sample', + price: { + defaultPrice: { + value: 1879, + isNet: true, + currency: 'EUR', + }, + originalPrice: { + value: 1779, + isNet: true, + currency: 'EUR', + }, + }, + mediaSet, + attributes: { + brand: 'Brand5', + color: 'color5', + }, + attributeNames: { + brand: 'Brand', + color: 'Color', + }, + }, + { + sku: '6', + name: 'Sample product no. 6 Sample product no. 6 Sample product no. 6 Sample product no. 6 Sample product no. 6 Sample product no. 6', + description: 'Lorem ipsum dolor\nsit amet.', + price: { + defaultPrice: { + value: 1879, + isNet: true, + currency: 'EUR', + }, + originalPrice: { + value: 1779, + isNet: true, + currency: 'EUR', + }, + }, + mediaSet, + }, + { + sku: '7', + name: 'Sample product no. 7', + description: 'Lorem ipsum dolor sit amet', + price: { + defaultPrice: { + value: 1900, + isNet: true, + currency: 'EUR', + }, + originalPrice: { + value: 1958, + isNet: true, + currency: 'EUR', + }, + }, + mediaSet, + attributes: { + brand: 'Brand7', + color: 'color7', + }, + attributeNames: { + brand: 'Brand', + color: 'Color', + }, + }, + { + sku: '8', + name: 'Sample product no. 8', + description: 'Lorem ipsum dolor sit amet.', + price: { + defaultPrice: { + value: 1900, + isNet: true, + currency: 'EUR', + }, + originalPrice: { + value: 1958, + isNet: true, + currency: 'EUR', + }, + }, + mediaSet, + attributes: { + brand: 'Brand8', + SampleAttribute: + 'Sample attribute lengthy value, Sample attribute lengthy value, Sample attribute lengthy value.', + }, + attributeNames: { + brand: 'Brand', + SampleAttribute: + 'Sample attribute lengthy name, Sample attribute lengthy name, Sample attribute lengthy name.', + }, + }, + + { + sku: 'single-image', + name: 'Sample product with one image', + description: 'Lorem ipsum dolor sit amet.', + mediaSet: [ + { + name: 'default', + media: [images[0]], + }, + ], + }, + { + sku: 'without-images', + name: 'Sample product without images', + description: + 'Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet.', + price: { + defaultPrice: { + value: 1879, + isNet: true, + currency: 'EUR', + }, + }, + mediaSet: [], + }, + { + sku: 'discontinued', + discontinued: true, + }, + { + sku: 'discontinued-with-note', + discontinued: true, + discontinuedNote: 'This product is discontinued...', + }, +]; diff --git a/libs/domain/product/src/services/adapter/mock/product-list/index.ts b/libs/domain/product/src/services/adapter/mock/product-list/index.ts new file mode 100644 index 000000000..c4a184eb5 --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/product-list/index.ts @@ -0,0 +1,3 @@ +export * from './mock-facet.generator'; +export * from './mock-product-list.adapter'; +export * from './mock-product-list.generator'; diff --git a/libs/domain/product/src/services/adapter/mock/product-list/mock-facet.generator.ts b/libs/domain/product/src/services/adapter/mock/product-list/mock-facet.generator.ts new file mode 100644 index 000000000..b5ea0d658 --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/product-list/mock-facet.generator.ts @@ -0,0 +1,65 @@ +import { + FacetType, + FacetValue, + RangeFacet, + ValueFacet, +} from '@spryker-oryx/product'; + +export function generateValues( + count: number, + prefix = 'Mock', + selectedValues?: string[], + children?: boolean +): FacetValue[] { + const values: FacetValue[] = []; + + for (let i = 0; i < count; i++) { + values.push({ + value: `${prefix}${i}`, + selected: + (selectedValues && selectedValues?.indexOf(`${prefix}${i}`) >= 0) ?? + false, + count: Number(`${i + 1}0`), + name: `${prefix}${i}`, + ...(children ? { children: generateValues(3, `Sub-${prefix}`) } : {}), + }); + } + + return values; +} + +export const generateFacet = ( + name: string, + parameter: string, + valuesLength: number, + selectedValues?: string[], + children = false +): ValueFacet => { + return { + name, + parameter, + valuesTreeLength: valuesLength, + ...(selectedValues && { selectedValues }), + values: generateValues(valuesLength, name, selectedValues, children), + type: FacetType.Single, + }; +}; + +export const generateRange = ( + name: string, + parameter: string, + range: number[], + selected?: number[] +): RangeFacet => { + const [min, max] = range; + return { + name, + parameter, + values: { + min, + max, + selected: { min: +(selected?.[0] ?? min), max: +(selected?.[1] ?? max) }, + }, + type: FacetType.Range, + }; +}; diff --git a/libs/domain/product/src/services/adapter/mock/product-list/mock-product-list.adapter.ts b/libs/domain/product/src/services/adapter/mock/product-list/mock-product-list.adapter.ts new file mode 100644 index 000000000..37f5e7891 --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/product-list/mock-product-list.adapter.ts @@ -0,0 +1,89 @@ +import { + ProductList, + ProductListAdapter, + ProductListQualifier, +} from '@spryker-oryx/product'; +import { Observable, of } from 'rxjs'; +import { generateFacet, generateRange } from './mock-facet.generator'; +import { createProductListMock } from './mock-product-list.generator'; + +export class MockProductListAdapter implements ProductListAdapter { + protected readonly alias: Record = { + minPrice: 'price[min]', + maxPrice: 'price[max]', + storageCapacity: 'storage_capacity[]', + minRating: 'rating[min]', + }; + + getKey(qualifier: ProductListQualifier): string { + const qualifierKeys = Object.keys(qualifier); + + return qualifierKeys.length + ? qualifierKeys + .reduce((params: string[], key) => { + const qualifierKey = key as keyof ProductListQualifier; + const param = qualifier[qualifierKey]; + + if (param) { + params.push( + `${this.alias[qualifierKey] ?? qualifierKey}=${param}` + ); + } + + return params; + }, []) + .join('&') + : ''; + } + + get(qualifier: ProductListQualifier): Observable { + return of({ + ...createProductListMock(qualifier), + pagination: { + itemsPerPage: 10, + currentPage: 1, + maxPage: 10, + numFound: 100, + }, + facets: [ + generateFacet('Brand', 'brand', 15, qualifier.brand?.split(',')), + generateFacet( + 'Category', + 'category', + 10, + qualifier.category?.split(',') + ), + generateFacet('Label', 'label', 3, qualifier.label?.split(','), true), + generateFacet('Color', 'color', 6, qualifier.color?.split(',')), + generateRange( + 'Range', + 'range', + [0, 100], + 'minPrice' in qualifier && 'maxPrice' in qualifier + ? [qualifier.minPrice!, qualifier.maxPrice!] + : [] + ), + generateRange( + 'Price', + 'price', + [0, 100000], + 'minPrice' in qualifier && 'maxPrice' in qualifier + ? [qualifier.minPrice! * 100, qualifier.maxPrice! * 100] + : [] + ), + generateRange( + 'Rating', + 'rating', + [0, 5], + 'minRating' in qualifier ? [qualifier.minRating!] : [] + ), + generateRange( + 'Selected rating', + 'selected-rating', + [0, 5], + [qualifier.minRating ?? 3] + ), + ], + }); + } +} diff --git a/libs/domain/product/src/services/adapter/mock/product-list/mock-product-list.generator.ts b/libs/domain/product/src/services/adapter/mock/product-list/mock-product-list.generator.ts new file mode 100644 index 000000000..7a98d67f4 --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/product-list/mock-product-list.generator.ts @@ -0,0 +1,24 @@ +import { + Product, + ProductList, + ProductListQualifier, +} from '@spryker-oryx/product'; +import { mockProducts } from '../mock-product'; + +const createProducts = (qualifier: ProductListQualifier): Product[] => { + const listLength = qualifier?.ipp || 12; + + if (listLength > mockProducts.length) { + return mockProducts; + } + + return [...mockProducts].splice(0, listLength); +}; + +export const createProductListMock = ( + productListQualifier: ProductListQualifier +): ProductList => { + return { + products: createProducts(productListQualifier), + }; +}; diff --git a/libs/domain/product/src/services/adapter/mock/product-relations/index.ts b/libs/domain/product/src/services/adapter/mock/product-relations/index.ts new file mode 100644 index 000000000..9dfd5c7e0 --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/product-relations/index.ts @@ -0,0 +1 @@ +export * from './mock-product-relations-list.service'; diff --git a/libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.adapter.ts b/libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.adapter.ts new file mode 100644 index 000000000..7f98cdea3 --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.adapter.ts @@ -0,0 +1,19 @@ +import { + Product, + ProductQualifier, + ProductRelationsListAdapter, +} from '@spryker-oryx/product'; +import { Observable, of } from 'rxjs'; +import { mockProducts } from '../mock-product'; + +export class MockProductRelationsListAdapter + implements ProductRelationsListAdapter +{ + get({ sku }: ProductQualifier): Observable { + return of([ + mockProducts[Number(sku) - 1], + mockProducts[Number(sku)], + mockProducts[Number(sku) + 1], + ]); + } +} diff --git a/libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.service.ts b/libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.service.ts new file mode 100644 index 000000000..d8c668e4a --- /dev/null +++ b/libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.service.ts @@ -0,0 +1,19 @@ +import { + Product, + ProductQualifier, + ProductRelationsListService, +} from '@spryker-oryx/product'; +import { Observable, of } from 'rxjs'; +import { mockProducts } from '../mock-product'; + +export class MockProductRelationsListService + implements ProductRelationsListService +{ + get({ sku }: ProductQualifier): Observable { + return of([ + mockProducts[Number(sku) - 1], + mockProducts[Number(sku)], + mockProducts[Number(sku) + 1], + ]); + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.spec.ts new file mode 100644 index 000000000..ea4617cf3 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.spec.ts @@ -0,0 +1,166 @@ +import { + HttpService, + JsonApiIncludeService, + JsonAPITransformerService, +} from '@spryker-oryx/core'; +import { HttpTestService } from '@spryker-oryx/core/testing'; +import { createInjector, destroyInjector } from '@spryker-oryx/di'; +import { featureVersion } from '@spryker-oryx/utilities'; +import { of } from 'rxjs'; +import { ApiProductModel } from '../../../models'; +import { ProductNormalizer } from './normalizers'; +import { ProductAdapter } from './product.adapter'; + +const mockApiUrl = 'mockApiUrl'; +const mockProduct = { + data: { + attributes: { + name: 'mockProduct', + sku: 'sku', + averageRating: 0, + }, + }, +}; +const mockTransformer = { + do: vi.fn().mockReturnValue(() => of(null)), +}; + +class MockJsonApiIncludeService implements Partial { + get() { + return of(''); + } +} + +describe('GlueProductService', () => { + let service: ProductAdapter; + let http: HttpTestService; + + beforeEach(() => { + const testInjector = createInjector({ + providers: [ + { + provide: HttpService, + useClass: HttpTestService, + }, + { + provide: ProductAdapter, + useClass: DefaultProductAdapter, + }, + { + provide: 'SCOS_BASE_URL', + useValue: mockApiUrl, + }, + { + provide: JsonAPITransformerService, + useValue: mockTransformer, + }, + { + provide: JsonApiIncludeService, + useClass: MockJsonApiIncludeService, + }, + ], + }); + + service = testInjector.inject(ProductAdapter); + http = testInjector.inject(HttpService) as HttpTestService; + }); + + afterEach(() => { + destroyInjector(); + }); + + it('should be provided', () => { + expect(service).toBeInstanceOf(DefaultProductAdapter); + }); + + describe('get', () => { + const mockQualifier = { sku: '123' }; + const imageInclude = 'concrete-product-image-sets'; + const testInclude = 'test-include'; + + beforeEach(() => { + http.flush(mockProduct); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should build url based on SKU', () => { + service.get(mockQualifier); + + expect(http.url).toContain( + `${mockApiUrl}/concrete-products/${mockQualifier.sku}` + ); + }); + + it('should add include to the url', () => { + service.get({ + ...mockQualifier, + include: [imageInclude], + }); + + expect(http.url?.split('?include=')[1].split(',')).toContain( + imageInclude + ); + }); + + it('should add several includes to the url', () => { + const params = { + ...mockQualifier, + include: [imageInclude, testInclude], + }; + + service.get(params); + + const includes = http.url + ?.split('?include=')[1] + .split('&fields')[0] + .split(','); + expect(includes).toHaveLength(7); + }); + + it('should call transformer with proper normalizer', () => { + service.get(mockQualifier).subscribe(); + + expect(mockTransformer.do).toHaveBeenCalledWith(ProductNormalizer); + }); + + it('should return transformed data', () => { + const mockTransformerData = 'mockTransformerData'; + const callback = vi.fn(); + mockTransformer.do.mockReturnValue(() => of(mockTransformerData)); + + service.get(mockQualifier).subscribe(callback); + + expect(callback).toHaveBeenCalledWith(mockTransformerData); + }); + + if (featureVersion >= '1.1') { + describe('category-nodes fields', () => { + const fields = `fields[${ApiProductModel.Includes.CategoryNodes}]=`; + + beforeEach(() => { + service.get(mockQualifier); + }); + + it('should add fields for category-nodes to the url', () => { + expect(http.url).toContain(fields); + }); + + [ + ApiProductModel.CategoryNodeFields.MetaDescription, + ApiProductModel.CategoryNodeFields.NodeId, + ApiProductModel.CategoryNodeFields.Order, + ApiProductModel.CategoryNodeFields.Name, + ApiProductModel.CategoryNodeFields.Parents, + ApiProductModel.CategoryNodeFields.IsActive, + ].forEach((field) => + it(`should contain ${field} in the url`, () => { + expect(http.url?.split(fields)[1]).toContain(field); + }) + ); + }); + } + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.ts b/libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.ts new file mode 100644 index 000000000..ad874ffca --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.ts @@ -0,0 +1,78 @@ +import { + HttpService, + JsonApiIncludeService, + JsonAPITransformerService, +} from '@spryker-oryx/core'; +import { inject } from '@spryker-oryx/di'; +import { featureVersion } from '@spryker-oryx/utilities'; +import { Observable, switchMap } from 'rxjs'; +import { PRODUCT } from '../../../entity'; +import { ApiProductModel, Product, ProductQualifier } from '../../../models'; +import { ProductNormalizer } from './normalizers'; +import { ProductAdapter } from './product.adapter'; + +export class GlueProductAdapter implements ProductAdapter { + protected productEndpoint = 'concrete-products'; + + constructor( + protected http = inject(HttpService), + protected SCOS_BASE_URL = inject('SCOS_BASE_URL'), + protected transformer = inject(JsonAPITransformerService), + protected includeService = inject(JsonApiIncludeService) + ) {} + + getKey(qualifier: ProductQualifier): string { + return (qualifier.sku ?? '') + qualifier.include?.sort()?.join(''); + } + + get({ sku, include }: ProductQualifier): Observable { + if (featureVersion >= '1.4') { + return this.includeService.get({ resource: PRODUCT }).pipe( + switchMap((includes) => + this.http.get( + `${this.SCOS_BASE_URL}/${this.productEndpoint}/${sku}${ + includes ? `?${includes}` : '' + }` + ) + ), + this.transformer.do(ProductNormalizer) + ); + } else { + include = [ + ApiProductModel.Includes.ConcreteProductImageSets, + ApiProductModel.Includes.ConcreteProductPrices, + ApiProductModel.Includes.ConcreteProductAvailabilities, + ApiProductModel.Includes.Labels, + ApiProductModel.Includes.AbstractProducts, + ApiProductModel.Includes.CategoryNodes, + ...(include ?? []), + ].filter((type, index, arr) => arr.indexOf(type) === index); + + const categoryNodeFields = [ + ApiProductModel.CategoryNodeFields.MetaDescription, + ApiProductModel.CategoryNodeFields.NodeId, + ApiProductModel.CategoryNodeFields.Order, + ApiProductModel.CategoryNodeFields.Name, + ApiProductModel.CategoryNodeFields.Parents, + ApiProductModel.CategoryNodeFields.IsActive, + ]; + + const fields = + featureVersion >= '1.1' + ? `&fields[${ + ApiProductModel.Includes.CategoryNodes + }]=${categoryNodeFields.join(',')} + ` + : ''; + + return this.http + .get( + `${this.SCOS_BASE_URL}/${this.productEndpoint}/${sku}${ + include ? '?include=' : '' + }${include?.join(',') || ''} + ${fields}` + ) + .pipe(this.transformer.do(ProductNormalizer)); + } + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/index.ts new file mode 100644 index 000000000..6dcb58610 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/index.ts @@ -0,0 +1,4 @@ +export * from './glue-product.adapter'; +export * from './normalizers'; +export * from './product-includes'; +export * from './product.adapter'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/availability.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/availability.normalizer.spec.ts new file mode 100644 index 000000000..7b5be355f --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/availability.normalizer.spec.ts @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { availabilityNormalizer } from './availability.normalizer'; +import { DeserializedAvailability } from './model'; + +const mockAvailability: DeserializedAvailability = { + id: 'test', + isNeverOutOfStock: false, + availability: false, + quantity: '10', +} as DeserializedAvailability; + +describe('Product Availability Normalizer', () => { + it('should transform ApiProductModel.ProductAvailability into ProductAvailability', () => { + const mockTransformed = { + isNeverOutOfStock: false, + availability: false, + quantity: Number(mockAvailability.quantity), + }; + const normalized = availabilityNormalizer(mockAvailability); + + expect(normalized).toEqual(mockTransformed); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/availability.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/availability.normalizer.ts new file mode 100644 index 000000000..134f77e4d --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/availability.normalizer.ts @@ -0,0 +1,26 @@ +import { Transformer } from '@spryker-oryx/core'; +import { ProductAvailability } from '../../../../../models/product.model'; +import { DeserializedAvailability } from './model'; + +export const AvailabilityNormalizer = 'oryx.AvailabilityNormalizer*'; + +export function availabilityNormalizer( + data: DeserializedAvailability | undefined +): ProductAvailability | undefined { + if (!data) { + return; + } + + const { id, ...availability } = data; + + return { + ...availability, + quantity: Number(availability.quantity), + }; +} + +declare global { + interface InjectionTokensContractMap { + [AvailabilityNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/index.ts new file mode 100644 index 000000000..58600e8e6 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/index.ts @@ -0,0 +1,2 @@ +export * from './availability.normalizer'; +export * from './model'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/model.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/model.ts new file mode 100644 index 000000000..c2163d69b --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/availability/model.ts @@ -0,0 +1,8 @@ +import { ApiProductModel } from '../../../../../models'; + +interface AvailabilityIncludeId { + id: string; +} + +export type DeserializedAvailability = AvailabilityIncludeId & + ApiProductModel.ProductAvailability; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/category-id.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/category-id.normalizer.spec.ts new file mode 100644 index 000000000..ca75a298a --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/category-id.normalizer.spec.ts @@ -0,0 +1,20 @@ +import { ApiProductModel } from '../../../../../models'; +import { categoryIdNormalizer } from './category-id.normalizer'; + +describe('Product Node Normalizer', () => { + it('should transform ApiProductModel.CategoryNodes[] into DeserializeCategoryIds', () => { + const mockTransformed = [ + { isActive: false, nodeId: 4 }, + { isActive: true, nodeId: 2 }, + { isActive: true, nodeId: 8 }, + ] as ApiProductModel.CategoryNodes[]; + const normalized = categoryIdNormalizer(mockTransformed); + + expect(normalized).toEqual({ + categoryIds: [ + String(mockTransformed[1].nodeId), + String(mockTransformed[2].nodeId), + ], + }); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/category-id.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/category-id.normalizer.ts new file mode 100644 index 000000000..293e7f3b8 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/category-id.normalizer.ts @@ -0,0 +1,30 @@ +import { Transformer } from '@spryker-oryx/core'; +import { ApiProductModel } from '../../../../../models'; +import { DeserializeCategoryIds } from './model'; + +export const CategoryIdNormalizer = 'oryx.CategoryIdNormalizer*'; + +export function categoryIdNormalizer( + data: ApiProductModel.CategoryNodes[] | undefined +): DeserializeCategoryIds | undefined { + if (!data?.length) { + return; + } + + const categoryIds = data.reduce( + (acc, curr) => (curr.isActive ? [...acc, String(curr.nodeId)] : acc), + [] + ); + + if (!categoryIds.length) { + return; + } + + return { categoryIds }; +} + +declare global { + interface InjectionTokensContractMap { + [CategoryIdNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/index.ts new file mode 100644 index 000000000..ad21a6cd3 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/index.ts @@ -0,0 +1,2 @@ +export * from './category-id.normalizer'; +export * from './model'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/model.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/model.ts new file mode 100644 index 000000000..3907fcc9d --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/category-id/model.ts @@ -0,0 +1 @@ +export type DeserializeCategoryIds = Record<'categoryIds', string[]>; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/concrete-products.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/concrete-products.normalizer.spec.ts new file mode 100644 index 000000000..01999f749 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/concrete-products.normalizer.spec.ts @@ -0,0 +1,64 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { of, take } from 'rxjs'; +import { CategoryIdNormalizer } from '../category-id'; +import { ProductNormalizer } from '../product'; +import { concreteProductsNormalizer } from './concrete-products.normalizer'; +import { DeserializedAbstract } from './model'; + +const mockAbstracts = [ + { + concreteProducts: [ + { + concrete: 'A', + }, + ], + categoryNodes: [{ category: 'a' }], + }, + { + concreteProducts: [ + { + concrete: 'B', + }, + { + concrete: 'C', + }, + ], + categoryNodes: [{ category: 'b' }], + }, +] as unknown as DeserializedAbstract[]; + +describe('Concrete Products Normalizer', () => { + it('should pass every concrete product to the ProductNormalizers via transformer', () => { + const mockTransformed = { mockTransformed: 'mockTransformed' }; + const mockTransformer = { + transform: vi.fn().mockReturnValue(of(mockTransformed)), + do: vi.fn().mockReturnValue(() => mockTransformed), + }; + concreteProductsNormalizer(mockAbstracts, mockTransformer) + .pipe(take(1)) + .subscribe((normalized) => { + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockAbstracts[0].concreteProducts![0], + ProductNormalizer + ); + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockAbstracts[0].categoryNodes, + CategoryIdNormalizer + ); + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockAbstracts[1].concreteProducts![0], + ProductNormalizer + ); + expect(mockTransformer.transform).not.toHaveBeenCalledWith( + mockAbstracts[1].concreteProducts![1], + ProductNormalizer + ); + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockAbstracts[1].categoryNodes, + CategoryIdNormalizer + ); + + expect(normalized).toEqual([mockTransformed, mockTransformed]); + }); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/concrete-products.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/concrete-products.normalizer.ts new file mode 100644 index 000000000..553993fae --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/concrete-products.normalizer.ts @@ -0,0 +1,44 @@ +import { Transformer, TransformerService } from '@spryker-oryx/core'; +import { camelize } from '@spryker-oryx/core/utilities'; +import { Observable, combineLatest, map } from 'rxjs'; +import { ApiProductModel, Product } from '../../../../../models'; +import { CategoryIdNormalizer } from '../category-id'; +import { ProductNormalizer } from '../product'; +import { DeserializedAbstract } from './model'; + +export const ConcreteProductsNormalizer = 'oryx.ConcreteProductsNormalizer*'; + +export function concreteProductsNormalizer( + data: DeserializedAbstract[], + transformer: TransformerService +): Observable { + const concreteProductsKey = camelize( + ApiProductModel.Includes.ConcreteProducts + ); + const categoryKey = camelize(ApiProductModel.Includes.CategoryNodes); + + return combineLatest( + data + .filter((abstract) => abstract[concreteProductsKey]?.length) + .map((abstract) => + combineLatest([ + transformer.transform( + abstract[concreteProductsKey]?.[0], + ProductNormalizer + ), + transformer.transform(abstract[categoryKey], CategoryIdNormalizer), + ]).pipe( + map(([product, nodeId]) => ({ + ...product, + ...nodeId, + })) + ) + ) + ); +} + +declare global { + interface InjectionTokensContractMap { + [ConcreteProductsNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/index.ts new file mode 100644 index 000000000..b0d1923c8 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/index.ts @@ -0,0 +1,2 @@ +export * from './concrete-products.normalizer'; +export * from './model'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/model.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/model.ts new file mode 100644 index 000000000..2189d4208 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/concrete-products/model.ts @@ -0,0 +1,10 @@ +import { CamelCase } from '@spryker-oryx/core/utilities'; +import { ApiProductModel } from '../../../../../models'; +import { DeserializedProductIncludes } from '../model'; + +export type DeserializedAbstract = ApiProductModel.Abstract & + Pick< + DeserializedProductIncludes, + | CamelCase + | CamelCase + >; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/facet-category.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/facet-category.normalizer.spec.ts new file mode 100644 index 000000000..b4c6b5d1a --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/facet-category.normalizer.spec.ts @@ -0,0 +1,67 @@ +import { facetCategoryNormalizer } from './facet-category.normalizer'; + +const categoryFacet = { + activeValue: '10', + config: { parameterName: 'category', isMultiValued: false }, + docCount: null, + localizedName: 'Categories', + name: 'category', + values: [ + { + value: 10, + docCount: 30, + }, + { value: 9, docCount: 30 }, + ], +}; + +const mockCategoryTreeFilter = [ + { + nodeId: 9, + name: 'Smart Wearables', + docCount: 30, + children: [ + { + nodeId: 10, + name: 'Smartwatches', + docCount: 30, + children: [], + }, + ], + }, +]; + +describe('Product Facet Normalizers', () => { + it('should return normalized product facet-navigation', () => { + expect( + facetCategoryNormalizer({ + categoryFacet, + categoryTreeFilter: mockCategoryTreeFilter, + }) + ).toEqual({ + type: 'single', + multiValued: false, + name: 'Categories', + parameter: 'category', + selectedValues: ['10'], + valuesTreeLength: 1, + values: [ + { + value: 9, + name: 'Smart Wearables', + count: 30, + selected: false, + children: [ + { + value: 10, + name: 'Smartwatches', + count: 30, + children: [], + selected: true, + }, + ], + }, + ], + }); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/facet-category.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/facet-category.normalizer.ts new file mode 100644 index 000000000..8fd8f1524 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/facet-category.normalizer.ts @@ -0,0 +1,50 @@ +import { Transformer } from '@spryker-oryx/core'; +import { ApiProductListModel, Facet, FacetValue } from '../../../../../models'; +import { parseFacetValue } from '../facet'; + +export interface FacetCategory { + categoryFacet: ApiProductListModel.ValueFacet; + categoryTreeFilter: ApiProductListModel.TreeFacet[]; +} + +export const FacetCategoryNormalizer = 'oryx.FacetCategoryNormalizer*'; + +export function facetCategoryNormalizer(fasets: FacetCategory): Facet { + const { categoryFacet, categoryTreeFilter } = fasets; + const parsedCategoryFacet = parseFacetValue(categoryFacet); + + const parse = ( + categoryTree: ApiProductListModel.TreeFacet[], + valuesList: FacetValue[] + ): any => + categoryTree + .filter(({ docCount }) => !!docCount) + .map((treeItem) => ({ + ...valuesList.find((valueList) => valueList.value === treeItem.nodeId)!, + name: treeItem.name, + children: treeItem.children?.length + ? parse(treeItem.children, valuesList) + : [], + })); + + const categoryTreeValues = parse( + categoryTreeFilter, + (parsedCategoryFacet?.values as FacetValue[]) ?? [] + ); + + const categoryTree = { + values: categoryTreeValues, + valuesTreeLength: categoryTreeValues.length, + }; + + return { + ...parsedCategoryFacet!, + ...categoryTree, + }; +} + +declare global { + interface InjectionTokensContractMap { + [FacetCategoryNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/index.ts new file mode 100644 index 000000000..ac183a3df --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-category/index.ts @@ -0,0 +1 @@ +export * from './facet-category.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/facet-range.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/facet-range.normalizer.spec.ts new file mode 100644 index 000000000..340b2e468 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/facet-range.normalizer.spec.ts @@ -0,0 +1,51 @@ +import { facetsRangeNormalizer } from './facet-range.normalizer'; + +const mockRangeFacets = [ + { + activeMax: 36660, + activeMin: 175, + config: { parameterName: 'price', isMultiValued: false }, + docCount: 2, + localizedName: 'Price range', + max: 36660, + min: 175, + name: 'price-DEFAULT-EUR-GROSS_MODE', + }, +]; + +const invalidRangeFacets = [ + { + activeMax: 2, + activeMin: 1, + config: { parameterName: 'mock', isMultiValued: false }, + docCount: 2, + localizedName: 'mock', + max: 2, + min: 1, + name: 'mock', + }, +]; + +describe('Product Facet Normalizers', () => { + it('should return normalized product facet-navigation', () => { + expect(facetsRangeNormalizer(mockRangeFacets)).toEqual([ + { + type: 'range', + parameter: 'price', + name: 'Price range', + values: { + max: 36660, + min: 175, + selected: { + max: 36660, + min: 175, + }, + }, + }, + ]); + }); + + it('should ignore invalid range facet', () => { + expect(facetsRangeNormalizer(invalidRangeFacets)).toEqual([]); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/facet-range.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/facet-range.normalizer.ts new file mode 100644 index 000000000..c0922482d --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/facet-range.normalizer.ts @@ -0,0 +1,45 @@ +import { Transformer } from '@spryker-oryx/core'; +import { + ApiProductListModel, + FacetType, + RangeFacet, +} from '../../../../../models'; + +export const FacetRangeNormalizer = 'oryx.FacetRangeNormalizer*'; + +export function facetsRangeNormalizer( + rangeFacets: ApiProductListModel.RangeFacet[] +): RangeFacet[] { + return rangeFacets.reduce((normalizedFacetList: RangeFacet[], facet) => { + //ignore the facet if difference between min and max is 1 + if (facet.max - facet.min <= 1) { + return normalizedFacetList; + } + + const { config, localizedName } = facet; + + const values = { + min: facet.min, + max: facet.max, + selected: { + max: facet.activeMax, + min: facet.activeMin, + }, + }; + + const normalizedFacet: RangeFacet = { + type: FacetType.Range, + name: localizedName, + parameter: config.parameterName, + values, + }; + + return [...normalizedFacetList, normalizedFacet]; + }, []); +} + +declare global { + interface InjectionTokensContractMap { + [FacetRangeNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/index.ts new file mode 100644 index 000000000..743227a92 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-range/index.ts @@ -0,0 +1 @@ +export * from './facet-range.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-rating/facet-rating.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-rating/facet-rating.normalizer.ts new file mode 100644 index 000000000..aedd557bc --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-rating/facet-rating.normalizer.ts @@ -0,0 +1,44 @@ +import { Transformer } from '@spryker-oryx/core'; +import { resolve } from '@spryker-oryx/di'; +import { RouterService } from '@spryker-oryx/router'; +import { map, Observable, take } from 'rxjs'; +import { ApiProductListModel, Facet, FacetType } from '../../../../../models'; +export const FacetRatingNormalizer = 'oryx.FacetRatingNormalizer*'; + +//TODO: temporary solution. Should be fixed after https://spryker.atlassian.net/browse/CC-31032. +// For now, it is only way to get current rating value because the backend wrongly returns the rating value. +export function facetRatingNormalizer( + facet: ApiProductListModel.RangeFacet +): Observable { + return resolve(RouterService) + .currentQuery() + .pipe( + take(1), + map((params) => { + const { config, localizedName } = facet; + const activeMin = params?.['rating[min]']; + + const values = { + min: 0, + max: facet.max, + selected: { + max: facet.activeMax, + min: activeMin ? Number(activeMin) : 0, + }, + }; + + return { + type: FacetType.Range, + name: localizedName, + parameter: config.parameterName, + values, + }; + }) + ); +} + +declare global { + interface InjectionTokensContractMap { + [FacetRatingNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-rating/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-rating/index.ts new file mode 100644 index 000000000..b8c973c73 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet-rating/index.ts @@ -0,0 +1 @@ +export * from './facet-rating.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/facet.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/facet.normalizer.spec.ts new file mode 100644 index 000000000..b54809647 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/facet.normalizer.spec.ts @@ -0,0 +1,52 @@ +import { ApiProductListModel } from '../../../../../models'; +import { facetsNormalizer } from './facet.normalizer'; + +const mockFacets: ApiProductListModel.ValueFacet[] = [ + { + name: 'color', + localizedName: 'Color', + docCount: null, + values: [ + { + value: 'Black', + docCount: 78, + }, + { + value: 'White', + docCount: 38, + }, + ], + activeValue: null, + config: { + parameterName: 'color', + isMultiValued: true, + }, + }, +]; + +describe('Product Facet Normalizers', () => { + it('should return normalized product facet-navigation', () => { + expect(facetsNormalizer({ facetList: mockFacets })).toEqual([ + { + type: 'multi', + name: 'Color', + parameter: 'color', + values: [ + { + count: 78, + selected: false, + value: 'Black', + }, + { + count: 38, + selected: false, + value: 'White', + }, + ], + selectedValues: undefined, + valuesTreeLength: 2, + multiValued: true, + }, + ]); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/facet.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/facet.normalizer.ts new file mode 100644 index 000000000..b6974f8ea --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/facet.normalizer.ts @@ -0,0 +1,88 @@ +import { Transformer } from '@spryker-oryx/core'; +import { + ApiProductListModel, + Facet, + FacetType, + FacetValue, +} from '../../../../../models'; + +export interface FacetNormalizerValue { + facetList: ApiProductListModel.ValueFacet[]; + numFound?: number; +} + +export const FacetNormalizer = 'oryx.FacetNormalizer*'; + +export function facetsNormalizer( + facetNormalizerValue: FacetNormalizerValue +): Facet[] { + const { facetList, numFound } = facetNormalizerValue; + + return facetList.reduce((normalizedFacetList: Facet[], facet) => { + const parsedValue = parseFacetValue(facet, numFound); + + return parsedValue + ? [...normalizedFacetList, parsedValue] + : normalizedFacetList; + }, []); +} + +export const parseFacetValue = ( + facet?: ApiProductListModel.ValueFacet, + numFound?: number +): Facet | null => { + if (!facet) { + return null; + } + + const { config, localizedName } = facet; + + const selectedValue = + 'activeValue' in facet + ? Array.isArray(facet.activeValue) + ? facet.activeValue + : (facet.activeValue as string)?.split(',') + : []; + + const facetValues = facet.values.reduce( + ( + facetList: FacetValue[], + value: { value: number | string; docCount: number } + ) => { + const selected = + (selectedValue ?? []).includes(String(value.value)) ?? false; + + if (!value.docCount || (!selected && value.docCount === numFound)) { + return facetList; + } + + const parsedFacedValue = { + value: value.value, + selected, + count: value.docCount, + }; + + return [...facetList, parsedFacedValue]; + }, + [] + ); + + return facetValues.length + ? { + type: config.isMultiValued ? FacetType.Multi : FacetType.Single, + name: localizedName, + parameter: config.parameterName, + values: facetValues, + selectedValues: selectedValue, + valuesTreeLength: facetValues.length, + /** @deprecated since 1.2 use facet.type === FacetType.Multi check instead*/ + multiValued: config.isMultiValued, + } + : null; +}; + +declare global { + interface InjectionTokensContractMap { + [FacetNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/index.ts new file mode 100644 index 000000000..c4711b551 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/facet/index.ts @@ -0,0 +1 @@ +export * from './facet.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/index.ts new file mode 100644 index 000000000..3c54b2169 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/index.ts @@ -0,0 +1,13 @@ +export * from './availability'; +export * from './category-id'; +export * from './concrete-products'; +export * from './facet'; +export * from './facet-category'; +export * from './facet-range'; +export * from './facet-rating'; +export * from './labels'; +export * from './media'; +export * from './model'; +export * from './price'; +export * from './product'; +export * from './product-list'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/index.ts new file mode 100644 index 000000000..31b172bad --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/index.ts @@ -0,0 +1 @@ +export * from './labels.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/labels.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/labels.normalizer.spec.ts new file mode 100644 index 000000000..e5be39700 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/labels.normalizer.spec.ts @@ -0,0 +1,55 @@ +import { ApiProductModel, ProductLabel } from '../../../../../models'; +import { productLabelNormalizer } from './labels.normalizer'; + +const mockLabel1: ApiProductModel.ProductLabel = { + id: 'foo', + name: 'Sale', + position: 1, + frontEndReference: 'highlight', +}; + +const mockLabel2: ApiProductModel.ProductLabel = { + id: 'foo', + name: 'New', + position: 2, + frontEndReference: 'unknown', +}; + +const mockLabel3: ApiProductModel.ProductLabel = { + id: 'foo', + name: 'Empty', + position: 3, +}; + +describe('productLabelsNormalizer', () => { + let normalized: ProductLabel[]; + + describe('appearance', () => { + describe('when the label contains a highlight frontEndReference', () => { + beforeEach(() => { + normalized = productLabelNormalizer([mockLabel1]); + }); + it('should convert it to highlight', () => { + expect(normalized[0].appearance).toEqual('error'); + }); + }); + + describe('when the label contains an unknown frontEndReference', () => { + beforeEach(() => { + normalized = productLabelNormalizer([mockLabel2]); + }); + it('should convert it to highlight', () => { + expect(normalized[0].appearance).toEqual('info'); + }); + }); + + describe('when the label does not contain a frontEndReference', () => { + beforeEach(() => { + normalized = productLabelNormalizer([mockLabel3]); + }); + it('should convert it to highlight', () => { + expect(normalized[0].appearance).toEqual('info'); + }); + }); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/labels.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/labels.normalizer.ts new file mode 100644 index 000000000..b03e0ecde --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/labels/labels.normalizer.ts @@ -0,0 +1,35 @@ +import { Transformer } from '@spryker-oryx/core'; +import { + ApiProductModel, + ProductLabel, + ProductLabelAppearance, +} from '../../../../../models'; + +export const ProductLabelsNormalizer = 'oryx.ProductLabelsNormalizer*'; + +export function productLabelNormalizer( + data: ApiProductModel.ProductLabel[] +): ProductLabel[] { + const normalizeAppearance = (data: ApiProductModel.ProductLabel): string => { + switch (data.frontEndReference) { + case 'highlight': + return ProductLabelAppearance.Highlight; + default: + return ProductLabelAppearance.Info; + } + }; + + return (data ?? []).map( + (label) => + ({ + name: label.name, + appearance: normalizeAppearance(label), + } as ProductLabel) + ); +} + +declare global { + interface InjectionTokensContractMap { + [ProductLabelsNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/index.ts new file mode 100644 index 000000000..ad9244468 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/index.ts @@ -0,0 +1,2 @@ +export * from './media-set.normalizer'; +export * from './media.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media-set.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media-set.normalizer.spec.ts new file mode 100644 index 000000000..4ccb05e69 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media-set.normalizer.spec.ts @@ -0,0 +1,73 @@ +import { Observable, of } from 'rxjs'; +import { ProductMediaSet } from '../../../../../models'; +import { mediaSetNormalizer } from './media-set.normalizer'; +import { ProductMediaNormalizer } from './media.normalizer'; + +const set1 = { + name: 'name', + images: [ + { + externalUrlSmall: 'set1/small-1.jpg', + externalUrlLarge: 'set1/large-1.jpg', + }, + ], +}; + +const set2 = { + name: 'name', + images: [ + { + externalUrlSmall: 'set2/small-1.jpg', + externalUrlLarge: 'set2/large-1.jpg', + }, + { + externalUrlSmall: 'set2/small-2.jpg', + externalUrlLarge: 'set2/large-2.jpg', + }, + ], +}; + +const imageSet = [set1, set2]; + +const mockTransformedValue = 'mockTransformedValue'; + +const mockTransformer = { + transform: vi.fn().mockReturnValue(of(mockTransformedValue)), + do: vi.fn(), +}; + +describe('productMediaNormalizer', () => { + let normalized: Observable; + + beforeEach(() => { + normalized = mediaSetNormalizer(imageSet, mockTransformer); + }); + + it('should call `ProductMediaNormalizer` transformer for image', () => { + imageSet.forEach((set) => { + set.images.forEach((image) => { + expect(mockTransformer.transform).toHaveBeenCalledWith( + image, + ProductMediaNormalizer + ); + }); + }); + }); + + it('should return observable', () => { + expect(normalized).toBeInstanceOf(Observable); + }); + + it('should return `ProductMediaSet[]` where media should be result of transformation', () => { + const expected = [ + { media: ['mockTransformedValue'], name: 'name' }, + { + media: ['mockTransformedValue', 'mockTransformedValue'], + name: 'name', + }, + ]; + const callback = vi.fn(); + normalized.subscribe(callback); + expect(callback).toHaveBeenCalledWith(expected); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media-set.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media-set.normalizer.ts new file mode 100644 index 000000000..3de1459f8 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media-set.normalizer.ts @@ -0,0 +1,27 @@ +import { Transformer, TransformerService } from '@spryker-oryx/core'; +import { Observable, combineLatest, map } from 'rxjs'; +import { ApiProductModel, ProductMediaSet } from '../../../../../models'; +import { ProductMediaNormalizer } from './media.normalizer'; + +export const ProductMediaSetNormalizer = 'oryx.ProductMediaSetNormalizer*'; + +export function mediaSetNormalizer( + data: ApiProductModel.ImageSet[] = [], + transformer: TransformerService +): Observable { + return combineLatest( + data.map((set) => + combineLatest( + set?.images.map((image) => + transformer.transform(image, ProductMediaNormalizer) + ) + ).pipe(map((images) => ({ media: images, name: set.name }))) + ) + ); +} + +declare global { + interface InjectionTokensContractMap { + [ProductMediaSetNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media.normalizer.spec.ts new file mode 100644 index 000000000..b5c66dc06 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media.normalizer.spec.ts @@ -0,0 +1,46 @@ +import { ProductMedia } from '@spryker-oryx/product'; +import { mediaNormalizer } from './media.normalizer'; + +const image = { + externalUrlSmall: 'set1/small-1.jpg', + externalUrlLarge: 'set1/large-1.jpg', +}; + +const duplicate = 'set3/image-1.jpg'; + +const image2 = { + externalUrlSmall: duplicate, + externalUrlLarge: duplicate, +}; + +describe('productMediaNormalizer', () => { + let normalized: ProductMedia; + + describe('when the source contains a small and large image', () => { + beforeEach(() => { + normalized = mediaNormalizer(image); + }); + + it('should fill the sm and lg fields', () => { + expect(normalized.sm).toBe('set1/small-1.jpg'); + expect(normalized.lg).toBe('set1/large-1.jpg'); + }); + + it('should not fill xs, md and xl', () => { + expect(normalized.xs).toBeUndefined(); + expect(normalized.md).toBeUndefined(); + expect(normalized.xl).toBeUndefined(); + }); + }); + + describe('when the large image url is identical with the small', () => { + beforeEach(() => { + normalized = mediaNormalizer(image2); + }); + + it('should not expose the large url', () => { + expect(normalized.sm).toBe(duplicate); + expect(normalized.lg).toBeUndefined(); + }); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media.normalizer.ts new file mode 100644 index 000000000..1a6816c8a --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/media/media.normalizer.ts @@ -0,0 +1,30 @@ +import { Transformer } from '@spryker-oryx/core'; +import { Size } from '@spryker-oryx/utilities'; +import { ApiProductModel, ProductMedia } from '../../../../../models'; + +export const ProductMediaNormalizer = 'oryx.ProductMediaNormalizer*'; +export const DefaultProductMediaNormalizer = `${ProductMediaNormalizer}Default`; + +export function mediaNormalizer( + data?: ApiProductModel.Image | undefined +): ProductMedia { + const sources: ProductMedia = {}; + if (data) { + const { externalUrlSmall, externalUrlLarge } = data; + + if (externalUrlSmall) { + sources[Size.Sm] = externalUrlSmall; + } + + if (externalUrlLarge !== externalUrlSmall) { + sources[Size.Lg] = externalUrlLarge; + } + } + return sources; +} + +declare global { + interface InjectionTokensContractMap { + [ProductMediaNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/model.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/model.ts new file mode 100644 index 000000000..efb73bff2 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/model.ts @@ -0,0 +1,44 @@ +import { CamelCase } from '@spryker-oryx/core/utilities'; +import { ApiProductModel } from '../../../../models'; +import { ApiProductListModel } from '../../../../models/product-list.api.model'; +import { DeserializedAvailability } from './availability'; + +export type DeserializedProductIncludes = { + [P in ApiProductModel.Includes as `${CamelCase

}`]?: P extends ApiProductModel.Includes.ConcreteProductImageSets + ? ApiProductModel.ImageSets[] + : P extends ApiProductModel.Includes.ConcreteProductPrices + ? ApiProductModel.Prices[] + : P extends ApiProductModel.Includes.CategoryNodes + ? ApiProductModel.CategoryNodes[] + : P extends ApiProductModel.Includes.AbstractProducts + ? (ApiProductModel.Abstract & + Pick< + DeserializedProductIncludes, + CamelCase + >)[] + : P extends ApiProductModel.Includes.ConcreteProductAvailabilities + ? DeserializedAvailability[] + : P extends ApiProductModel.Includes.ConcreteProducts + ? (ApiProductModel.Concrete & + Pick< + DeserializedProductIncludes, + CamelCase + >)[] + : never; +}; + +export type DeserializedProductListIncludes = { + [P in ApiProductListModel.Includes as `${CamelCase

}`]?: P extends ApiProductListModel.Includes.AbstractProducts + ? ApiProductModel.Abstract[] + : P extends ApiProductListModel.Includes.RangeFacets + ? ApiProductListModel.RangeFacet[] + : P extends ApiProductListModel.Includes.ValueFacets + ? ApiProductListModel.ValueFacet[] + : P extends ApiProductListModel.Includes.CategoryTreeFilter + ? ApiProductListModel.TreeFacet[] + : P extends ApiProductListModel.Includes.Pagination + ? ApiProductListModel.Pagination + : P extends ApiProductListModel.Includes.Sort + ? ApiProductListModel.Sort + : never; +}; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/index.ts new file mode 100644 index 000000000..f64bd9a2c --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/index.ts @@ -0,0 +1 @@ +export * from './pagination.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/pagination.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/pagination.normalizer.spec.ts new file mode 100644 index 000000000..56f8e30e2 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/pagination.normalizer.spec.ts @@ -0,0 +1,27 @@ +import { ApiProductListModel } from '@spryker-oryx/product'; +import { paginationNormalizer } from './pagination.normalizer'; +import Pagination = ApiProductListModel.Pagination; + +const pagination: Pagination = { + numFound: 100, + currentPage: 1, + currentItemsPerPage: 20, + maxPage: 5, + config: { + parameterName: 'name', + itemsPerPageParameterName: '21', + defaultItemsPerPage: 12, + validItemsPerPageOptions: [12, 24, 36], + }, +}; + +describe('Product Pagination Normalizers', () => { + it('should return normalized product pagination', () => { + expect(paginationNormalizer(pagination)).toEqual({ + numFound: 100, + currentPage: 1, + itemsPerPage: 20, + maxPage: 5, + }); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/pagination.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/pagination.normalizer.ts new file mode 100644 index 000000000..9ee479d3f --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/pagination/pagination.normalizer.ts @@ -0,0 +1,23 @@ +import { Transformer } from '@spryker-oryx/core'; +import { ApiProductListModel, Pagination } from '../../../../../models'; + +export const PaginationNormalizer = 'oryx.PaginationNormalizer*'; + +export function paginationNormalizer( + data: ApiProductListModel.Pagination +): Pagination { + const { maxPage, currentItemsPerPage, currentPage, numFound } = data; + + return { + itemsPerPage: currentItemsPerPage, + currentPage, + maxPage, + numFound, + }; +} + +declare global { + interface InjectionTokensContractMap { + [PaginationNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/index.ts new file mode 100644 index 000000000..034f61b20 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/index.ts @@ -0,0 +1 @@ +export * from './price.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/price.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/price.normalizer.spec.ts new file mode 100644 index 000000000..bf1c15ad3 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/price.normalizer.spec.ts @@ -0,0 +1,50 @@ +import { ApiProductModel } from '../../../../../models'; +import { priceNormalizer } from './price.normalizer'; + +const generatePrice = ( + value: number, + currencyCode = 'EUR', + isNet = true, + isDefault = true +): ApiProductModel.Prices => { + return { + prices: [ + { + grossAmount: isNet ? undefined : value, + netAmount: isNet ? value : undefined, + currency: { code: currencyCode }, + priceTypeName: isDefault ? 'DEFAULT' : 'ORIGINAL', + }, + ], + }; +}; + +describe('Price Normalizer', () => { + it('should convert the price when default net price is given', () => { + const normalized = priceNormalizer(generatePrice(300, 'EUR', false)); + expect(normalized.defaultPrice?.value).toEqual(300); + expect(normalized.defaultPrice?.isNet).toEqual(false); + expect(normalized.defaultPrice?.currency).toEqual('EUR'); + }); + + it('should convert the price when default gross price is given', () => { + const normalized = priceNormalizer(generatePrice(300)); + expect(normalized.defaultPrice?.value).toEqual(300); + expect(normalized.defaultPrice?.isNet).toEqual(true); + expect(normalized.defaultPrice?.currency).toEqual('EUR'); + }); + + it('should convert the price when original net price is given', () => { + const normalized = priceNormalizer(generatePrice(300, 'EUR', true, false)); + expect(normalized.originalPrice?.value).toEqual(300); + expect(normalized.originalPrice?.isNet).toEqual(true); + expect(normalized.originalPrice?.currency).toEqual('EUR'); + }); + + it('should convert the price when original gross price is given', () => { + const normalized = priceNormalizer(generatePrice(300, 'EUR', false, false)); + expect(normalized.originalPrice?.value).toEqual(300); + expect(normalized.originalPrice?.isNet).toEqual(false); + expect(normalized.originalPrice?.currency).toEqual('EUR'); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/price.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/price.normalizer.ts new file mode 100644 index 000000000..34c006eb6 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/price/price.normalizer.ts @@ -0,0 +1,41 @@ +import { Transformer } from '@spryker-oryx/core'; +import { + ApiProductModel, + ProductPrice, + ProductPrices, +} from '../../../../../models'; + +export const PriceNormalizer = 'oryx.PriceNormalizer*'; + +export function priceNormalizer(data: ApiProductModel.Prices): ProductPrices { + const normalize = ( + data?: ApiProductModel.Price + ): ProductPrice | undefined => { + const value = data?.grossAmount ?? data?.netAmount; + + if (!data || !value) { + return; + } + + return { + value, + currency: data.currency.code, + isNet: !!data.netAmount, + }; + }; + + return { + defaultPrice: normalize( + data?.prices?.find((price) => price.priceTypeName === 'DEFAULT') + ), + originalPrice: normalize( + data?.prices?.find((price) => price.priceTypeName === 'ORIGINAL') + ), + }; +} + +declare global { + interface InjectionTokensContractMap { + [PriceNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/index.ts new file mode 100644 index 000000000..cd2f04f07 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/index.ts @@ -0,0 +1,2 @@ +export * from './model'; +export * from './product-list.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/model.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/model.ts new file mode 100644 index 000000000..c801f4bcd --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/model.ts @@ -0,0 +1,13 @@ +import { CamelCase } from '@spryker-oryx/core/utilities'; +import { ApiProductListModel } from '../../../../../models/product-list.api.model'; +import { DeserializedProductListIncludes } from '../model'; + +export type DeserializedProductList = Pick< + DeserializedProductListIncludes, + | CamelCase + | CamelCase + | CamelCase + | CamelCase + | CamelCase + | CamelCase +>; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/product-list.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/product-list.normalizer.spec.ts new file mode 100644 index 000000000..3b150b353 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/product-list.normalizer.spec.ts @@ -0,0 +1,30 @@ +import { of, take } from 'rxjs'; +import { ConcreteProductsNormalizer } from '../concrete-products'; +import { concreteProductNormalizer } from './product-list.normalizer'; + +const mockDeserializedProductList = { + abstractProducts: [], +}; + +const mockTransformer = { + transform: vi.fn(), + do: vi.fn(), +}; + +describe('Product Catalog Normalizers', () => { + it('should call transformers and return products', () => { + const mockProductsResult = 'mockProductsResult'; + mockTransformer.transform.mockReturnValue(of(mockProductsResult)); + concreteProductNormalizer([mockDeserializedProductList], mockTransformer) + .pipe(take(1)) + .subscribe((result) => { + expect(result).toEqual({ + products: mockProductsResult, + }); + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockDeserializedProductList.abstractProducts, + ConcreteProductsNormalizer + ); + }); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/product-list.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/product-list.normalizer.ts new file mode 100644 index 000000000..b7277c9b3 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product-list/product-list.normalizer.ts @@ -0,0 +1,121 @@ +import { Transformer, TransformerService } from '@spryker-oryx/core'; +import { camelize } from '@spryker-oryx/core/utilities'; +import { Provider } from '@spryker-oryx/di'; +import { Observable, combineLatest, map } from 'rxjs'; +import { ApiProductListModel, Facet, ProductList } from '../../../../../models'; +import { ConcreteProductsNormalizer } from '../concrete-products'; +import { FacetNormalizer } from '../facet'; +import { FacetCategoryNormalizer } from '../facet-category'; +import { FacetRangeNormalizer } from '../facet-range'; +import { FacetRatingNormalizer } from '../facet-rating'; +import { DeserializedProductListIncludes } from '../model'; +import { PaginationNormalizer } from '../pagination'; +import { SortNormalizer } from '../sort'; +import { DeserializedProductList } from './model'; + +export const ProductListNormalizer = 'oryx.ProductListNormalizer*'; + +export function paginationNormalizer( + data: [DeserializedProductList], + transformer: TransformerService +): Observable> { + const abstractKey = camelize(ApiProductListModel.Includes.Pagination); + const { [abstractKey]: pagination } = data[0]; + + return transformer + .transform(pagination, PaginationNormalizer) + .pipe(map((pagination) => ({ pagination }))); +} + +export function concreteProductNormalizer( + data: DeserializedProductListIncludes[], + transformer: TransformerService +): Observable> { + const abstractKey = camelize(ApiProductListModel.Includes.AbstractProducts); + const { [abstractKey]: products } = data[0]; + + return transformer + .transform(products, ConcreteProductsNormalizer) + .pipe(map((products) => ({ products }))); +} + +export function sortingNormalizer( + data: [DeserializedProductList], + transformer: TransformerService +): Observable> { + const abstractKey = camelize(ApiProductListModel.Includes.Sort); + const { [abstractKey]: sort } = data[0]; + + return transformer + .transform(sort, SortNormalizer) + .pipe(map((sort) => ({ sort }))); +} + +export function productFacetNormalizer( + data: [DeserializedProductList], + transformer: TransformerService +): Observable> { + const { rangeFacets, categoryTreeFilter, valueFacets, pagination } = data[0]; + + const categoryFacet = valueFacets!.splice( + valueFacets!.findIndex((v) => v.name === 'category'), + 1 + ); + + //TODO: drop and use ordinary range normalizer after https://spryker.atlassian.net/browse/CC-31032 + const ratingFacet = data[0].rangeFacets!.splice( + data[0].rangeFacets!.findIndex((v) => v.name === 'rating'), + 1 + ); + + return combineLatest([ + transformer.transform( + { + categoryFacet: categoryFacet[0], + categoryTreeFilter, + }, + FacetCategoryNormalizer + ), + //TODO: drop and use ordinary range normalizer after https://spryker.atlassian.net/browse/CC-31032 + transformer.transform(ratingFacet[0], FacetRatingNormalizer), + transformer.transform(rangeFacets, FacetRangeNormalizer), + transformer.transform( + { + facetList: valueFacets, + numFound: pagination?.numFound, + }, + FacetNormalizer + ), + ]).pipe( + map((facets) => { + return { + facets: facets.filter((facet) => facet).flat() as Facet[], + }; + }) + ); +} + +export const productListNormalizer: Provider[] = [ + { + provide: ProductListNormalizer, + useValue: paginationNormalizer, + }, + { + provide: ProductListNormalizer, + useValue: concreteProductNormalizer, + }, + { + provide: ProductListNormalizer, + useValue: productFacetNormalizer, + }, + { + provide: ProductListNormalizer, + useValue: sortingNormalizer, + }, +]; + +declare global { + interface InjectionTokensContractMap { + [ProductListNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/index.ts new file mode 100644 index 000000000..6160e1b75 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/index.ts @@ -0,0 +1,2 @@ +export * from './model'; +export * from './product.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/model.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/model.ts new file mode 100644 index 000000000..ba5b351d4 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/model.ts @@ -0,0 +1,13 @@ +import { CamelCase } from '@spryker-oryx/core/utilities'; +import { ApiProductModel } from '../../../../../models'; +import { DeserializedProductIncludes } from '../model'; + +export type DeserializedProduct = ApiProductModel.Concrete & + Pick< + DeserializedProductIncludes, + | CamelCase + | CamelCase + | CamelCase + | CamelCase + | CamelCase + >; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/product.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/product.normalizer.spec.ts new file mode 100644 index 000000000..c727dcf04 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/product.normalizer.spec.ts @@ -0,0 +1,194 @@ +import { camelize } from '@spryker-oryx/core/utilities'; +import { of, take } from 'rxjs'; +import { ApiProductModel, Product } from '../../../../../models'; +import { CategoryNormalizer } from '../../../../category'; +import { CategoryIdNormalizer } from '../category-id'; +import { ProductMediaSetNormalizer } from '../media'; +import { PriceNormalizer } from '../price'; +import { DeserializedProduct } from './model'; +import { + productAttributeNormalizer, + productCategoryNormalizer, + productMediaSetNormalizer, + productNodeNormalizer, + productPriceNormalizer, +} from './product.normalizer'; + +let mockDeserializedProduct: DeserializedProduct; +const mockTokensData = { + [PriceNormalizer]: { + defaultPrice: { + value: 300, + isNet: true, + currency: 'EUR', + }, + }, + [ProductMediaSetNormalizer]: [ + { + externalUrlLarge: 'externalUrlLarge', + externalUrlSmall: 'externalUrlSmall', + }, + ], + [CategoryIdNormalizer]: '20', +}; +const mockTransformer = { + transform: vi + .fn() + .mockImplementation((data, token: keyof typeof mockTokensData): unknown => + of(mockTokensData[token]) + ), + do: vi.fn(), +}; + +describe('Product Normalizers', () => { + beforeEach(() => { + mockDeserializedProduct = { + sku: 'sku', + name: 'name', + description: 'description', + reviewCount: 3, + averageRating: '5', + [camelize(ApiProductModel.Includes.ConcreteProductImageSets)]: [ + { + imageSets: [ + { + name: 'test', + images: [ + { + externalUrlLarge: 'externalUrlLarge', + externalUrlSmall: 'externalUrlSmall', + }, + ], + }, + ], + }, + ], + [camelize(ApiProductModel.Includes.ConcreteProductPrices)]: [ + { + price: 100, + prices: [ + { + grossAmount: 300, + netAmount: 200, + currency: { code: 'EUR' }, + priceTypeName: 'ORIGINAL' as const, + }, + ], + }, + ], + attributes: { + color: 'red', + brand: 'Brand1', + }, + attributeNames: { + color: 'Color', + brand: 'Brand', + }, + [camelize(ApiProductModel.Includes.AbstractProducts)]: [ + { + [camelize(ApiProductModel.Includes.CategoryNodes)]: [ + { + nodeId: 8, + isActive: false, + }, + { + nodeId: 10, + isActive: true, + }, + ], + }, + ], + } as unknown as DeserializedProduct; + }); + + describe('Product Attributes Normalizer', () => { + it('should transform DeserializedProduct into Product', () => { + const mockResult: Product = { + sku: mockDeserializedProduct.sku, + name: mockDeserializedProduct.name, + description: mockDeserializedProduct.description, + averageRating: Number(mockDeserializedProduct.averageRating), + reviewCount: mockDeserializedProduct.reviewCount, + attributes: mockDeserializedProduct.attributes, + attributeNames: mockDeserializedProduct.attributeNames, + }; + const normalized = productAttributeNormalizer(mockDeserializedProduct); + expect(normalized).toEqual(mockResult); + }); + + it('should return `averageRating` as 0 if its falsy', () => { + delete mockDeserializedProduct.averageRating; + const normalized = productAttributeNormalizer(mockDeserializedProduct); + expect(normalized).toEqual( + expect.objectContaining({ + averageRating: 0, + }) + ); + }); + }); + + describe('Product Price Normalizer', () => { + it('should call price transformer', () => { + productPriceNormalizer(mockDeserializedProduct, mockTransformer) + .pipe(take(1)) + .subscribe((normalized) => { + expect(normalized).toEqual({ + price: mockTokensData[PriceNormalizer], + }); + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockDeserializedProduct.concreteProductPrices?.[0], + PriceNormalizer + ); + }); + }); + }); + + describe('Product Images Normalizers', () => { + it('should call image transformer', () => + new Promise((done) => { + productMediaSetNormalizer(mockDeserializedProduct, mockTransformer) + .pipe(take(1)) + .subscribe((normalized) => { + expect(normalized).toEqual({ + mediaSet: mockTokensData[ProductMediaSetNormalizer], + }); + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockDeserializedProduct.concreteProductImageSets?.[0].imageSets, + ProductMediaSetNormalizer + ); + done(); + }); + })); + }); + + describe('Product Node Normalizer', () => { + it('should call node transformer', () => + new Promise((done) => { + productNodeNormalizer(mockDeserializedProduct, mockTransformer) + .pipe(take(1)) + .subscribe((normalized) => { + expect(normalized).toBe(mockTokensData[CategoryIdNormalizer]); + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockDeserializedProduct.abstractProducts?.[0].categoryNodes, + CategoryIdNormalizer + ); + done(); + }); + })); + }); + + describe('Product Category Normalizer', () => { + it('should call categories transformer', () => + new Promise((done) => { + productCategoryNormalizer(mockDeserializedProduct, mockTransformer) + .pipe(take(1)) + .subscribe(() => { + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockDeserializedProduct.abstractProducts?.[0].categoryNodes, + CategoryNormalizer + ); + done(); + }); + })); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/product.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/product.normalizer.ts new file mode 100644 index 000000000..952e36ca7 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/product/product.normalizer.ts @@ -0,0 +1,171 @@ +import { Transformer, TransformerService } from '@spryker-oryx/core'; +import { camelize } from '@spryker-oryx/core/utilities'; +import { Provider } from '@spryker-oryx/di'; +import { Observable, map, of } from 'rxjs'; +import { ApiProductModel, Product } from '../../../../../models'; +import { CategoryNormalizer } from '../../../../category'; +import { AvailabilityNormalizer } from '../availability'; +import { CategoryIdNormalizer } from '../category-id'; +import { ProductLabelsNormalizer } from '../labels/labels.normalizer'; +import { ProductMediaSetNormalizer } from '../media'; +import { PriceNormalizer } from '../price'; +import { DeserializedProduct } from './model'; + +export const ProductNormalizer = 'oryx.ProductNormalizer*'; + +export function productAttributeNormalizer( + data: DeserializedProduct +): Partial { + const { + sku, + name, + description, + averageRating, + reviewCount, + attributes, + attributeNames, + } = data; + + return { + sku, + name, + description, + averageRating: averageRating ? Number(averageRating) : 0, + reviewCount, + attributes, + attributeNames, + }; +} + +export function productPriceNormalizer( + data: DeserializedProduct, + transformer: TransformerService +): Observable> { + const priceKey = camelize(ApiProductModel.Includes.ConcreteProductPrices); + const { [priceKey]: price } = data; + + return transformer + .transform(price?.[0], PriceNormalizer) + .pipe(map((price) => ({ price }))); +} + +export function productLabelsNormalizer( + data: DeserializedProduct, + transformer: TransformerService +): Observable> { + const labelsKey = camelize(ApiProductModel.Includes.Labels); + const { [labelsKey]: labels } = data; + + return transformer + .transform(labels, ProductLabelsNormalizer) + .pipe(map((labels) => ({ labels }))); +} + +export function productMediaSetNormalizer( + data: DeserializedProduct, + transformer: TransformerService +): Observable> { + const imageKey = camelize(ApiProductModel.Includes.ConcreteProductImageSets); + const { [imageKey]: images } = data; + return transformer + .transform(images?.[0].imageSets, ProductMediaSetNormalizer) + .pipe(map((sets) => ({ mediaSet: sets }))); +} + +export function productAvailabilityNormalizer( + data: DeserializedProduct, + transformer: TransformerService +): Observable> { + const stockKey = camelize( + ApiProductModel.Includes.ConcreteProductAvailabilities + ); + const { [stockKey]: availability } = data; + return transformer + .transform(availability?.[0], AvailabilityNormalizer) + .pipe(map((availability) => ({ availability }))); +} + +export function productDiscontinuedNormalizer( + data: DeserializedProduct, + transformer: TransformerService +): Observable> { + return of({ + discontinued: data.isDiscontinued, + discontinuedNote: data.discontinuedNote, + } as Product); +} + +export function productNodeNormalizer( + data: DeserializedProduct, + transformer: TransformerService +): Observable> { + const abstractKey = camelize(ApiProductModel.Includes.AbstractProducts); + const nodeKey = camelize(ApiProductModel.Includes.CategoryNodes); + const { [abstractKey]: abstract } = data; + + if (!abstract?.length) { + return of({}); + } + + const { [nodeKey]: node } = abstract[0]; + + return transformer.transform(node, CategoryIdNormalizer); +} + +export function productCategoryNormalizer( + data: DeserializedProduct, + transformer: TransformerService +): Observable> { + const abstractKey = camelize(ApiProductModel.Includes.AbstractProducts); + const nodeKey = camelize(ApiProductModel.Includes.CategoryNodes); + const { [abstractKey]: abstract } = data; + + if (!abstract?.length) { + return of({}); + } + + const { [nodeKey]: node } = abstract[0]; + + return transformer.transform(node, CategoryNormalizer); +} + +export const productNormalizer: Provider[] = [ + { + provide: ProductNormalizer, + useValue: productAttributeNormalizer, + }, + { + provide: ProductNormalizer, + useValue: productPriceNormalizer, + }, + { + provide: ProductNormalizer, + useValue: productMediaSetNormalizer, + }, + { + provide: ProductNormalizer, + useValue: productLabelsNormalizer, + }, + { + provide: ProductNormalizer, + useValue: productAvailabilityNormalizer, + }, + { + provide: ProductNormalizer, + useValue: productDiscontinuedNormalizer, + }, + { + provide: ProductNormalizer, + useValue: productNodeNormalizer, + }, + { + provide: ProductNormalizer, + useValue: productCategoryNormalizer, + }, +]; + +declare global { + interface InjectionTokensContractMap { + [ProductNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/index.ts new file mode 100644 index 000000000..563b5d2ae --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/index.ts @@ -0,0 +1 @@ +export * from './relations-list.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/relations-list.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/relations-list.normalizer.spec.ts new file mode 100644 index 000000000..1a86082a8 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/relations-list.normalizer.spec.ts @@ -0,0 +1,33 @@ +import { DeserializedProductListIncludes } from '@spryker-oryx/product'; +import { of } from 'rxjs'; +import { ProductNormalizer } from '../product/product.normalizer'; +import { listNormalizer } from './relations-list.normalizer'; + +describe('Product relations list Normalizer', () => { + const mockDeserializedProducts: DeserializedProductListIncludes[] = [{}, {}]; + const mockTransformer = { + transform: vi.fn(), + do: vi.fn(), + }; + + describe('listNormalizer', () => { + it('should call Product Normalizer for each DeserializedProduct', () => { + const mockProductsResult = 'mockProductsResult'; + mockTransformer.transform.mockReturnValue(of(mockProductsResult)); + + listNormalizer(mockDeserializedProducts, mockTransformer).subscribe( + (result) => { + expect(result).toEqual( + mockDeserializedProducts.map(() => mockProductsResult) + ); + mockDeserializedProducts.forEach((product) => { + expect(mockTransformer.transform).toHaveBeenCalledWith( + product, + ProductNormalizer + ); + }); + } + ); + }); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/relations-list.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/relations-list.normalizer.ts new file mode 100644 index 000000000..2c7bddeb3 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/relations-list/relations-list.normalizer.ts @@ -0,0 +1,30 @@ +import { Transformer, TransformerService } from '@spryker-oryx/core'; +import { Provider } from '@spryker-oryx/di'; +import { Observable, combineLatest } from 'rxjs'; +import { Product } from '../../../../../models/product.model'; +import { DeserializedProductListIncludes } from '../model'; +import { ProductNormalizer } from '../product/product.normalizer'; + +export const RelationsListNormalizer = 'oryx.RelationsListNormalizer*'; + +export function listNormalizer( + data: DeserializedProductListIncludes[], + transformer: TransformerService +): Observable { + return combineLatest( + data.map((product) => transformer.transform(product, ProductNormalizer)) + ); +} + +export const relationsListNormalizer: Provider[] = [ + { + provide: RelationsListNormalizer, + useValue: listNormalizer, + }, +]; + +declare global { + interface InjectionTokensContractMap { + [RelationsListNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/index.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/index.ts new file mode 100644 index 000000000..e34ea699d --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/index.ts @@ -0,0 +1 @@ +export * from './sort.normalizer'; diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/sort.normalizer.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/sort.normalizer.spec.ts new file mode 100644 index 000000000..b04933995 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/sort.normalizer.spec.ts @@ -0,0 +1,60 @@ +import { ApiProductListModel } from '@spryker-oryx/product'; +import { sortNormalizer } from './sort.normalizer'; + +const sort = { + sortParamNames: [ + 'rating', + 'name_asc', + 'name_desc', + 'price_asc', + 'price_desc', + 'popularity', + ], + sortParamLocalizedNames: { + rating: 'Sort by product ratings', + name_asc: 'Sort by name ascending', + name_desc: 'Sort by name descending', + price_asc: 'Sort by price ascending', + price_desc: 'Sort by price descending', + popularity: 'Sort by popularity', + }, + currentSortParam: null, + currentSortOrder: null, +}; + +describe('Product Sorting Normalizers', () => { + it('should return normalized product sorting', () => { + expect(sortNormalizer(sort as unknown as ApiProductListModel.Sort)).toEqual( + { + sortOrder: null, + sortParam: null, + sortValues: [ + { + sortKey: 'rating', + sortName: 'Sort by product ratings', + }, + { + sortKey: 'name_asc', + sortName: 'Sort by name ascending', + }, + { + sortKey: 'name_desc', + sortName: 'Sort by name descending', + }, + { + sortKey: 'price_asc', + sortName: 'Sort by price ascending', + }, + { + sortKey: 'price_desc', + sortName: 'Sort by price descending', + }, + { + sortKey: 'popularity', + sortName: 'Sort by popularity', + }, + ], + } + ); + }); +}); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/sort.normalizer.ts b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/sort.normalizer.ts new file mode 100644 index 000000000..faf5e946a --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/normalizers/sort/sort.normalizer.ts @@ -0,0 +1,28 @@ +import { Transformer } from '@spryker-oryx/core'; +import { snakify } from '@spryker-oryx/core/utilities'; +import { ApiProductListModel, ProductListSort } from '../../../../../models'; + +export const SortNormalizer = 'oryx.SortNormalizer*'; + +export function sortNormalizer( + sort: ApiProductListModel.Sort +): ProductListSort { + const { currentSortParam, currentSortOrder, sortParamLocalizedNames } = sort; + + return { + sortOrder: currentSortOrder, + sortParam: currentSortParam, + sortValues: Object.entries(sortParamLocalizedNames).map( + ([sortKey, sortName]) => ({ + sortKey: snakify(sortKey), + sortName, + }) + ), + }; +} + +declare global { + interface InjectionTokensContractMap { + [SortNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/product/src/services/adapter/spryker-glue/product-includes.ts b/libs/domain/product/src/services/adapter/spryker-glue/product-includes.ts new file mode 100644 index 000000000..c8146dbd5 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/product-includes.ts @@ -0,0 +1,22 @@ +import { provideIncludes } from '@spryker-oryx/core'; +import { PRODUCT } from '../../../entity'; +import { ApiProductModel } from '../../../models'; + +export const productIncludes = provideIncludes(PRODUCT, [ + ApiProductModel.Includes.ConcreteProductImageSets, + ApiProductModel.Includes.ConcreteProductPrices, + ApiProductModel.Includes.ConcreteProductAvailabilities, + ApiProductModel.Includes.Labels, + ApiProductModel.Includes.AbstractProducts, + { + include: ApiProductModel.Includes.CategoryNodes, + fields: [ + ApiProductModel.CategoryNodeFields.MetaDescription, + ApiProductModel.CategoryNodeFields.NodeId, + ApiProductModel.CategoryNodeFields.Order, + ApiProductModel.CategoryNodeFields.Name, + ApiProductModel.CategoryNodeFields.Parents, + ApiProductModel.CategoryNodeFields.IsActive, + ], + }, +]); diff --git a/libs/domain/product/src/services/adapter/spryker-glue/product.adapter.ts b/libs/domain/product/src/services/adapter/spryker-glue/product.adapter.ts new file mode 100644 index 000000000..cfc3a7e09 --- /dev/null +++ b/libs/domain/product/src/services/adapter/spryker-glue/product.adapter.ts @@ -0,0 +1,15 @@ +import { Observable } from 'rxjs'; +import { Product, ProductQualifier } from '../../../models'; + +export interface ProductAdapter { + getKey(qualifier: ProductQualifier): string; + get(qualifier: ProductQualifier): Observable; +} + +export const ProductAdapter = 'oryx.ProductAdapter'; + +declare global { + interface InjectionTokensContractMap { + [ProductAdapter]: ProductAdapter; + } +} diff --git a/libs/domain/product/src/services/category/adapter/glue-product-category.adapter.spec.ts b/libs/domain/product/src/services/category/adapter/glue-product-category.adapter.spec.ts new file mode 100644 index 000000000..8d965a904 --- /dev/null +++ b/libs/domain/product/src/services/category/adapter/glue-product-category.adapter.spec.ts @@ -0,0 +1,98 @@ +import { HttpService, JsonAPITransformerService } from '@spryker-oryx/core'; +import { HttpTestService } from '@spryker-oryx/core/testing'; +import { createInjector, destroyInjector } from '@spryker-oryx/di'; +import { of } from 'rxjs'; +import { ApiProductModel } from '../../../models'; +import { GlueProductCategoryAdapter } from './glue-product-category.adapter'; +import { CategoryNodeNormalizer, CategoryTreeNormalizer } from './normalizers'; +import { ProductCategoryAdapter } from './product-category.adapter'; + +const mockApiUrl = 'mockApiUrl'; +const mockTransformer = { + do: vi.fn().mockReturnValue(() => of(null)), +}; + +describe('GlueProductCategoryAdapter', () => { + let adapter: ProductCategoryAdapter; + let http: HttpTestService; + + beforeEach(() => { + const testInjector = createInjector({ + providers: [ + { + provide: HttpService, + useClass: HttpTestService, + }, + { + provide: ProductCategoryAdapter, + useClass: GlueProductCategoryAdapter, + }, + { + provide: 'SCOS_BASE_URL', + useValue: mockApiUrl, + }, + { + provide: JsonAPITransformerService, + useValue: mockTransformer, + }, + ], + }); + + adapter = testInjector.inject(ProductCategoryAdapter); + http = testInjector.inject(HttpService); + }); + + afterEach(() => { + destroyInjector(); + vi.clearAllMocks(); + }); + + describe('get', () => { + const mockCategoryId = 'mockId'; + const fields = `fields[${ApiProductModel.Includes.CategoryNodes}]=`; + + beforeEach(() => { + adapter.get({ id: mockCategoryId }); + }); + + it('should build url based on category id', () => { + expect(http.url).toContain( + `${mockApiUrl}/category-nodes/${mockCategoryId}` + ); + }); + + it('should add fields for category-nodes to the url', () => { + expect(http.url).toContain(fields); + }); + + [ + ApiProductModel.CategoryNodeFields.MetaDescription, + ApiProductModel.CategoryNodeFields.NodeId, + ApiProductModel.CategoryNodeFields.Order, + ApiProductModel.CategoryNodeFields.Name, + ApiProductModel.CategoryNodeFields.Parents, + ].forEach((field) => + it(`should contain ${field} in the url`, () => { + expect(http.url?.split(fields)[1]).toContain(field); + }) + ); + + it('should call transformer with category node normalizer', () => { + expect(mockTransformer.do).toHaveBeenCalledWith(CategoryNodeNormalizer); + }); + }); + + describe('getTree', () => { + beforeEach(() => { + adapter.getTree(); + }); + + it('should build the url', () => { + expect(http.url).toContain(`${mockApiUrl}/category-trees`); + }); + + it('should call transformer with category tree normalizer', () => { + expect(mockTransformer.do).toHaveBeenCalledWith(CategoryTreeNormalizer); + }); + }); +}); diff --git a/libs/domain/product/src/services/category/adapter/glue-product-category.adapter.ts b/libs/domain/product/src/services/category/adapter/glue-product-category.adapter.ts new file mode 100644 index 000000000..818fdb07e --- /dev/null +++ b/libs/domain/product/src/services/category/adapter/glue-product-category.adapter.ts @@ -0,0 +1,48 @@ +import { HttpService, JsonAPITransformerService } from '@spryker-oryx/core'; +import { inject } from '@spryker-oryx/di'; +import { Observable } from 'rxjs'; +import { + ApiProductCategoryModel, + ProductCategory, + ProductCategoryQualifier, +} from '../../../models'; +import { CategoryNodeNormalizer, CategoryTreeNormalizer } from './normalizers'; +import { ProductCategoryAdapter } from './product-category.adapter'; + +export class GlueProductCategoryAdapter implements ProductCategoryAdapter { + constructor( + protected http = inject(HttpService), + protected SCOS_BASE_URL = inject('SCOS_BASE_URL'), + protected transformer = inject(JsonAPITransformerService) + ) {} + + get( + qualifier: ProductCategoryQualifier | string + ): Observable { + if (typeof qualifier === 'string') return this.get({ id: qualifier }); + + const fields = [ + ApiProductCategoryModel.Fields.MetaDescription, + ApiProductCategoryModel.Fields.NodeId, + ApiProductCategoryModel.Fields.Order, + ApiProductCategoryModel.Fields.Name, + ApiProductCategoryModel.Fields.Parents, + ]; + + return this.http + .get( + `${this.SCOS_BASE_URL}/category-nodes/${ + qualifier.id + }?fields[category-nodes]=${fields.join(',')}` + ) + .pipe(this.transformer.do(CategoryNodeNormalizer)); + } + + getTree(qualifier?: ProductCategoryQualifier): Observable { + return this.http + .get( + `${this.SCOS_BASE_URL}/category-trees` + ) + .pipe(this.transformer.do(CategoryTreeNormalizer)); + } +} diff --git a/libs/domain/product/src/services/category/adapter/index.ts b/libs/domain/product/src/services/category/adapter/index.ts index a9f80beef..121270751 100644 --- a/libs/domain/product/src/services/category/adapter/index.ts +++ b/libs/domain/product/src/services/category/adapter/index.ts @@ -1,3 +1,3 @@ -export * from './default-product-category.adapter'; +export * from './glue-product-category.adapter'; export * from './normalizers'; export * from './product-category.adapter'; diff --git a/libs/domain/product/src/services/default-product.service.spec.ts b/libs/domain/product/src/services/default-product.service.spec.ts index 15edd5fb6..5c92435b5 100644 --- a/libs/domain/product/src/services/default-product.service.spec.ts +++ b/libs/domain/product/src/services/default-product.service.spec.ts @@ -4,7 +4,7 @@ import { productQueries } from '@spryker-oryx/product'; import { Observable, of, switchMap, take } from 'rxjs'; import { SpyInstance } from 'vitest'; import { ProductQualifier } from '../models'; -import { ProductAdapter } from './adapter/product.adapter'; +import { ProductAdapter } from './adapter/spryker-glue/product.adapter'; import { DefaultProductService } from './default-product.service'; import { ProductService } from './product.service'; diff --git a/libs/domain/product/src/services/index.ts b/libs/domain/product/src/services/index.ts index 9866ff623..09e2cf7b4 100644 --- a/libs/domain/product/src/services/index.ts +++ b/libs/domain/product/src/services/index.ts @@ -1,4 +1,5 @@ -export * from './adapter'; +export * from './adapter/mock'; +export * from './adapter/spryker-glue'; export * from './category'; export * from './default-product.service'; export * from './images'; diff --git a/libs/domain/product/src/services/list/adapter/glue-product-list.adapter.spec.ts b/libs/domain/product/src/services/list/adapter/glue-product-list.adapter.spec.ts new file mode 100644 index 000000000..76a8a91ee --- /dev/null +++ b/libs/domain/product/src/services/list/adapter/glue-product-list.adapter.spec.ts @@ -0,0 +1,149 @@ +import { + HttpService, + JsonApiIncludeService, + JsonAPITransformerService, +} from '@spryker-oryx/core'; +import { HttpTestService } from '@spryker-oryx/core/testing'; +import { createInjector, destroyInjector } from '@spryker-oryx/di'; +import { featureVersion } from '@spryker-oryx/utilities'; +import { of } from 'rxjs'; +import { ApiProductModel, ProductListQualifier } from '../../../models'; +import { ProductListNormalizer } from '../../adapter/spryker-glue'; +import { GlueProductListAdapter } from './glue-product-list.adapter'; +import { ProductListAdapter } from './product-list.adapter'; + +const mockApiUrl = 'mockApiUrl'; +const mockProducts = { + data: [ + { + attributes: { + abstractProducts: [], + concreteProducts: [], + }, + }, + ], +}; +const mockTransformer = { + do: vi.fn().mockReturnValue(() => of(null)), +}; + +class MockJsonApiIncludeService implements Partial { + get() { + return of(''); + } +} + +describe('GlueProductCategoryAdapter', () => { + let service: ProductListAdapter; + let http: HttpTestService; + + beforeEach(() => { + const testInjector = createInjector({ + providers: [ + { + provide: HttpService, + useClass: HttpTestService, + }, + { + provide: ProductListAdapter, + useClass: GlueProductListAdapter, + }, + { + provide: 'SCOS_BASE_URL', + useValue: mockApiUrl, + }, + { + provide: JsonAPITransformerService, + useValue: mockTransformer, + }, + { + provide: JsonApiIncludeService, + useClass: MockJsonApiIncludeService, + }, + ], + }); + + service = testInjector.inject(ProductListAdapter); + http = testInjector.inject(HttpService) as HttpTestService; + }); + + afterEach(() => { + destroyInjector(); + vi.clearAllMocks(); + }); + + it('should be provided', () => { + expect(service).toBeInstanceOf(GlueProductListAdapter); + }); + + describe('get method', () => { + const mockQualifier: ProductListQualifier = { q: 'test' }; + + beforeEach(() => { + http.flush(mockProducts); + + service.get(mockQualifier); + }); + + it('should build correct base url', () => { + expect(http.url).toContain( + `${mockApiUrl}/catalog-search?q=${mockQualifier.q}` + ); + }); + + if (featureVersion >= '1.1') { + describe('category-nodes fields', () => { + const fields = `fields[${ApiProductModel.Includes.CategoryNodes}]=`; + + it('should add fields for category-nodes to the url', () => { + expect(http.url).toContain(fields); + }); + + [ + ApiProductModel.CategoryNodeFields.MetaDescription, + ApiProductModel.CategoryNodeFields.NodeId, + ApiProductModel.CategoryNodeFields.Order, + ApiProductModel.CategoryNodeFields.Name, + ApiProductModel.CategoryNodeFields.Children, + ApiProductModel.CategoryNodeFields.IsActive, + ].forEach((field) => + it(`should contain ${field} in the url`, () => { + expect(http.url?.split(fields)[1]).toContain(field); + }) + ); + }); + } + + it('should call transformer with proper normalizer', () => { + service.get(mockQualifier).subscribe(); + + expect(mockTransformer.do).toHaveBeenCalledWith(ProductListNormalizer); + }); + + it('should return transformed data', () => { + const mockTransformerData = 'mockTransformerData'; + const callback = vi.fn(); + mockTransformer.do.mockReturnValue(() => of(mockTransformerData)); + + service.get(mockQualifier).subscribe(callback); + + expect(callback).toHaveBeenCalledWith(mockTransformerData); + }); + }); + + describe('getKey method', () => { + it('should generate key from query string', () => { + const query = { q: 'test', brand: 'sony', color: 'red' }; + expect(service.getKey(query)).toBe('q=test&brand=sony&color=red'); + }); + + it('should correct transform alias to query string', () => { + const query = { q: 'test', maxPrice: 12, minPrice: 1 }; + expect(service.getKey(query)).toBe('q=test&price[max]=12&price[min]=1'); + }); + + it('should generate empty string when query param is not provided', () => { + expect(service.getKey({})).toBe(''); + }); + }); +}); diff --git a/libs/domain/product/src/services/list/adapter/glue-product-list.adapter.ts b/libs/domain/product/src/services/list/adapter/glue-product-list.adapter.ts new file mode 100644 index 000000000..2cba47eb6 --- /dev/null +++ b/libs/domain/product/src/services/list/adapter/glue-product-list.adapter.ts @@ -0,0 +1,113 @@ +import { + HttpService, + JsonApiIncludeService, + JsonAPITransformerService, +} from '@spryker-oryx/core'; +import { inject } from '@spryker-oryx/di'; +import { featureVersion } from '@spryker-oryx/utilities'; +import { Observable, switchMap } from 'rxjs'; +import { PRODUCTS } from '../../../entity'; +import { + ApiProductModel, + ProductList, + ProductListQualifier, +} from '../../../models'; +import { ProductListNormalizer } from '../../adapter/spryker-glue'; +import { ProductListAdapter } from './product-list.adapter'; + +export class GlueProductListAdapter implements ProductListAdapter { + protected queryEndpoint = 'catalog-search'; + protected readonly alias: Record = { + minPrice: 'price[min]', + maxPrice: 'price[max]', + minRating: 'rating[min]', + storageCapacity: 'storage_capacity[]', + }; + + constructor( + protected http = inject(HttpService), + protected SCOS_BASE_URL = inject('SCOS_BASE_URL'), + protected transformer = inject(JsonAPITransformerService), + protected includeService = inject(JsonApiIncludeService) + ) {} + + getKey(qualifier: ProductListQualifier): string { + const qualifierKeys = Object.keys(qualifier); + + return qualifierKeys.length + ? qualifierKeys + .reduce((params: string[], key) => { + const qualifierKey = key as keyof ProductListQualifier; + const param = qualifier[qualifierKey]; + + if (param) { + const paramList = String(param).split(','); + + params.push( + paramList.length > 1 + ? paramList + .map( + (plv) => + `${this.alias[qualifierKey] ?? qualifierKey}[]=${plv}` + ) + .join('&') + : `${this.alias[qualifierKey] ?? qualifierKey}=${param}` + ); + } + return params; + }, []) + .join('&') + : ''; + } + + get(qualifier: ProductListQualifier): Observable { + if (featureVersion >= '1.4') { + return this.includeService.get({ resource: PRODUCTS }).pipe( + switchMap((includes) => + this.http.get( + `${this.SCOS_BASE_URL}/${this.queryEndpoint}?${this.getKey( + qualifier + )}&${includes}` + ) + ), + this.transformer.do(ProductListNormalizer) + ); + } else { + const include = [ + ApiProductModel.Includes.AbstractProducts, + ApiProductModel.Includes.CategoryNodes, + ApiProductModel.Includes.ConcreteProducts, + ApiProductModel.Includes.ConcreteProductImageSets, + ApiProductModel.Includes.ConcreteProductPrices, + ApiProductModel.Includes.ConcreteProductAvailabilities, + ApiProductModel.Includes.Labels, + ]; + + const categoryNodeFields = [ + ApiProductModel.CategoryNodeFields.MetaDescription, + ApiProductModel.CategoryNodeFields.NodeId, + ApiProductModel.CategoryNodeFields.Order, + ApiProductModel.CategoryNodeFields.Name, + ApiProductModel.CategoryNodeFields.Children, + ApiProductModel.CategoryNodeFields.IsActive, + ]; + + const fields = + featureVersion >= '1.1' + ? `&fields[${ + ApiProductModel.Includes.CategoryNodes + }]=${categoryNodeFields.join(',')} + ` + : ''; + + return this.http + .get( + `${this.SCOS_BASE_URL}/${this.queryEndpoint}?${this.getKey( + qualifier + )}&include=${include?.join(',')} + ${fields}` + ) + .pipe(this.transformer.do(ProductListNormalizer)); + } + } +} diff --git a/libs/domain/product/src/services/list/adapter/index.ts b/libs/domain/product/src/services/list/adapter/index.ts index b548370b0..c8ff2ac09 100644 --- a/libs/domain/product/src/services/list/adapter/index.ts +++ b/libs/domain/product/src/services/list/adapter/index.ts @@ -1,2 +1,2 @@ -export * from './default-product-list.adapter'; +export * from './glue-product-list.adapter'; export * from './product-list.adapter'; diff --git a/libs/domain/product/src/services/product.providers.ts b/libs/domain/product/src/services/product.providers.ts index e4c200c5c..01960213e 100644 --- a/libs/domain/product/src/services/product.providers.ts +++ b/libs/domain/product/src/services/product.providers.ts @@ -11,14 +11,15 @@ import { AvailabilityNormalizer, CategoryIdNormalizer, ConcreteProductsNormalizer, - DefaultProductAdapter, DefaultProductMediaNormalizer, FacetCategoryNormalizer, FacetNormalizer, FacetRangeNormalizer, FacetRatingNormalizer, + GlueProductAdapter, PriceNormalizer, ProductAdapter, + ProductLabelsNormalizer, ProductMediaSetNormalizer, availabilityNormalizer, categoryIdNormalizer, @@ -31,26 +32,30 @@ import { mediaSetNormalizer, priceNormalizer, productIncludes, + productLabelNormalizer, productListNormalizer, productNormalizer, } from './adapter'; -import { - ProductLabelsNormalizer, - productLabelNormalizer, -} from './adapter/normalizers/labels/labels.normalizer'; +import { MockProductListAdapter } from './adapter/mock'; +import { MockProductCategoryAdapter } from './adapter/mock/mock-category.adapter'; +import { MockProductAdapter } from './adapter/mock/mock-product.adapter'; +import { MockProductRelationsListAdapter } from './adapter/mock/product-relations/mock-product-relations-list.adapter'; import { PaginationNormalizer, paginationNormalizer, -} from './adapter/normalizers/pagination'; -import { relationsListNormalizer } from './adapter/normalizers/relations-list'; -import { SortNormalizer, sortNormalizer } from './adapter/normalizers/sort'; +} from './adapter/spryker-glue/normalizers/pagination'; +import { relationsListNormalizer } from './adapter/spryker-glue/normalizers/relations-list'; +import { + SortNormalizer, + sortNormalizer, +} from './adapter/spryker-glue/normalizers/sort'; import { CategoryListNormalizer, CategoryNodeNormalizer, CategoryNormalizer, CategoryTreeNormalizer, - DefaultProductCategoryAdapter, DefaultProductCategoryService, + GlueProductCategoryAdapter, ProductCategoryAdapter, ProductCategoryService, categoryEffects, @@ -69,7 +74,6 @@ import { } from './images/product-media.config'; import { productJsonLdNormalizers } from './jsonld'; import { - DefaultProductListAdapter, DefaultProductListPageService, DefaultProductListService, ProductListAdapter, @@ -84,7 +88,6 @@ import { } from './product-context'; import { ProductService } from './product.service'; import { - DefaultProductRelationsListAdapter, DefaultProductRelationsListService, ProductRelationsListAdapter, ProductRelationsListService, @@ -103,34 +106,18 @@ import { productQueries } from './state/queries'; export const ProductTokenResourceResolverToken = `${TokenResourceResolvers}PRODUCT`; -export const productProviders: Provider[] = [ +export const glueProductConnectors = [ { provide: ProductAdapter, - useClass: DefaultProductAdapter, - }, - { - provide: ProductService, - useClass: DefaultProductService, + useClass: GlueProductAdapter, }, { provide: ProductListAdapter, - useClass: DefaultProductListAdapter, - }, - { - provide: ProductListService, - useClass: DefaultProductListService, - }, - { - provide: ProductRelationsListService, - useClass: DefaultProductRelationsListService, + useClass: MockProductListAdapter, }, { provide: ProductRelationsListAdapter, - useClass: DefaultProductRelationsListAdapter, - }, - { - provide: ProductListPageService, - useClass: DefaultProductListPageService, + useClass: MockProductRelationsListAdapter, }, { provide: PriceNormalizer, @@ -156,7 +143,6 @@ export const productProviders: Provider[] = [ provide: FacetCategoryNormalizer, useValue: facetCategoryNormalizer, }, - //TODO: drop and use ordinary range normalizer after https://spryker.atlassian.net/browse/CC-31032 { provide: FacetRatingNormalizer, useValue: facetRatingNormalizer, @@ -181,6 +167,71 @@ export const productProviders: Provider[] = [ provide: ConcreteProductsNormalizer, useValue: concreteProductsNormalizer, }, + ...productNormalizer, + ...productListNormalizer, + ...relationsListNormalizer, + { + provide: CategoryIdNormalizer, + useValue: categoryIdNormalizer, + }, + { + provide: CategoryNormalizer, + useFactory: categoryNormalizerFactory, + }, + { + provide: CategoryListNormalizer, + useFactory: categoryListNormalizerFactory, + }, + { + provide: CategoryNodeNormalizer, + useValue: categoryNodeNormalizer, + }, + { + provide: CategoryTreeNormalizer, + useValue: categoryTreeNormalizer, + }, + { + provide: ProductCategoryAdapter, + useClass: GlueProductCategoryAdapter, + }, +]; + +export const mockProductConnectors = [ + { + provide: ProductAdapter, + useClass: MockProductAdapter, + }, + { + provide: ProductListAdapter, + useClass: MockProductListAdapter, + }, + { + provide: ProductRelationsListAdapter, + useClass: MockProductRelationsListAdapter, + }, + { + provide: ProductCategoryAdapter, + useClass: MockProductCategoryAdapter, + }, +]; + +export const productProviders: Provider[] = [ + { + provide: ProductListService, + useClass: DefaultProductListService, + }, + { + provide: ProductService, + useClass: DefaultProductService, + }, + { + provide: ProductListPageService, + useClass: DefaultProductListPageService, + }, + { + provide: ProductRelationsListService, + useClass: DefaultProductRelationsListService, + }, { provide: ProductImageService, useClass: DefaultProductImageService, @@ -189,9 +240,6 @@ export const productProviders: Provider[] = [ provide: ProductMediaConfig, useValue: productMediaConfig, }, - ...productNormalizer, - ...productListNormalizer, - ...relationsListNormalizer, ...productQueries, ...productEffects, ...categoryEffects, @@ -215,30 +263,6 @@ export const productProviders: Provider[] = [ provide: PageMetaResolver, useClass: ProductPageRobotMetaResolver, }, - { - provide: CategoryIdNormalizer, - useValue: categoryIdNormalizer, - }, - { - provide: CategoryNormalizer, - useFactory: categoryNormalizerFactory, - }, - { - provide: CategoryListNormalizer, - useFactory: categoryListNormalizerFactory, - }, - { - provide: CategoryNodeNormalizer, - useValue: categoryNodeNormalizer, - }, - { - provide: CategoryTreeNormalizer, - useValue: categoryTreeNormalizer, - }, - { - provide: ProductCategoryAdapter, - useClass: DefaultProductCategoryAdapter, - }, { provide: ProductCategoryService, useClass: DefaultProductCategoryService, @@ -258,3 +282,13 @@ export const productProviders: Provider[] = [ }), ...productJsonLdNormalizers, ]; + +export const glueProductProviders = [ + ...productProviders, + ...glueProductConnectors, +]; + +export const mockProductProviders = [ + ...productProviders, + ...mockProductConnectors, +]; diff --git a/libs/domain/product/src/services/related/adapter/glue-product-relations-list.adapter.spec.ts b/libs/domain/product/src/services/related/adapter/glue-product-relations-list.adapter.spec.ts new file mode 100644 index 000000000..6a8592c79 --- /dev/null +++ b/libs/domain/product/src/services/related/adapter/glue-product-relations-list.adapter.spec.ts @@ -0,0 +1,101 @@ +import { HttpService, JsonAPITransformerService } from '@spryker-oryx/core'; +import { HttpTestService } from '@spryker-oryx/core/testing'; +import { createInjector, destroyInjector } from '@spryker-oryx/di'; +import { of } from 'rxjs'; +import { ProductQualifier } from '../../../models'; +import { RelationsListNormalizer } from '../../adapter/spryker-glue/normalizers/relations-list'; +import { GlueProductRelationsListAdapter } from './glue-product-relations-list.adapter'; +import { ProductRelationsListAdapter } from './product-relations-list.adapter'; + +const mockApiUrl = 'mockApiUrl'; +const mockProducts = { + data: [ + { + attributes: { + abstractProducts: [], + concreteProducts: [], + }, + }, + ], +}; + +const mockTransformer = { + do: vi.fn().mockReturnValue(() => of(null)), +}; + +describe('GlueProductRelationsListAdapter', () => { + let adapter: ProductRelationsListAdapter; + let http: HttpTestService; + + beforeEach(() => { + const testInjector = createInjector({ + providers: [ + { + provide: HttpService, + useClass: HttpTestService, + }, + { + provide: ProductRelationsListAdapter, + useClass: GlueProductRelationsListAdapter, + }, + { + provide: 'SCOS_BASE_URL', + useValue: mockApiUrl, + }, + { + provide: JsonAPITransformerService, + useValue: mockTransformer, + }, + ], + }); + + adapter = testInjector.inject( + ProductRelationsListAdapter + ) as GlueProductRelationsListAdapter; + http = testInjector.inject(HttpService) as HttpTestService; + }); + + afterEach(() => { + vi.clearAllMocks(); + destroyInjector(); + }); + + it('should be provided', () => { + expect(adapter).toBeInstanceOf(GlueProductRelationsListAdapter); + }); + + describe('get method', () => { + const mockQualifier: ProductQualifier = { sku: 'test' }; + + beforeEach(() => { + http.flush(mockProducts); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should have correct http path', () => { + adapter.get(mockQualifier); + expect(http.url).toContain( + `concrete-products/${mockQualifier.sku}/concrete-alternative-products` + ); + }); + + it('should call transformer with proper normalizer', () => { + adapter.get(mockQualifier).subscribe(); + + expect(mockTransformer.do).toHaveBeenCalledWith(RelationsListNormalizer); + }); + + it('should return transformed data', () => { + const mockTransformerData = 'mockTransformerData'; + const callback = vi.fn(); + mockTransformer.do.mockReturnValue(() => of(mockTransformerData)); + + adapter.get(mockQualifier).subscribe(callback); + + expect(callback).toHaveBeenCalledWith(mockTransformerData); + }); + }); +}); diff --git a/libs/domain/product/src/services/related/adapter/glue-product-relations-list.adapter.ts b/libs/domain/product/src/services/related/adapter/glue-product-relations-list.adapter.ts new file mode 100644 index 000000000..7b61b9351 --- /dev/null +++ b/libs/domain/product/src/services/related/adapter/glue-product-relations-list.adapter.ts @@ -0,0 +1,32 @@ +import { HttpService, JsonAPITransformerService } from '@spryker-oryx/core'; +import { inject } from '@spryker-oryx/di'; +import { Observable } from 'rxjs'; +import { ApiProductModel, Product, ProductQualifier } from '../../../models'; +import { RelationsListNormalizer } from '../../adapter/spryker-glue/normalizers/relations-list'; +import { ProductRelationsListAdapter } from './product-relations-list.adapter'; + +export class GlueProductRelationsListAdapter + implements ProductRelationsListAdapter +{ + protected getQueryEndpoint(sku: string): string { + return `concrete-products/${sku}/concrete-alternative-products`; + } + + constructor( + protected http = inject(HttpService), + protected SCOS_BASE_URL = inject('SCOS_BASE_URL'), + protected transformer = inject(JsonAPITransformerService) + ) {} + + get({ sku }: ProductQualifier): Observable { + const include = [ApiProductModel.Includes.ConcreteProductPrices]; + + return this.http + .get( + `${this.SCOS_BASE_URL}/${this.getQueryEndpoint( + sku! + )}?include=${include?.join(',')}` + ) + .pipe(this.transformer.do(RelationsListNormalizer)); + } +} diff --git a/libs/domain/product/src/services/related/adapter/index.ts b/libs/domain/product/src/services/related/adapter/index.ts index a3dff22a5..b9823b9e8 100644 --- a/libs/domain/product/src/services/related/adapter/index.ts +++ b/libs/domain/product/src/services/related/adapter/index.ts @@ -1,2 +1,2 @@ -export * from './default-product-relations-list.adapter'; +export * from './glue-product-relations-list.adapter'; export * from './product-relations-list.adapter'; diff --git a/libs/domain/product/src/services/state/queries.ts b/libs/domain/product/src/services/state/queries.ts index 34db7390b..350ff717c 100644 --- a/libs/domain/product/src/services/state/queries.ts +++ b/libs/domain/product/src/services/state/queries.ts @@ -3,7 +3,7 @@ import { inject } from '@spryker-oryx/di'; import { LocaleChanged } from '@spryker-oryx/i18n'; import { CurrencyChanged, PriceModeChanged } from '@spryker-oryx/site'; import { Product, ProductQualifier } from '../../models'; -import { ProductAdapter } from '../adapter'; +import { ProductAdapter } from '../adapter/spryker-glue'; import { ProductLoaded } from './events'; export const ProductQuery = 'oryx.productQuery'; diff --git a/libs/domain/search/src/feature.ts b/libs/domain/search/src/feature.ts index d209ccf2d..f4ad1f594 100644 --- a/libs/domain/search/src/feature.ts +++ b/libs/domain/search/src/feature.ts @@ -1,6 +1,10 @@ import { AppFeature } from '@spryker-oryx/core'; import * as components from './components'; -import { searchProviders } from './services'; +import { + glueSearchProviders, + mockSearchProviders, + searchProviders, +} from './services'; export * from './components'; export const searchComponents = Object.values(components); @@ -9,3 +13,13 @@ export const searchFeature: AppFeature = { providers: searchProviders, components: searchComponents, }; + +export const glueSearchFeature: AppFeature = { + providers: glueSearchProviders, + components: searchComponents, +}; + +export const mockSearchFeature: AppFeature = { + providers: mockSearchProviders, + components: searchComponents, +}; diff --git a/libs/domain/search/src/mocks/src/mock-search.providers.ts b/libs/domain/search/src/mocks/src/mock-search.providers.ts index 8dd938279..d0f7248dd 100644 --- a/libs/domain/search/src/mocks/src/mock-search.providers.ts +++ b/libs/domain/search/src/mocks/src/mock-search.providers.ts @@ -1,17 +1,17 @@ import { Provider } from '@spryker-oryx/di'; import { DefaultFacetListService, - DefaultSuggestionAdapter, - defaultSuggestionRenderer, DefaultSuggestionRendererService, FacetColorsMapping, FacetListService, - productSuggestionRenderer, + GlueSuggestionAdapter, SuggestionAdapter, SuggestionField, SuggestionRenderer, SuggestionRendererService, SuggestionService, + defaultSuggestionRenderer, + productSuggestionRenderer, } from '@spryker-oryx/search'; import { SortingService } from '../../services/sorting.service'; import { mockFacetColors } from './mock-facet-colors'; @@ -21,7 +21,7 @@ import { MockSuggestionService } from './suggestion/mock-suggestion.service'; export const mockSearchProviders: Provider[] = [ { provide: SuggestionAdapter, - useClass: DefaultSuggestionAdapter, + useClass: GlueSuggestionAdapter, }, { provide: SuggestionService, diff --git a/libs/domain/search/src/services/adapter/index.ts b/libs/domain/search/src/services/adapter/index.ts index 9d8b51a70..75f2d6239 100644 --- a/libs/domain/search/src/services/adapter/index.ts +++ b/libs/domain/search/src/services/adapter/index.ts @@ -1,4 +1,2 @@ -export * from './content-suggestion.adapter'; -export * from './default-suggestion.adapter'; -export * from './normalizers'; -export * from './suggestion.adapter'; +export * from './mock'; +export * from './spryker-glue'; diff --git a/libs/domain/search/src/services/adapter/mock/index.ts b/libs/domain/search/src/services/adapter/mock/index.ts new file mode 100644 index 000000000..ddb7e49c1 --- /dev/null +++ b/libs/domain/search/src/services/adapter/mock/index.ts @@ -0,0 +1,3 @@ +export * from './mock-completion'; +export * from './mock-suggestion.adapter'; +export * from './mock-suggestion.generator'; diff --git a/libs/domain/search/src/services/adapter/mock/mock-completion.ts b/libs/domain/search/src/services/adapter/mock/mock-completion.ts new file mode 100644 index 000000000..2ca695fd1 --- /dev/null +++ b/libs/domain/search/src/services/adapter/mock/mock-completion.ts @@ -0,0 +1,15 @@ +export const completion: string[] = [ + 'red product', + 'yellow product for vegans', + 'random red product', + 'product', + 'product offer', + 'product new', + 'product with long title that should be broken into multiple lines', + 'product with title', + 'prodotto dall`italia', + 'two words', + 'best product ever 2000', + 'qwerty asdfg zxcvb', + 'super-fancy long title for the popular product from 1998', +]; diff --git a/libs/domain/search/src/services/adapter/mock/mock-suggestion.adapter.ts b/libs/domain/search/src/services/adapter/mock/mock-suggestion.adapter.ts new file mode 100644 index 000000000..f8ba55826 --- /dev/null +++ b/libs/domain/search/src/services/adapter/mock/mock-suggestion.adapter.ts @@ -0,0 +1,17 @@ +import { + Suggestion, + SuggestionAdapter, + SuggestionQualifier, +} from '@spryker-oryx/search'; +import { Observable, of } from 'rxjs'; +import { completion } from './mock-completion'; +import { createSuggestionMock } from './mock-suggestion.generator'; + +export class MockSuggestionAdapter implements SuggestionAdapter { + getKey(qualifier: SuggestionQualifier): string { + return ''; + } + get(qualifier: SuggestionQualifier): Observable { + return of(createSuggestionMock(qualifier, completion)); + } +} diff --git a/libs/domain/search/src/services/adapter/mock/mock-suggestion.generator.ts b/libs/domain/search/src/services/adapter/mock/mock-suggestion.generator.ts new file mode 100644 index 000000000..449cb760a --- /dev/null +++ b/libs/domain/search/src/services/adapter/mock/mock-suggestion.generator.ts @@ -0,0 +1,80 @@ +import { Product, PRODUCTS } from '@spryker-oryx/product'; +import { RouteType } from '@spryker-oryx/router'; +import { + Suggestion, + SuggestionField, + SuggestionQualifier, + SuggestionResource, +} from '@spryker-oryx/search'; +import { featureVersion } from '@spryker-oryx/utilities'; + +const dummyUrl = (): string => '#'; +const makeTheNameGreatAgain = (name: string): string => + name + .split(' ') + .map((w) => `${w.charAt(0).toUpperCase()}${w.slice(1)}`) + .join(' '); + +const createResources = ( + completion: string[], + resourceName: string, + type: RouteType +): SuggestionResource[] => { + return completion.map((c) => ({ + name: `${makeTheNameGreatAgain(c)} ${resourceName}`, + url: dummyUrl(), + type, + })); +}; + +const createProducts = (completion: string[]): Product[] => { + return completion.map((c, i) => ({ + price: { + defaultPrice: { + value: 1999, + currency: 'EUR', + isNet: false, + }, + ...(i % 2 + ? { + originalPrice: { + value: 2000, + currency: 'EUR', + isNet: false, + }, + } + : {}), + }, + name: `${makeTheNameGreatAgain(c)}`, + description: 'test', + sku: String(i), + images: [ + { + sm: 'https://images.icecat.biz/img/gallery_mediums/29801891_9454.jpg', + lg: 'https://images.icecat.biz/img/gallery/29801891_9454.jpg', + }, + ], + })); +}; + +export const createSuggestionMock = ( + { query }: SuggestionQualifier, + _completions: string[] +): Suggestion => { + const re = new RegExp(`^${query}.*`, 'i'); + const completion = _completions.filter((c) => c.match(re)); + + return { + [SuggestionField.Suggestions]: completion.map((name) => ({ + name, + params: { q: name }, + type: featureVersion >= '1.4' ? PRODUCTS : RouteType.ProductList, + })), + [SuggestionField.Categories]: createResources( + completion, + 'Category', + RouteType.Category + ), + [SuggestionField.Products]: createProducts(completion), + }; +}; diff --git a/libs/domain/search/src/services/adapter/spryker-glue/content-suggestion.adapter.spec.ts b/libs/domain/search/src/services/adapter/spryker-glue/content-suggestion.adapter.spec.ts new file mode 100644 index 000000000..0b0753f5a --- /dev/null +++ b/libs/domain/search/src/services/adapter/spryker-glue/content-suggestion.adapter.spec.ts @@ -0,0 +1,74 @@ +import { ContentService } from '@spryker-oryx/content'; +import { createInjector, destroyInjector } from '@spryker-oryx/di'; +import { of } from 'rxjs'; +import { SuggestionQualifier } from '../../../models'; +import { ContentSuggestionAdapter } from './content-suggestion.adapter'; +import { SuggestionAdapter, SuggestionField } from './suggestion.adapter'; + +const mockContentService = { + getAll: vi.fn(), +}; + +describe('ContentSuggestionAdapter', () => { + let adapter: SuggestionAdapter; + let contentService: ContentService; + + beforeEach(() => { + const testInjector = createInjector({ + providers: [ + { provide: ContentService, useValue: mockContentService }, + { provide: SuggestionAdapter, useClass: ContentSuggestionAdapter }, + ], + }); + + contentService = testInjector.inject(ContentService); + adapter = testInjector.inject(SuggestionAdapter)[0]; + }); + + afterEach(() => { + destroyInjector(); + vi.resetAllMocks(); + }); + + it('should return suggestions based on the provided qualifier', () => { + const callback = vi.fn(); + const qualifier: SuggestionQualifier = { + query: 'test', + entities: ['article'], + }; + + const data = [ + { + id: '1', + heading: 'Test Article 1', + _meta: { + type: 'article', + }, + }, + { + id: '2', + heading: 'Test Article 2', + _meta: { + type: 'article', + }, + }, + ]; + mockContentService.getAll.mockReturnValue(of(data)); + adapter.get(qualifier).subscribe(callback); + expect(contentService.getAll).toHaveBeenCalledWith(qualifier); + expect(callback).toHaveBeenCalledWith({ + [SuggestionField.Contents]: [ + { + name: data[0].heading, + id: data[0].id, + type: data[0]._meta.type, + }, + { + name: data[1].heading, + id: data[1].id, + type: data[1]._meta.type, + }, + ], + }); + }); +}); diff --git a/libs/domain/search/src/services/adapter/spryker-glue/content-suggestion.adapter.ts b/libs/domain/search/src/services/adapter/spryker-glue/content-suggestion.adapter.ts new file mode 100644 index 000000000..1d992d684 --- /dev/null +++ b/libs/domain/search/src/services/adapter/spryker-glue/content-suggestion.adapter.ts @@ -0,0 +1,34 @@ +import { ContentService } from '@spryker-oryx/content'; +import { inject } from '@spryker-oryx/di'; +import { Observable, map } from 'rxjs'; +import { Suggestion, SuggestionQualifier } from '../../../models'; +import { SuggestionAdapter, SuggestionField } from './suggestion.adapter'; + +declare global { + interface ContentFields { + [SuggestionField.Contents]: undefined; + } +} + +export class ContentSuggestionAdapter implements SuggestionAdapter { + protected content = inject(ContentService); + + /** + * @deprecated Since version 1.1. Will be removed. + */ + getKey(qualifier: SuggestionQualifier): string { + return qualifier.query ?? ''; + } + + get(qualifier: SuggestionQualifier): Observable { + return this.content.getAll(qualifier).pipe( + map((data) => ({ + [SuggestionField.Contents]: data?.map((entry) => ({ + name: entry.heading ?? entry._meta.name ?? '', + id: entry.id, + type: entry._meta.type, + })), + })) + ); + } +} diff --git a/libs/domain/search/src/services/adapter/spryker-glue/glue-suggestion.adapter.spec.ts b/libs/domain/search/src/services/adapter/spryker-glue/glue-suggestion.adapter.spec.ts new file mode 100644 index 000000000..340a38d63 --- /dev/null +++ b/libs/domain/search/src/services/adapter/spryker-glue/glue-suggestion.adapter.spec.ts @@ -0,0 +1,125 @@ +import { + HttpService, + JsonApiIncludeService, + JsonAPITransformerService, +} from '@spryker-oryx/core'; +import { HttpTestService } from '@spryker-oryx/core/testing'; +import { createInjector, destroyInjector } from '@spryker-oryx/di'; +import { of } from 'rxjs'; +import { SuggestionQualifier } from '../../../models'; +import { GlueSuggestionAdapter } from './glue-suggestion.adapter'; +import { SuggestionNormalizer } from './normalizers'; +import { SuggestionAdapter } from './suggestion.adapter'; + +const mockApiUrl = 'mockApiUrl'; +const mockSuggestion = { + data: [ + { + attributes: { + completion: [], + categories: [], + cmsPages: [], + }, + }, + ], +}; +const mockTransformer = { + transform: vi.fn().mockReturnValue(of(null)), + do: vi.fn().mockReturnValue(() => of(null)), +}; +class MockJsonApiIncludeService implements Partial { + get() { + return of(''); + } +} + +describe('GlueSuggestionAdapter', () => { + let service: SuggestionAdapter; + let http: HttpTestService; + + beforeEach(() => { + const testInjector = createInjector({ + providers: [ + { + provide: HttpService, + useClass: HttpTestService, + }, + { + provide: SuggestionAdapter, + useClass: GlueSuggestionAdapter, + }, + { + provide: 'SCOS_BASE_URL', + useValue: mockApiUrl, + }, + { + provide: JsonAPITransformerService, + useValue: mockTransformer, + }, + { + provide: JsonApiIncludeService, + useClass: MockJsonApiIncludeService, + }, + ], + }); + + service = testInjector.inject(SuggestionAdapter)[0]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + http = testInjector.inject(HttpService) as HttpTestService; + }); + + afterEach(() => { + destroyInjector(); + }); + + it('should be provided', () => { + expect(service).toBeInstanceOf(GlueSuggestionAdapter); + }); + + describe('get method', () => { + const mockQualifier: SuggestionQualifier = { query: 'test' }; + + beforeEach(() => { + http.flush(mockSuggestion); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should build correct base url', () => { + service.get(mockQualifier); + + expect(http.url).toContain( + `${mockApiUrl}/catalog-search-suggestions?q=${mockQualifier.query}` + ); + }); + + it('should call transformer with proper normalizer', () => { + service.get(mockQualifier).subscribe(); + + expect(mockTransformer.do).toHaveBeenCalledWith(SuggestionNormalizer); + }); + + it('should return transformed data', () => { + const mockTransformerData = 'mockTransformerData'; + const callback = vi.fn(); + mockTransformer.do.mockReturnValue(() => of(mockTransformerData)); + + service.get(mockQualifier).subscribe(callback); + + expect(callback).toHaveBeenCalledWith(mockTransformerData); + }); + }); + + describe('getKey method', () => { + it('should generate key from query string', () => { + const query = 'test'; + expect(service.getKey({ query })).toBe(query); + }); + + it('should generate empty string when query param is not provided', () => { + expect(service.getKey({})).toBe(''); + }); + }); +}); diff --git a/libs/domain/search/src/services/adapter/spryker-glue/glue-suggestion.adapter.ts b/libs/domain/search/src/services/adapter/spryker-glue/glue-suggestion.adapter.ts new file mode 100644 index 000000000..04653fa47 --- /dev/null +++ b/libs/domain/search/src/services/adapter/spryker-glue/glue-suggestion.adapter.ts @@ -0,0 +1,83 @@ +import { + HttpService, + JsonApiIncludeService, + JsonAPITransformerService, +} from '@spryker-oryx/core'; +import { inject } from '@spryker-oryx/di'; +import { ApiProductModel, PRODUCTS } from '@spryker-oryx/product'; +import { featureVersion } from '@spryker-oryx/utilities'; +import { Observable, of, switchMap } from 'rxjs'; +import { + ApiSuggestionModel, + Suggestion, + SuggestionQualifier, +} from '../../../models'; +import { SuggestionNormalizer } from './normalizers'; +import { SuggestionAdapter, SuggestionField } from './suggestion.adapter'; + +export class GlueSuggestionAdapter implements SuggestionAdapter { + protected queryEndpoint = 'catalog-search-suggestions'; + + constructor( + protected http = inject(HttpService), + protected SCOS_BASE_URL = inject('SCOS_BASE_URL'), + protected transformer = inject(JsonAPITransformerService), + protected includeService = inject(JsonApiIncludeService) + ) {} + + /** + * @deprecated Since version 1.1. Will be removed. + */ + getKey({ query }: SuggestionQualifier): string { + return query ?? ''; + } + + get({ query, entities }: SuggestionQualifier): Observable { + if ( + !entities?.length || + entities.some((entity) => + [ + SuggestionField.Categories, + SuggestionField.Suggestions, + SuggestionField.Products, + ].includes(entity as SuggestionField) + ) + ) { + if (featureVersion >= '1.4') { + const includes$ = + !entities?.length || entities?.includes(SuggestionField.Products) + ? this.includeService.get({ resource: PRODUCTS }) + : of(''); + + return includes$.pipe( + switchMap((includes) => + this.http.get( + `${this.SCOS_BASE_URL}/${this.queryEndpoint}?q=${query}&${includes}` + ) + ), + this.transformer.do(SuggestionNormalizer) + ); + } else { + const include = entities?.includes(SuggestionField.Products) + ? [ + ApiProductModel.Includes.AbstractProducts, + ApiProductModel.Includes.CategoryNodes, + ApiProductModel.Includes.ConcreteProducts, + ApiProductModel.Includes.ConcreteProductImageSets, + ApiProductModel.Includes.ConcreteProductPrices, + ApiProductModel.Includes.ConcreteProductAvailabilities, + ApiProductModel.Includes.Labels, + ].join(',') + : ''; + + return this.http + .get( + `${this.SCOS_BASE_URL}/${this.queryEndpoint}?q=${query}&include=${include}` + ) + .pipe(this.transformer.do(SuggestionNormalizer)); + } + } + + return of({}); + } +} diff --git a/libs/domain/search/src/services/adapter/spryker-glue/index.ts b/libs/domain/search/src/services/adapter/spryker-glue/index.ts new file mode 100644 index 000000000..e510c3e4e --- /dev/null +++ b/libs/domain/search/src/services/adapter/spryker-glue/index.ts @@ -0,0 +1,4 @@ +export * from './content-suggestion.adapter'; +export * from './glue-suggestion.adapter'; +export * from './normalizers'; +export * from './suggestion.adapter'; diff --git a/libs/domain/search/src/services/adapter/spryker-glue/normalizers/index.ts b/libs/domain/search/src/services/adapter/spryker-glue/normalizers/index.ts new file mode 100644 index 000000000..33fe96534 --- /dev/null +++ b/libs/domain/search/src/services/adapter/spryker-glue/normalizers/index.ts @@ -0,0 +1 @@ +export * from './suggestion'; diff --git a/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/index.ts b/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/index.ts new file mode 100644 index 000000000..4e88de803 --- /dev/null +++ b/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/index.ts @@ -0,0 +1,2 @@ +export * from './model'; +export * from './suggestion.normalizer'; diff --git a/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/model.ts b/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/model.ts new file mode 100644 index 000000000..a75d4ac5d --- /dev/null +++ b/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/model.ts @@ -0,0 +1,13 @@ +import { CamelCase } from '@spryker-oryx/core/utilities'; +import { + ApiProductModel, + DeserializedProductIncludes, +} from '@spryker-oryx/product'; +import { ApiSuggestionModel } from '../../../../../models'; + +export type DeserializedSuggestion = ApiSuggestionModel.Attributes & + Pick< + DeserializedProductIncludes, + | CamelCase + | CamelCase + >; diff --git a/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/suggestion.normalizer.spec.ts b/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/suggestion.normalizer.spec.ts new file mode 100644 index 000000000..46d88abb3 --- /dev/null +++ b/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/suggestion.normalizer.spec.ts @@ -0,0 +1,72 @@ +import { ConcreteProductsNormalizer } from '@spryker-oryx/product'; +import { RouteType } from '@spryker-oryx/router'; +import { of, take } from 'rxjs'; +import { SuggestionField } from '../../suggestion.adapter'; +import { DeserializedSuggestion } from './model'; +import { + suggestionAttributesNormalizer, + suggestionProductNormalizer, +} from './suggestion.normalizer'; + +const mockDeserializedSuggestion = { + completion: ['A'], + categories: [ + { + name: 'name', + type: RouteType.Category, + }, + ], + cmsPages: [ + { + name: 'name', + }, + ], + abstractProducts: [ + { + abstractProducts: 'abstractProducts', + }, + ], +} as unknown as DeserializedSuggestion; +const mockTransformer = { + transform: vi.fn(), + do: vi.fn(), +}; + +describe('Suggestion Normalizers', () => { + describe('Suggestion Attributes Normalizer', () => { + it('should transform DeserializedSuggestion into Suggestion', () => { + const mockResult = { + [SuggestionField.Suggestions]: [ + { + name: mockDeserializedSuggestion.completion[0], + params: { q: mockDeserializedSuggestion.completion[0] }, + type: RouteType.ProductList, + }, + ], + [SuggestionField.Categories]: mockDeserializedSuggestion.categories, + }; + const normalized = suggestionAttributesNormalizer([ + mockDeserializedSuggestion, + ]); + expect(normalized).toEqual(mockResult); + }); + }); + + describe('Suggestion Product Normalizer', () => { + it('should call transformers and return result', () => { + const mockProductsResult = 'mockProductsResult'; + mockTransformer.transform.mockReturnValue(of(mockProductsResult)); + suggestionProductNormalizer([mockDeserializedSuggestion], mockTransformer) + .pipe(take(1)) + .subscribe((result) => { + expect(result).toEqual({ + products: mockProductsResult, + }); + expect(mockTransformer.transform).toHaveBeenCalledWith( + mockDeserializedSuggestion.abstractProducts, + ConcreteProductsNormalizer + ); + }); + }); + }); +}); diff --git a/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/suggestion.normalizer.ts b/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/suggestion.normalizer.ts new file mode 100644 index 000000000..bfd536cb7 --- /dev/null +++ b/libs/domain/search/src/services/adapter/spryker-glue/normalizers/suggestion/suggestion.normalizer.ts @@ -0,0 +1,67 @@ +import { Transformer, TransformerService } from '@spryker-oryx/core'; +import { camelize } from '@spryker-oryx/core/utilities'; +import { Provider } from '@spryker-oryx/di'; +import { + ApiProductModel, + ConcreteProductsNormalizer, + PRODUCTS, + Product, +} from '@spryker-oryx/product'; +import { RouteType } from '@spryker-oryx/router'; +import { featureVersion } from '@spryker-oryx/utilities'; +import { Observable, map } from 'rxjs'; +import { Suggestion } from '../../../../../models'; +import { SuggestionField } from '../../suggestion.adapter'; +import { DeserializedSuggestion } from './model'; + +export const SuggestionNormalizer = 'oryx.SuggestionNormalizer*'; + +export function suggestionAttributesNormalizer( + data: DeserializedSuggestion[] +): Partial { + const { completion, categories, categoryCollection } = data[0]; + + return { + [SuggestionField.Suggestions]: completion.map((name) => ({ + name, + params: { q: name }, + type: featureVersion >= '1.4' ? PRODUCTS : RouteType.ProductList, + })), + [SuggestionField.Categories]: categories.map((category) => ({ + name: category.name, + id: categoryCollection?.find( + (collection) => category.name === collection.name + )?.idCategory, + type: RouteType.Category, + })), + }; +} + +export function suggestionProductNormalizer( + data: DeserializedSuggestion[], + transformer: TransformerService +): Observable> { + const abstractsKey = camelize(ApiProductModel.Includes.AbstractProducts); + const { [abstractsKey]: products } = data[0]; + + return transformer + .transform(products, ConcreteProductsNormalizer) + .pipe(map((products) => ({ [SuggestionField.Products]: products }))); +} + +export const suggestionNormalizer: Provider[] = [ + { + provide: SuggestionNormalizer, + useValue: suggestionAttributesNormalizer, + }, + { + provide: SuggestionNormalizer, + useValue: suggestionProductNormalizer, + }, +]; + +declare global { + interface InjectionTokensContractMap { + [SuggestionNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/search/src/services/adapter/spryker-glue/suggestion.adapter.ts b/libs/domain/search/src/services/adapter/spryker-glue/suggestion.adapter.ts new file mode 100644 index 000000000..d141e9738 --- /dev/null +++ b/libs/domain/search/src/services/adapter/spryker-glue/suggestion.adapter.ts @@ -0,0 +1,27 @@ +import { Observable } from 'rxjs'; +import { Suggestion, SuggestionQualifier } from '../../../models'; + +export const enum SuggestionField { + Suggestions = 'suggestions', + Categories = 'categories', + Products = 'products', + Contents = 'contents', +} + +export type SuggestionEntities = (SuggestionField | string)[]; + +export interface SuggestionAdapter { + /** + * @deprecated Since version 1.1. Will be removed. + */ + getKey(qualifier: SuggestionQualifier): string; + get(qualifier: SuggestionQualifier): Observable; +} + +export const SuggestionAdapter = 'oryx.SuggestionAdapter*'; + +declare global { + interface InjectionTokensContractMap { + [SuggestionAdapter]: SuggestionAdapter; + } +} diff --git a/libs/domain/search/src/services/revealers/products-experience-data.revealer.spec.ts b/libs/domain/search/src/services/revealers/products-experience-data.revealer.spec.ts index 4d533073d..7207b9194 100644 --- a/libs/domain/search/src/services/revealers/products-experience-data.revealer.spec.ts +++ b/libs/domain/search/src/services/revealers/products-experience-data.revealer.spec.ts @@ -2,7 +2,7 @@ import { nextFrame } from '@open-wc/testing-helpers'; import { createInjector, destroyInjector, getInjector } from '@spryker-oryx/di'; import { MessageType, postMessage } from '@spryker-oryx/experience'; import { of } from 'rxjs'; -import { SuggestionField } from '../adapter'; +import { SuggestionField } from '../adapter/spryker-glue'; import { SuggestionService } from '../suggestion'; import { ProductsExperienceDataRevealer } from './products-experience-data.revealer'; diff --git a/libs/domain/search/src/services/revealers/products-experience-data.revealer.ts b/libs/domain/search/src/services/revealers/products-experience-data.revealer.ts index 8b923af7a..b18b29e17 100644 --- a/libs/domain/search/src/services/revealers/products-experience-data.revealer.ts +++ b/libs/domain/search/src/services/revealers/products-experience-data.revealer.ts @@ -7,7 +7,7 @@ import { } from '@spryker-oryx/experience'; import { Observable, of, switchMap, tap } from 'rxjs'; import { Suggestion } from '../../models'; -import { SuggestionField } from '../adapter'; +import { SuggestionField } from '../adapter/spryker-glue'; import { SuggestionService } from '../suggestion'; /** diff --git a/libs/domain/search/src/services/revealers/suggestion-experience-data.revealer.spec.ts b/libs/domain/search/src/services/revealers/suggestion-experience-data.revealer.spec.ts index 9a746ce76..76b19cc58 100644 --- a/libs/domain/search/src/services/revealers/suggestion-experience-data.revealer.spec.ts +++ b/libs/domain/search/src/services/revealers/suggestion-experience-data.revealer.spec.ts @@ -3,7 +3,7 @@ import { createInjector, destroyInjector, getInjector } from '@spryker-oryx/di'; import { MessageType, postMessage } from '@spryker-oryx/experience'; import { RouteType } from '@spryker-oryx/router'; import { of } from 'rxjs'; -import { SuggestionField } from '../adapter'; +import { SuggestionField } from '../adapter/spryker-glue'; import { SuggestionService } from '../suggestion'; import { SuggestionExperienceDataRevealer } from './suggestion-experience-data.revealer'; diff --git a/libs/domain/search/src/services/search.providers.ts b/libs/domain/search/src/services/search.providers.ts index 643aa4b3c..08b6ae200 100644 --- a/libs/domain/search/src/services/search.providers.ts +++ b/libs/domain/search/src/services/search.providers.ts @@ -4,13 +4,14 @@ import { ExperienceDataRevealer } from '@spryker-oryx/experience'; import { provideLitRoutes } from '@spryker-oryx/router/lit'; import { featureVersion } from '@spryker-oryx/utilities'; import { facetProviders } from '../renderers'; +import { MockSuggestionAdapter } from './adapter'; import { ContentSuggestionAdapter, - DefaultSuggestionAdapter, + GlueSuggestionAdapter, SuggestionAdapter, SuggestionField, suggestionNormalizer, -} from './adapter'; +} from './adapter/spryker-glue'; import { DefaultFacetListService } from './default-facet-list.service'; import { DefaultSortingService } from './default-sorting.service'; import { FacetListService } from './facet-list.service'; @@ -35,11 +36,21 @@ import { productSuggestionRenderer, } from './suggestion'; -export const searchProviders: Provider[] = [ +export const glueSearchConnectors: Provider[] = [ { provide: SuggestionAdapter, - useClass: DefaultSuggestionAdapter, + useClass: GlueSuggestionAdapter, }, +]; + +export const mockSearchConnectors: Provider[] = [ + { + provide: SuggestionAdapter, + useClass: MockSuggestionAdapter, + }, +]; + +export const searchProviders: Provider[] = [ featureVersion >= '1.4' ? { provide: SuggestionAdapter, @@ -85,6 +96,16 @@ export const searchProviders: Provider[] = [ : provideLitRoutes({ routes: categoryRoutes })), ]; +export const glueSearchProviders: Provider[] = [ + ...glueSearchConnectors, + ...searchProviders, +]; + +export const mockSearchProviders: Provider[] = [ + ...mockSearchConnectors, + ...searchProviders, +]; + export const searchPreviewProviders: Provider[] = [ { provide: ExperienceDataRevealer, diff --git a/libs/domain/search/src/services/suggestion/default-suggestion.service.spec.ts b/libs/domain/search/src/services/suggestion/default-suggestion.service.spec.ts index a028ac714..2f33821b6 100644 --- a/libs/domain/search/src/services/suggestion/default-suggestion.service.spec.ts +++ b/libs/domain/search/src/services/suggestion/default-suggestion.service.spec.ts @@ -1,5 +1,6 @@ import { DefaultQueryService, QueryService } from '@spryker-oryx/core'; import { createInjector, destroyInjector } from '@spryker-oryx/di'; +import { RouteType } from '@spryker-oryx/router'; import { createSuggestionMock } from '@spryker-oryx/search/mocks'; import { Observable, of, switchMap, take } from 'rxjs'; import { SpyInstance } from 'vitest'; @@ -7,10 +8,9 @@ import { SuggestionQualifier } from '../../models'; import { SuggestionAdapter, SuggestionField, -} from '../adapter/suggestion.adapter'; +} from '../adapter/spryker-glue/suggestion.adapter'; import { DefaultSuggestionService } from './default-suggestion.service'; import { SuggestionService } from './suggestion.service'; -import { RouteType } from '@spryker-oryx/router'; const completion = ['test', 'test 1', 'test 2', 'any', 'any test']; diff --git a/libs/domain/search/src/services/suggestion/default-suggestion.service.ts b/libs/domain/search/src/services/suggestion/default-suggestion.service.ts index 8007a3ba4..6b80ba9e6 100644 --- a/libs/domain/search/src/services/suggestion/default-suggestion.service.ts +++ b/libs/domain/search/src/services/suggestion/default-suggestion.service.ts @@ -5,7 +5,7 @@ import { ProductsLoaded } from '@spryker-oryx/product'; import { CurrencyChanged, PriceModeChanged } from '@spryker-oryx/site'; import { merge, Observable, scan } from 'rxjs'; import { Suggestion, SuggestionQualifier } from '../../models'; -import { SuggestionAdapter } from '../adapter'; +import { SuggestionAdapter } from '../adapter/spryker-glue'; import { SuggestionService } from './suggestion.service'; export class DefaultSuggestionService implements SuggestionService { diff --git a/libs/domain/search/src/services/suggestion/renderer/default-suggestion-renderer.service.ts b/libs/domain/search/src/services/suggestion/renderer/default-suggestion-renderer.service.ts index 9ed4825df..db1385a39 100644 --- a/libs/domain/search/src/services/suggestion/renderer/default-suggestion-renderer.service.ts +++ b/libs/domain/search/src/services/suggestion/renderer/default-suggestion-renderer.service.ts @@ -2,7 +2,7 @@ import { inject } from '@spryker-oryx/di'; import { html, TemplateResult } from 'lit'; import { Observable } from 'rxjs'; import { Suggestion } from '../../../models'; -import { SuggestionField } from '../../adapter'; +import { SuggestionField } from '../../adapter/spryker-glue'; import { SuggestionService } from '../suggestion.service'; import { SuggestionRenderer, diff --git a/libs/domain/search/src/services/suggestion/renderer/suggestion-renderer.service.ts b/libs/domain/search/src/services/suggestion/renderer/suggestion-renderer.service.ts index 3b91d341e..83c5f7d5e 100644 --- a/libs/domain/search/src/services/suggestion/renderer/suggestion-renderer.service.ts +++ b/libs/domain/search/src/services/suggestion/renderer/suggestion-renderer.service.ts @@ -1,7 +1,7 @@ import { TemplateResult } from 'lit'; import { Observable } from 'rxjs'; import { Suggestion } from '../../../models'; -import { SuggestionField } from '../../adapter'; +import { SuggestionField } from '../../adapter/spryker-glue'; export const SuggestionRendererService = 'oryx.SuggestionRendererService'; export const SuggestionRenderer = 'oryx.SuggestionRenderer*'; diff --git a/libs/domain/site/src/feature.ts b/libs/domain/site/src/feature.ts index eee806f75..aecfeb1b1 100644 --- a/libs/domain/site/src/feature.ts +++ b/libs/domain/site/src/feature.ts @@ -1,6 +1,10 @@ import { AppFeature } from '@spryker-oryx/core'; import * as components from './components'; -import { siteProviders } from './services'; +import { + glueSiteProviders, + mockSiteProviders, + siteProviders, +} from './services'; export * from './components'; export const siteComponents = Object.values(components); @@ -9,3 +13,13 @@ export const siteFeature: AppFeature = { providers: siteProviders, components: siteComponents, }; + +export const glueSiteFeature: AppFeature = { + providers: glueSiteProviders, + components: siteComponents, +}; + +export const mockSiteFeature: AppFeature = { + providers: mockSiteProviders, + components: siteComponents, +}; diff --git a/libs/domain/site/src/services/adapter/index.ts b/libs/domain/site/src/services/adapter/index.ts index 6cffbb8dc..75f2d6239 100644 --- a/libs/domain/site/src/services/adapter/index.ts +++ b/libs/domain/site/src/services/adapter/index.ts @@ -1,3 +1,2 @@ -export * from './default-store.adapter'; -export * from './normalizers'; -export * from './store.adapter'; +export * from './mock'; +export * from './spryker-glue'; diff --git a/libs/domain/site/src/services/adapter/mock/index.ts b/libs/domain/site/src/services/adapter/mock/index.ts new file mode 100644 index 000000000..7c997261c --- /dev/null +++ b/libs/domain/site/src/services/adapter/mock/index.ts @@ -0,0 +1,2 @@ +export * from './mock-store'; +export * from './mock-store.adapter'; diff --git a/libs/domain/site/src/services/adapter/mock/mock-store.adapter.ts b/libs/domain/site/src/services/adapter/mock/mock-store.adapter.ts new file mode 100644 index 000000000..def396bd1 --- /dev/null +++ b/libs/domain/site/src/services/adapter/mock/mock-store.adapter.ts @@ -0,0 +1,9 @@ +import { Store, StoreAdapter } from '@spryker-oryx/site'; +import { Observable, of } from 'rxjs'; +import { MockStore } from './mock-store'; + +export class MockStoreAdapter implements StoreAdapter { + get(): Observable { + return of([MockStore]); + } +} diff --git a/libs/domain/site/src/services/adapter/mock/mock-store.ts b/libs/domain/site/src/services/adapter/mock/mock-store.ts new file mode 100644 index 000000000..f4dbf154d --- /dev/null +++ b/libs/domain/site/src/services/adapter/mock/mock-store.ts @@ -0,0 +1,47 @@ +import { Locale } from '@spryker-oryx/i18n'; +import { Country, Currency, Store } from '@spryker-oryx/site'; + +export const Country1: Country = { + iso2Code: 'DE', + iso3Code: 'DEU', + name: 'Germany', + postalCodeMandatory: true, + postalCodeRegex: 'd{5}', +}; + +export const Country2: Country = { + iso2Code: 'AT', + iso3Code: 'AUT', + name: 'Austria', + postalCodeMandatory: true, + postalCodeRegex: '\\d{4}', +}; + +export const MockCurrency1: Currency = { + code: 'EUR', + name: 'Euro', +}; + +export const MockCurrency2: Currency = { + code: 'CHF', + name: 'Swiss Franc', +}; + +export const MockLocale1: Locale = { + code: 'en', + name: 'en_US', +}; + +export const MockLocale2: Locale = { + code: 'de', + name: 'de_DE', +}; + +export const MockStore: Store = { + id: 'DE', + countries: [Country1, Country2], + currencies: [MockCurrency1, MockCurrency2], + defaultCurrency: MockCurrency1.code, + locales: [MockLocale1, MockLocale2], + timeZone: 'Europe/Berlin', +}; diff --git a/libs/domain/site/src/services/adapter/spryker-glue/glue-store.adapter.ts b/libs/domain/site/src/services/adapter/spryker-glue/glue-store.adapter.ts new file mode 100644 index 000000000..103f2928f --- /dev/null +++ b/libs/domain/site/src/services/adapter/spryker-glue/glue-store.adapter.ts @@ -0,0 +1,20 @@ +import { HttpService, JsonAPITransformerService } from '@spryker-oryx/core'; +import { inject } from '@spryker-oryx/di'; +import { Observable } from 'rxjs'; +import { Store } from '../../../models'; +import { StoreNormalizer } from './normalizers'; +import { StoreAdapter } from './store.adapter'; + +export class GlueStoreAdapter implements StoreAdapter { + constructor( + protected httpService = inject(HttpService), + protected SCOS_BASE_URL = inject('SCOS_BASE_URL'), + protected transformer = inject(JsonAPITransformerService) + ) {} + + get(): Observable { + return this.httpService + .get(`${this.SCOS_BASE_URL}/stores`) + .pipe(this.transformer.do(StoreNormalizer)); + } +} diff --git a/libs/domain/site/src/services/adapter/spryker-glue/glue.adapter.spec.ts b/libs/domain/site/src/services/adapter/spryker-glue/glue.adapter.spec.ts new file mode 100644 index 000000000..afcc8c18d --- /dev/null +++ b/libs/domain/site/src/services/adapter/spryker-glue/glue.adapter.spec.ts @@ -0,0 +1,78 @@ +import { HttpService, JsonAPITransformerService } from '@spryker-oryx/core'; +import { HttpTestService } from '@spryker-oryx/core/testing'; +import { createInjector, destroyInjector } from '@spryker-oryx/di'; +import { of } from 'rxjs'; +import { GlueStoreAdapter } from './default-store.adapter'; +import { StoreNormalizer } from './normalizers'; +import { StoreAdapter } from './store.adapter'; + +const mockApiUrl = 'mockApiUrl'; + +const mockTransformer = { + transform: vi.fn().mockReturnValue(of(null)), + do: vi.fn().mockReturnValue(() => of(null)), +}; + +describe('GlueStoreAdapter', () => { + let service: StoreAdapter; + let http: HttpTestService; + + beforeEach(() => { + const testInjector = createInjector({ + providers: [ + { + provide: HttpService, + useClass: HttpTestService, + }, + { + provide: StoreAdapter, + useClass: GlueStoreAdapter, + }, + { + provide: 'SCOS_BASE_URL', + useValue: mockApiUrl, + }, + { + provide: JsonAPITransformerService, + useValue: mockTransformer, + }, + ], + }); + + service = testInjector.inject(StoreAdapter); + http = testInjector.inject(HttpService) as HttpTestService; + }); + + afterEach(() => { + vi.clearAllMocks(); + destroyInjector(); + }); + + it('should be provided', () => { + expect(service).toBeInstanceOf(GlueStoreAdapter); + }); + + describe('get should send `get` request', () => { + it('should build url', () => { + service.get().subscribe(); + + expect(http.url).toBe(`${mockApiUrl}/stores`); + }); + + it('should call transformer data with data from response', () => { + service.get().subscribe(); + + expect(mockTransformer.do).toHaveBeenCalledWith(StoreNormalizer); + }); + + it('should return transformed data', () => { + const mockTransformerData = 'mockTransformerData'; + const callback = vi.fn(); + mockTransformer.do.mockReturnValue(() => of(mockTransformerData)); + + service.get().subscribe(callback); + + expect(callback).toHaveBeenCalledWith(mockTransformerData); + }); + }); +}); diff --git a/libs/domain/site/src/services/adapter/spryker-glue/index.ts b/libs/domain/site/src/services/adapter/spryker-glue/index.ts new file mode 100644 index 000000000..96dc90cb8 --- /dev/null +++ b/libs/domain/site/src/services/adapter/spryker-glue/index.ts @@ -0,0 +1,3 @@ +export * from './glue-store.adapter'; +export * from './normalizers'; +export * from './store.adapter'; diff --git a/libs/domain/site/src/services/adapter/spryker-glue/normalizers/index.ts b/libs/domain/site/src/services/adapter/spryker-glue/normalizers/index.ts new file mode 100644 index 000000000..5025974c8 --- /dev/null +++ b/libs/domain/site/src/services/adapter/spryker-glue/normalizers/index.ts @@ -0,0 +1,2 @@ +export * from './model'; +export * from './store'; diff --git a/libs/domain/site/src/services/adapter/spryker-glue/normalizers/model.ts b/libs/domain/site/src/services/adapter/spryker-glue/normalizers/model.ts new file mode 100644 index 000000000..fa1dfa742 --- /dev/null +++ b/libs/domain/site/src/services/adapter/spryker-glue/normalizers/model.ts @@ -0,0 +1,3 @@ +import { ApiStoreModel } from '../../../../models'; + +export type DeserializedStores = ApiStoreModel.Attributes[]; diff --git a/libs/domain/site/src/services/adapter/spryker-glue/normalizers/store/index.ts b/libs/domain/site/src/services/adapter/spryker-glue/normalizers/store/index.ts new file mode 100644 index 000000000..8bc628c3a --- /dev/null +++ b/libs/domain/site/src/services/adapter/spryker-glue/normalizers/store/index.ts @@ -0,0 +1 @@ +export * from './store.normalizer'; diff --git a/libs/domain/site/src/services/adapter/spryker-glue/normalizers/store/store.normalizer.ts b/libs/domain/site/src/services/adapter/spryker-glue/normalizers/store/store.normalizer.ts new file mode 100644 index 000000000..796246aa2 --- /dev/null +++ b/libs/domain/site/src/services/adapter/spryker-glue/normalizers/store/store.normalizer.ts @@ -0,0 +1,34 @@ +import { Transformer } from '@spryker-oryx/core'; +import { Provider } from '@spryker-oryx/di'; +import { Store } from '../../../../../models'; +import { DeserializedStores } from '../model'; + +export const StoreNormalizer = 'oryx.StoreNormalizer*'; + +export function storeAttributesNormalizer(data: DeserializedStores): Store[] { + // TODO: drop this when the backend is healthy again; current dynamic + // multi-store backend is exposing numbers rather than the locale isocode. + data.map((store) => + store.locales.map((locale) => { + if (!isNaN(Number(locale.code))) { + locale.code = locale.name.split(/_|-/)?.[0]; + } + return locale; + }) + ); + + return data; +} + +export const storeNormalizer: Provider[] = [ + { + provide: StoreNormalizer, + useValue: storeAttributesNormalizer, + }, +]; + +declare global { + interface InjectionTokensContractMap { + [StoreNormalizer]: Transformer[]; + } +} diff --git a/libs/domain/site/src/services/adapter/spryker-glue/store.adapter.ts b/libs/domain/site/src/services/adapter/spryker-glue/store.adapter.ts new file mode 100644 index 000000000..8abe67b67 --- /dev/null +++ b/libs/domain/site/src/services/adapter/spryker-glue/store.adapter.ts @@ -0,0 +1,14 @@ +import { Observable } from 'rxjs'; +import { Store } from '../../../models'; + +export interface StoreAdapter { + get: () => Observable; +} + +export const StoreAdapter = 'oryx.StoreAdapter'; + +declare global { + interface InjectionTokensContractMap { + [StoreAdapter]: StoreAdapter; + } +} diff --git a/libs/domain/site/src/services/site.providers.ts b/libs/domain/site/src/services/site.providers.ts index aa135e516..6d49dba60 100644 --- a/libs/domain/site/src/services/site.providers.ts +++ b/libs/domain/site/src/services/site.providers.ts @@ -3,7 +3,12 @@ import { Provider } from '@spryker-oryx/di'; import { LocaleAdapter } from '@spryker-oryx/i18n'; import { featureVersion } from '@spryker-oryx/utilities'; import { PriceModes } from '../models'; -import { DefaultStoreAdapter, StoreAdapter, storeNormalizer } from './adapter'; +import { MockStoreAdapter } from './adapter'; +import { + GlueStoreAdapter, + StoreAdapter, + storeNormalizer, +} from './adapter/spryker-glue'; import { BreadcrumbService, DefaultBreadcrumbService } from './breadcrumb'; import { CountryService, DefaultCountryService } from './country'; import { @@ -48,6 +53,21 @@ declare global { } } +export const glueSiteConnectors: Provider[] = [ + { + provide: StoreAdapter, + useClass: GlueStoreAdapter, + }, + ...storeNormalizer, +]; + +export const mockSiteConnectors: Provider[] = [ + { + provide: StoreAdapter, + useClass: MockStoreAdapter, + }, +]; + export const siteProviders: Provider[] = [ { provide: 'SCOS_BASE_URL', @@ -72,10 +92,6 @@ export const siteProviders: Provider[] = [ useClass: DefaultStoreService, }, - { - provide: StoreAdapter, - useClass: DefaultStoreAdapter, - }, { provide: CountryService, useClass: DefaultCountryService, @@ -112,7 +128,6 @@ export const siteProviders: Provider[] = [ provide: GenderService, useClass: DefaultGenderService, }, - ...storeNormalizer, { provide: HttpInterceptor, useClass: AcceptLanguageInterceptor, @@ -146,3 +161,13 @@ export const siteProviders: Provider[] = [ // useClass: StoreInterceptor, // }, ]; + +export const glueSiteProviders: Provider[] = [ + ...glueSiteConnectors, + ...siteProviders, +]; + +export const mockSiteProviders: Provider[] = [ + ...mockSiteConnectors, + ...siteProviders, +]; diff --git a/libs/domain/site/src/services/store/default-store.service.spec.ts b/libs/domain/site/src/services/store/default-store.service.spec.ts index 37d73e7f8..40732ffa7 100644 --- a/libs/domain/site/src/services/store/default-store.service.spec.ts +++ b/libs/domain/site/src/services/store/default-store.service.spec.ts @@ -1,6 +1,6 @@ import { createInjector, destroyInjector } from '@spryker-oryx/di'; import { of } from 'rxjs'; -import { StoreAdapter } from '../adapter'; +import { StoreAdapter } from '../adapter/spryker-glue'; import { DefaultStoreService } from './default-store.service'; import { StoreService } from './store.service'; diff --git a/libs/domain/site/src/services/store/default-store.service.ts b/libs/domain/site/src/services/store/default-store.service.ts index 33feeda15..fa3265aca 100644 --- a/libs/domain/site/src/services/store/default-store.service.ts +++ b/libs/domain/site/src/services/store/default-store.service.ts @@ -8,7 +8,7 @@ import { shareReplay, } from 'rxjs'; import { Store } from '../../models'; -import { StoreAdapter } from '../adapter'; +import { StoreAdapter } from '../adapter/spryker-glue'; import { StoreService } from './store.service'; export class DefaultStoreService implements StoreService { diff --git a/libs/template/presets/storefront/app.ts b/libs/template/presets/storefront/app.ts index 70195351b..e619497f0 100644 --- a/libs/template/presets/storefront/app.ts +++ b/libs/template/presets/storefront/app.ts @@ -3,7 +3,11 @@ import { applicationFeature, } from '@spryker-oryx/application'; import { SapiAuthComponentsFeature, SapiAuthFeature } from '@spryker-oryx/auth'; -import { cartFeature } from '@spryker-oryx/cart'; +import { + cartFeature, + glueCartFeature, + mockCartFeature, +} from '@spryker-oryx/cart'; import { checkoutFeature } from '@spryker-oryx/checkout'; import { contentFeature } from '@spryker-oryx/content'; import { AppFeature, coreFeature } from '@spryker-oryx/core'; @@ -17,15 +21,28 @@ import { import { formFeature } from '@spryker-oryx/form'; import { I18nFeature } from '@spryker-oryx/i18n'; import { orderFeature } from '@spryker-oryx/order'; -import { productFeature } from '@spryker-oryx/product'; +import { + glueProductFeature, + mockProductFeature, + productFeature, +} from '@spryker-oryx/product'; import { brandGraphics, commonGraphics, materialDesignLink, } from '@spryker-oryx/resources'; import { RouterFeature } from '@spryker-oryx/router'; -import { searchFeature, searchPreviewProviders } from '@spryker-oryx/search'; -import { siteFeature } from '@spryker-oryx/site'; +import { + glueSearchFeature, + mockSearchFeature, + searchFeature, + searchPreviewProviders, +} from '@spryker-oryx/search'; +import { + glueSiteFeature, + mockSiteFeature, + siteFeature, +} from '@spryker-oryx/site'; import { uiFeature } from '@spryker-oryx/ui'; import { userFeature } from '@spryker-oryx/user'; import { featureVersion } from '@spryker-oryx/utilities'; @@ -80,3 +97,19 @@ export const storefrontFeatures: AppFeature[] = [ }, StaticExperienceFeature, ]; + +export const storefrontMockFeatures = [ + ...storefrontFeatures, + mockCartFeature, + mockSiteFeature, + mockProductFeature, + mockSearchFeature, +]; + +export const storefrontGlueFeatures = [ + ...storefrontFeatures, + glueCartFeature, + glueSiteFeature, + glueProductFeature, + glueSearchFeature, +]; From f15b583af980fa93be7d48aadd453663dc0b6f21 Mon Sep 17 00:00:00 2001 From: Alexander Kovalenko Date: Mon, 10 Jun 2024 19:04:28 +0300 Subject: [PATCH 2/9] Fixed TS error --- apps/storefront/dependencies.sh | 2 +- apps/storybook/dependencies.sh | 2 +- .../src/services/adapter/mock/mock-product.ts | 17 +++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/storefront/dependencies.sh b/apps/storefront/dependencies.sh index 0a49cdab9..52202c3cc 100644 --- a/apps/storefront/dependencies.sh +++ b/apps/storefront/dependencies.sh @@ -1 +1 @@ -DEPENDENCIES="apps/storefront/* libs/base/di/* libs/base/ui/* libs/base/utilities/* libs/domain/cart/* libs/domain/checkout/* libs/domain/content/* libs/domain/merchant/* libs/domain/order/* libs/domain/product/* libs/domain/search/* libs/domain/site/* libs/domain/user/* libs/platform/auth/* libs/platform/core/* libs/platform/experience/* libs/platform/form/* libs/platform/i18n/* libs/platform/indexed-db/* libs/platform/offline/* libs/platform/push-notification/* libs/platform/router/* libs/template/application/* libs/template/labs/* libs/template/presets/* libs/template/resources/* libs/template/themes/*" +DEPENDENCIES="apps/storefront/* libs/base/di/* libs/base/ui/* libs/base/utilities/* libs/domain/cart/* libs/domain/checkout/* libs/domain/content/* libs/domain/merchant/* libs/domain/order/* libs/domain/product/* libs/domain/search/* libs/domain/site/* libs/domain/user/* libs/platform/auth/* libs/platform/core/* libs/platform/experience/* libs/platform/form/* libs/platform/i18n/* libs/platform/router/* libs/template/application/* libs/template/labs/* libs/template/presets/* libs/template/resources/* libs/template/themes/*" \ No newline at end of file diff --git a/apps/storybook/dependencies.sh b/apps/storybook/dependencies.sh index 4aab638df..751527c52 100644 --- a/apps/storybook/dependencies.sh +++ b/apps/storybook/dependencies.sh @@ -1 +1 @@ -DEPENDENCIES="apps/storybook/* libs/base/di/* libs/base/ui/* libs/base/utilities/* libs/domain/cart/* libs/domain/checkout/* libs/domain/content/* libs/domain/merchant/* libs/domain/order/* libs/domain/product/* libs/domain/search/* libs/domain/site/* libs/domain/user/* libs/platform/auth/* libs/platform/core/* libs/platform/experience/* libs/platform/form/* libs/platform/i18n/* libs/platform/indexed-db/* libs/platform/offline/* libs/platform/push-notification/* libs/platform/router/* libs/template/application/* libs/template/presets/* libs/template/resources/* libs/template/themes/*" +DEPENDENCIES="apps/storybook/* libs/base/ui/* libs/domain/content/*" \ No newline at end of file diff --git a/libs/domain/product/src/services/adapter/mock/mock-product.ts b/libs/domain/product/src/services/adapter/mock/mock-product.ts index 638b72f6b..2275bda58 100644 --- a/libs/domain/product/src/services/adapter/mock/mock-product.ts +++ b/libs/domain/product/src/services/adapter/mock/mock-product.ts @@ -1,14 +1,15 @@ -import { Product, ProductLabel, ProductMediaSet } from '@spryker-oryx/product'; - -export const enum ProductLabelAppearance { - Highlight = 'error', - Info = 'info', -} +import { + Product, + ProductLabel, + ProductLabelAppearance, + ProductMediaSet, +} from '@spryker-oryx/product'; const img1 = { sm: 'https://images.icecat.biz/img/gallery_mediums/29885545_9575.jpg', lg: 'https://images.icecat.biz/img/gallery/29885545_9575.jpg', }; + const img2 = { sm: 'https://images.icecat.biz/img/norm/medium/26138343-5454.jpg', lg: 'https://images.icecat.biz/img/norm/high/26138343-5454.jpg', @@ -49,12 +50,12 @@ const mediaSet: ProductMediaSet[] = [ const newLabel: ProductLabel = { name: 'New', - appearance: ProductLabelAppearance.Highlight, + appearance: 'error' as ProductLabelAppearance, }; const saleLabel: ProductLabel = { name: 'sale', - appearance: ProductLabelAppearance.Info, + appearance: 'info' as ProductLabelAppearance, }; export const mockProducts: Product[] = [ From 186f0fdf183e29d2a75564f0580790b8c3cb4fb2 Mon Sep 17 00:00:00 2001 From: Alexander Kovalenko Date: Mon, 10 Jun 2024 19:46:18 +0300 Subject: [PATCH 3/9] Fixed TS error. --- .../product-relations/mock-product-relations-list.adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.adapter.ts b/libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.adapter.ts index 7f98cdea3..d7d56c391 100644 --- a/libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.adapter.ts +++ b/libs/domain/product/src/services/adapter/mock/product-relations/mock-product-relations-list.adapter.ts @@ -9,7 +9,7 @@ import { mockProducts } from '../mock-product'; export class MockProductRelationsListAdapter implements ProductRelationsListAdapter { - get({ sku }: ProductQualifier): Observable { + get({ sku }: ProductQualifier): Observable { return of([ mockProducts[Number(sku) - 1], mockProducts[Number(sku)], From 145662d6b1b07bb760434bbfe199850ce0bdf428 Mon Sep 17 00:00:00 2001 From: Alexander Kovalenko Date: Tue, 11 Jun 2024 10:25:17 +0300 Subject: [PATCH 4/9] Temp fix --- .../src/adapter/mock/mock-checkout.adapter.ts | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts index 4893a82d4..94388a37e 100644 --- a/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts +++ b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts @@ -1,4 +1,3 @@ -import { mockDefaultCart } from '@spryker-oryx/cart/mocks'; import { CheckoutAdapter, CheckoutData, @@ -6,28 +5,15 @@ import { PlaceOrderData, } from '@spryker-oryx/checkout'; import { Observable, of } from 'rxjs'; +import {mockCheckout, mockPlaceOrderResponse} from "./mock-checkout"; export class MockCheckoutAdapter implements CheckoutAdapter { get(props: PlaceOrderData): Observable { - const checkoutData = { - addresses: [props.billingAddress, props.shippingAddress], - paymentProviders: 1, - selectedShipmentMethods: 1, - selectedPaymentMethods: 1, - paymentMethods: [...props.payments], - shipments: [...props.shipments], - carriers: 1, - shipment: props.shipment, - carts: { - id: props.cartId, - ...mockDefaultCart, - }, - }; - - return of(checkoutData); + return of(mockCheckout as CheckoutData); } placeOrder(data: PlaceOrderData): Observable { - return undefined; + return of(mockPlaceOrderResponse as CheckoutResponse); + ); } } From df034095d8958ab7648d492e0c0c6cf8de193b09 Mon Sep 17 00:00:00 2001 From: Alexander Kovalenko Date: Tue, 11 Jun 2024 10:34:56 +0300 Subject: [PATCH 5/9] Temp fix --- .../checkout/services/src/adapter/mock/mock-checkout.adapter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts index 94388a37e..44a29f10a 100644 --- a/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts +++ b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts @@ -14,6 +14,5 @@ export class MockCheckoutAdapter implements CheckoutAdapter { placeOrder(data: PlaceOrderData): Observable { return of(mockPlaceOrderResponse as CheckoutResponse); - ); } } From 9f2692ef1d8f558e33c397884684e430a5662cea Mon Sep 17 00:00:00 2001 From: Alexander Kovalenko Date: Tue, 11 Jun 2024 11:01:46 +0300 Subject: [PATCH 6/9] Temp fix --- .../services/src/adapter/mock/index.ts | 1 - .../src/adapter/mock/mock-checkout.adapter.ts | 36 +++++++++---------- .../src/adapter/mock/mock-checkout.ts | 2 +- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/libs/domain/checkout/services/src/adapter/mock/index.ts b/libs/domain/checkout/services/src/adapter/mock/index.ts index adf0c0b83..003b3db26 100644 --- a/libs/domain/checkout/services/src/adapter/mock/index.ts +++ b/libs/domain/checkout/services/src/adapter/mock/index.ts @@ -1,2 +1 @@ export * from './mock-checkout'; -export * from './mock-checkout.adapter'; diff --git a/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts index 44a29f10a..df4e54e53 100644 --- a/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts +++ b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.adapter.ts @@ -1,18 +1,18 @@ -import { - CheckoutAdapter, - CheckoutData, - CheckoutResponse, - PlaceOrderData, -} from '@spryker-oryx/checkout'; -import { Observable, of } from 'rxjs'; -import {mockCheckout, mockPlaceOrderResponse} from "./mock-checkout"; - -export class MockCheckoutAdapter implements CheckoutAdapter { - get(props: PlaceOrderData): Observable { - return of(mockCheckout as CheckoutData); - } - - placeOrder(data: PlaceOrderData): Observable { - return of(mockPlaceOrderResponse as CheckoutResponse); - } -} +// import { +// CheckoutAdapter, +// CheckoutData, +// CheckoutResponse, +// PlaceOrderData, +// } from '@spryker-oryx/checkout'; +// import { Observable, of } from 'rxjs'; +// import {mockCheckout, mockPlaceOrderResponse} from "./mock-checkout"; +// +// export class MockCheckoutAdapter implements CheckoutAdapter { +// get(props: PlaceOrderData): Observable { +// return of(mockCheckout as CheckoutData); +// } +// +// placeOrder(data: PlaceOrderData): Observable { +// return of(mockPlaceOrderResponse as CheckoutResponse); +// } +// } diff --git a/libs/domain/checkout/services/src/adapter/mock/mock-checkout.ts b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.ts index 3f0b487a2..3381aa8d0 100644 --- a/libs/domain/checkout/services/src/adapter/mock/mock-checkout.ts +++ b/libs/domain/checkout/services/src/adapter/mock/mock-checkout.ts @@ -1,4 +1,4 @@ -import { PlaceOrderData } from '../../models'; +import { PlaceOrderData } from '@spryker-oryx/checkout'; export const mockSelectedShipmentMethod = { selectedShipmentMethod: { From cc86a77c53a36e0649d39a7bf555c9b7ec7fdcf5 Mon Sep 17 00:00:00 2001 From: Alexander Kovalenko Date: Tue, 11 Jun 2024 11:04:26 +0300 Subject: [PATCH 7/9] Temp fix --- apps/storefront/dependencies.sh | 2 +- apps/storybook/dependencies.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/storefront/dependencies.sh b/apps/storefront/dependencies.sh index 52202c3cc..c4ac2253d 100644 --- a/apps/storefront/dependencies.sh +++ b/apps/storefront/dependencies.sh @@ -1 +1 @@ -DEPENDENCIES="apps/storefront/* libs/base/di/* libs/base/ui/* libs/base/utilities/* libs/domain/cart/* libs/domain/checkout/* libs/domain/content/* libs/domain/merchant/* libs/domain/order/* libs/domain/product/* libs/domain/search/* libs/domain/site/* libs/domain/user/* libs/platform/auth/* libs/platform/core/* libs/platform/experience/* libs/platform/form/* libs/platform/i18n/* libs/platform/router/* libs/template/application/* libs/template/labs/* libs/template/presets/* libs/template/resources/* libs/template/themes/*" \ No newline at end of file +DEPENDENCIES="apps/storefront/* libs/base/di/* libs/base/ui/* libs/base/utilities/* libs/domain/cart/* libs/domain/checkout/* libs/domain/content/* libs/domain/merchant/* libs/domain/order/* libs/domain/product/* libs/domain/search/* libs/domain/site/* libs/domain/user/* libs/platform/auth/* libs/platform/core/* libs/platform/experience/* libs/platform/form/* libs/platform/i18n/* libs/platform/indexed-db/* libs/platform/offline/* libs/platform/push-notification/* libs/platform/router/* libs/template/application/* libs/template/labs/* libs/template/presets/* libs/template/resources/* libs/template/themes/*" \ No newline at end of file diff --git a/apps/storybook/dependencies.sh b/apps/storybook/dependencies.sh index 751527c52..776aac632 100644 --- a/apps/storybook/dependencies.sh +++ b/apps/storybook/dependencies.sh @@ -1 +1 @@ -DEPENDENCIES="apps/storybook/* libs/base/ui/* libs/domain/content/*" \ No newline at end of file +DEPENDENCIES="apps/storybook/* libs/base/di/* libs/base/ui/* libs/base/utilities/* libs/domain/cart/* libs/domain/checkout/* libs/domain/content/* libs/domain/merchant/* libs/domain/order/* libs/domain/product/* libs/domain/search/* libs/domain/site/* libs/domain/user/* libs/platform/auth/* libs/platform/core/* libs/platform/experience/* libs/platform/form/* libs/platform/i18n/* libs/platform/indexed-db/* libs/platform/offline/* libs/platform/push-notification/* libs/platform/router/* libs/template/application/* libs/template/presets/* libs/template/resources/* libs/template/themes/*" \ No newline at end of file From fe566d33ca4242fb1fb673647f1308ed695e6cde Mon Sep 17 00:00:00 2001 From: Alexander Kovalenko Date: Tue, 11 Jun 2024 18:35:53 +0300 Subject: [PATCH 8/9] Temp fix --- .../site/src/services/adapter/spryker-glue/glue.adapter.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/domain/site/src/services/adapter/spryker-glue/glue.adapter.spec.ts b/libs/domain/site/src/services/adapter/spryker-glue/glue.adapter.spec.ts index afcc8c18d..06aae3885 100644 --- a/libs/domain/site/src/services/adapter/spryker-glue/glue.adapter.spec.ts +++ b/libs/domain/site/src/services/adapter/spryker-glue/glue.adapter.spec.ts @@ -2,7 +2,7 @@ import { HttpService, JsonAPITransformerService } from '@spryker-oryx/core'; import { HttpTestService } from '@spryker-oryx/core/testing'; import { createInjector, destroyInjector } from '@spryker-oryx/di'; import { of } from 'rxjs'; -import { GlueStoreAdapter } from './default-store.adapter'; +import { GlueStoreAdapter } from './glue-store.adapter'; import { StoreNormalizer } from './normalizers'; import { StoreAdapter } from './store.adapter'; From a7ea03d13021006a439e9bdf33ac07e3051ed442 Mon Sep 17 00:00:00 2001 From: Alexander Kovalenko Date: Wed, 12 Jun 2024 09:37:10 +0300 Subject: [PATCH 9/9] Temp fix --- .../adapter/spryker-glue/glue-product.adapter.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.spec.ts b/libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.spec.ts index ea4617cf3..a4a7ff276 100644 --- a/libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.spec.ts +++ b/libs/domain/product/src/services/adapter/spryker-glue/glue-product.adapter.spec.ts @@ -5,6 +5,7 @@ import { } from '@spryker-oryx/core'; import { HttpTestService } from '@spryker-oryx/core/testing'; import { createInjector, destroyInjector } from '@spryker-oryx/di'; +import { GlueProductAdapter } from '@spryker-oryx/product'; import { featureVersion } from '@spryker-oryx/utilities'; import { of } from 'rxjs'; import { ApiProductModel } from '../../../models'; @@ -44,7 +45,7 @@ describe('GlueProductService', () => { }, { provide: ProductAdapter, - useClass: DefaultProductAdapter, + useClass: GlueProductAdapter, }, { provide: 'SCOS_BASE_URL', @@ -70,7 +71,7 @@ describe('GlueProductService', () => { }); it('should be provided', () => { - expect(service).toBeInstanceOf(DefaultProductAdapter); + expect(service).toBeInstanceOf(GlueProductAdapter); }); describe('get', () => {