diff --git a/.env-cmdrc b/.env-cmdrc index 2462eb6c36c..73bb133bc45 100644 --- a/.env-cmdrc +++ b/.env-cmdrc @@ -81,5 +81,10 @@ "opps":{ "CX_BASE_URL": "https://api.cg79x9wuu9-eccommerc1-s5-public.model-t.myhybris.cloud", "CX_OPPS": "true" + }, + "s4-service":{ + "CX_BASE_URL": "https://api.cg79x9wuu9-eccommerc1-s8-public.model-t.myhybris.cloud", + "CX_S4_SERVICE": "true", + "CX_B2B": "true" } } diff --git a/core-libs/setup/tsconfig.spec.json b/core-libs/setup/tsconfig.spec.json index d03322f5038..1e7adccf9a9 100644 --- a/core-libs/setup/tsconfig.spec.json +++ b/core-libs/setup/tsconfig.spec.json @@ -577,6 +577,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/extra-webpack.config.js b/extra-webpack.config.js index f82092a56c7..c6f0f66837c 100644 --- a/extra-webpack.config.js +++ b/extra-webpack.config.js @@ -71,6 +71,7 @@ module.exports = { 'feature-libs/pickup-in-store' ), '@spartacus/s4om': path.join(__dirname, 'integration-libs/s4om'), + '@spartacus/s4-service': path.join(__dirname, 'integration-libs/s4-service'), }, }, }; diff --git a/feature-libs/asm/tsconfig.schematics.json b/feature-libs/asm/tsconfig.schematics.json index d1b6df92f88..a9e1a8dbc30 100644 --- a/feature-libs/asm/tsconfig.schematics.json +++ b/feature-libs/asm/tsconfig.schematics.json @@ -574,6 +574,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/cart/tsconfig.schematics.json b/feature-libs/cart/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/feature-libs/cart/tsconfig.schematics.json +++ b/feature-libs/cart/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/checkout/tsconfig.schematics.json b/feature-libs/checkout/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/feature-libs/checkout/tsconfig.schematics.json +++ b/feature-libs/checkout/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/customer-ticketing/tsconfig.schematics.json b/feature-libs/customer-ticketing/tsconfig.schematics.json index e5fb75aa73e..2e09eb491ef 100644 --- a/feature-libs/customer-ticketing/tsconfig.schematics.json +++ b/feature-libs/customer-ticketing/tsconfig.schematics.json @@ -597,6 +597,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/estimated-delivery-date/tsconfig.schematics.json b/feature-libs/estimated-delivery-date/tsconfig.schematics.json index e5fb75aa73e..2e09eb491ef 100644 --- a/feature-libs/estimated-delivery-date/tsconfig.schematics.json +++ b/feature-libs/estimated-delivery-date/tsconfig.schematics.json @@ -597,6 +597,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/order/components/order-details/order-overview/order-overview.component.html b/feature-libs/order/components/order-details/order-overview/order-overview.component.html index 09ae46932fc..bb719197824 100644 --- a/feature-libs/order/components/order-details/order-overview/order-overview.component.html +++ b/feature-libs/order/components/order-details/order-overview/order-overview.component.html @@ -105,6 +105,12 @@ [content]="getDeliveryModeCardContent(order?.deliveryMode) | async" > + + + diff --git a/feature-libs/order/components/order-details/order-overview/order-overview.component.ts b/feature-libs/order/components/order-details/order-overview/order-overview.component.ts index 0d015aaf09d..b95f507e920 100644 --- a/feature-libs/order/components/order-details/order-overview/order-overview.component.ts +++ b/feature-libs/order/components/order-details/order-overview/order-overview.component.ts @@ -17,7 +17,7 @@ import { Card, CmsComponentData } from '@spartacus/storefront'; import { Observable, combineLatest, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { OrderDetailsService } from '../order-details.service'; -import { paymentMethodCard } from '@spartacus/order/root'; +import { OrderOutlets, paymentMethodCard } from '@spartacus/order/root'; @Component({ selector: 'cx-order-overview', @@ -26,6 +26,7 @@ import { paymentMethodCard } from '@spartacus/order/root'; }) export class OrderOverviewComponent { readonly cartOutlets = CartOutlets; + readonly orderOutlets = OrderOutlets; order$: Observable = this.orderDetailsService.getOrderDetails(); isOrderLoading$: Observable = diff --git a/feature-libs/order/root/model/order-outlets.model.ts b/feature-libs/order/root/model/order-outlets.model.ts index a953eb544fe..4e30a9edea3 100644 --- a/feature-libs/order/root/model/order-outlets.model.ts +++ b/feature-libs/order/root/model/order-outlets.model.ts @@ -10,4 +10,5 @@ export enum OrderOutlets { ORDER_CONSIGNMENT = 'cx-order-consignment', CONSIGNMENT_DELIVERY_INFO = 'cx-order-consignment-delivery-info', + SERVICE_DETAILS = 'cx-service-details-card', } diff --git a/feature-libs/order/tsconfig.schematics.json b/feature-libs/order/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/feature-libs/order/tsconfig.schematics.json +++ b/feature-libs/order/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/organization/tsconfig.schematics.json b/feature-libs/organization/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/feature-libs/organization/tsconfig.schematics.json +++ b/feature-libs/organization/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/pdf-invoices/tsconfig.schematics.json b/feature-libs/pdf-invoices/tsconfig.schematics.json index e5fb75aa73e..2e09eb491ef 100644 --- a/feature-libs/pdf-invoices/tsconfig.schematics.json +++ b/feature-libs/pdf-invoices/tsconfig.schematics.json @@ -597,6 +597,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/pickup-in-store/tsconfig.schematics.json b/feature-libs/pickup-in-store/tsconfig.schematics.json index 7c834d8c898..cc1e55926eb 100644 --- a/feature-libs/pickup-in-store/tsconfig.schematics.json +++ b/feature-libs/pickup-in-store/tsconfig.schematics.json @@ -600,6 +600,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/product-configurator/tsconfig.schematics.json b/feature-libs/product-configurator/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/feature-libs/product-configurator/tsconfig.schematics.json +++ b/feature-libs/product-configurator/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/product/tsconfig.schematics.json b/feature-libs/product/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/feature-libs/product/tsconfig.schematics.json +++ b/feature-libs/product/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/qualtrics/tsconfig.schematics.json b/feature-libs/qualtrics/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/feature-libs/qualtrics/tsconfig.schematics.json +++ b/feature-libs/qualtrics/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/quote/tsconfig.schematics.json b/feature-libs/quote/tsconfig.schematics.json index 7c834d8c898..cc1e55926eb 100644 --- a/feature-libs/quote/tsconfig.schematics.json +++ b/feature-libs/quote/tsconfig.schematics.json @@ -600,6 +600,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/requested-delivery-date/tsconfig.schematics.json b/feature-libs/requested-delivery-date/tsconfig.schematics.json index e5fb75aa73e..2e09eb491ef 100644 --- a/feature-libs/requested-delivery-date/tsconfig.schematics.json +++ b/feature-libs/requested-delivery-date/tsconfig.schematics.json @@ -597,6 +597,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/smartedit/tsconfig.schematics.json b/feature-libs/smartedit/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/feature-libs/smartedit/tsconfig.schematics.json +++ b/feature-libs/smartedit/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/storefinder/tsconfig.schematics.json b/feature-libs/storefinder/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/feature-libs/storefinder/tsconfig.schematics.json +++ b/feature-libs/storefinder/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/tracking/tsconfig.schematics.json b/feature-libs/tracking/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/feature-libs/tracking/tsconfig.schematics.json +++ b/feature-libs/tracking/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/feature-libs/user/tsconfig.schematics.json b/feature-libs/user/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/feature-libs/user/tsconfig.schematics.json +++ b/feature-libs/user/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/integration-libs/cdc/tsconfig.schematics.json b/integration-libs/cdc/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/integration-libs/cdc/tsconfig.schematics.json +++ b/integration-libs/cdc/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/integration-libs/cdp/tsconfig.schematics.json b/integration-libs/cdp/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/integration-libs/cdp/tsconfig.schematics.json +++ b/integration-libs/cdp/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/integration-libs/cds/tsconfig.schematics.json b/integration-libs/cds/tsconfig.schematics.json index 7e200571303..185cef46904 100644 --- a/integration-libs/cds/tsconfig.schematics.json +++ b/integration-libs/cds/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/integration-libs/cpq-quote/tsconfig.schematics.json b/integration-libs/cpq-quote/tsconfig.schematics.json index e5fb75aa73e..2e09eb491ef 100644 --- a/integration-libs/cpq-quote/tsconfig.schematics.json +++ b/integration-libs/cpq-quote/tsconfig.schematics.json @@ -597,6 +597,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/integration-libs/digital-payments/tsconfig.schematics.json b/integration-libs/digital-payments/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/integration-libs/digital-payments/tsconfig.schematics.json +++ b/integration-libs/digital-payments/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/integration-libs/epd-visualization/tsconfig.schematics.json b/integration-libs/epd-visualization/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/integration-libs/epd-visualization/tsconfig.schematics.json +++ b/integration-libs/epd-visualization/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/integration-libs/opps/tsconfig.schematics.json b/integration-libs/opps/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/integration-libs/opps/tsconfig.schematics.json +++ b/integration-libs/opps/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/integration-libs/s4-service/README.md b/integration-libs/s4-service/README.md new file mode 100644 index 00000000000..eb0b4f81378 --- /dev/null +++ b/integration-libs/s4-service/README.md @@ -0,0 +1,7 @@ +# Spartacus S/4HANA Service Integration Integration + +Spartacus' S/4HANA Service Integration library integrates SAP Commerce Cloud with SAP S/4HANA Service, enabling the replication of service products from SAP S/4HANA Service to SAP Commerce Cloud, and service orders from SAP Commerce Cloud to SAP S/4HANA Service. + +It can be added to the existing Spartacus application by running `ng add @spartacus/s4-service`. For more information about Spartacus schematics, visit the [official Spartacus schematics documentation page](https://sap.github.io/spartacus-docs/schematics/). + +For more information, see [Spartacus](https://github.com/SAP/spartacus). diff --git a/integration-libs/s4-service/_index.scss b/integration-libs/s4-service/_index.scss new file mode 100644 index 00000000000..eded5653c89 --- /dev/null +++ b/integration-libs/s4-service/_index.scss @@ -0,0 +1,25 @@ +@import '@spartacus/checkout'; + +@import './styles/index'; + +$s4-service-components-allowlist: cx-service-details !default; + +$skipComponentStyles: () !default; + +@each $selector in $s4-service-components-allowlist { + #{$selector} { + // skip selectors if they're added to the $skipComponentStyles list + @if (index($skipComponentStyles, $selector) ==null) { + @extend %#{$selector} !optional; + } + } +} + +// add body specific selectors +body { + @each $selector in $s4-service-components-allowlist { + @if (index($skipComponentStyles, $selector) ==null) { + @extend %#{$selector}__body !optional; + } + } +} diff --git a/integration-libs/s4-service/assets/ng-package.json b/integration-libs/s4-service/assets/ng-package.json new file mode 100644 index 00000000000..38e01ac17de --- /dev/null +++ b/integration-libs/s4-service/assets/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/integration-libs/s4-service/assets/public_api.ts b/integration-libs/s4-service/assets/public_api.ts new file mode 100644 index 00000000000..76dd4f8f788 --- /dev/null +++ b/integration-libs/s4-service/assets/public_api.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './translations/translations'; diff --git a/integration-libs/s4-service/assets/translations/en/index.ts b/integration-libs/s4-service/assets/translations/en/index.ts new file mode 100644 index 00000000000..c58430e3133 --- /dev/null +++ b/integration-libs/s4-service/assets/translations/en/index.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import s4Service from './s4-service.json'; + +export const en = { + s4Service, +}; diff --git a/integration-libs/s4-service/assets/translations/en/s4-service.json b/integration-libs/s4-service/assets/translations/en/s4-service.json new file mode 100644 index 00000000000..afd5204babe --- /dev/null +++ b/integration-libs/s4-service/assets/translations/en/s4-service.json @@ -0,0 +1,12 @@ +{ + "serviceOrderCheckout": { + "serviceDetails": "Service Details", + "heading": "Service Schedule - Date and Time", + "datePickerLabel": "Schedule Service Date", + "timePickerLabel": "Schedule Service Time", + "cardLabel": "Scheduled At", + "clickContinue": "No Service details required. Click Continue", + "emptyServiceDetailsCard": "None", + "unknownError": "An unknown error occurred. Please contact support." + } +} diff --git a/integration-libs/s4-service/assets/translations/translations.ts b/integration-libs/s4-service/assets/translations/translations.ts new file mode 100644 index 00000000000..9ef87ebe6c5 --- /dev/null +++ b/integration-libs/s4-service/assets/translations/translations.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TranslationChunksConfig, TranslationResources } from '@spartacus/core'; +import { en } from './en/index'; + +export const s4ServiceTranslations: TranslationResources = { + en, +}; + +export const s4ServiceTranslationChunksConfig: TranslationChunksConfig = { + s4Service: ['serviceOrderCheckout'], +}; diff --git a/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.component.html b/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.component.html new file mode 100644 index 00000000000..7ff0da77949 --- /dev/null +++ b/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.component.html @@ -0,0 +1,212 @@ +
+ +

+ {{ 'checkoutReview.review' | cxTranslate }} +

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+
+
+ +
+ +
+ + + +
+
+
+ + + +
+
+ + + + +
+ +
+ + + +
+
+
+ + + +
+ {{ + 'cartItems.cartTotal' + | cxTranslate: { count: cart.deliveryItemsQuantity } + }}: + {{ cart.totalPrice?.formattedValue }} +
+
+ {{ 'checkoutReview.placeOrder' | cxTranslate }} +
+
+ + + + +
+
+
diff --git a/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.component.spec.ts b/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.component.spec.ts new file mode 100644 index 00000000000..2a0cd47733e --- /dev/null +++ b/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.component.spec.ts @@ -0,0 +1,519 @@ +import { Component, Input, Pipe, PipeTransform } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { + ActiveCartFacade, + Cart, + DeliveryMode, + OrderEntry, + PaymentType, +} from '@spartacus/cart/base/root'; +import { + CheckoutCostCenterFacade, + CheckoutPaymentTypeFacade, +} from '@spartacus/checkout/b2b/root'; +import { CheckoutStepService } from '@spartacus/checkout/base/components'; +import { + CheckoutDeliveryAddressFacade, + CheckoutDeliveryModesFacade, + CheckoutPaymentFacade, + CheckoutStep, + CheckoutStepType, +} from '@spartacus/checkout/base/root'; +import { + Address, + CostCenter, + Country, + I18nTestingModule, + PaymentDetails, + QueryState, + UserCostCenterService, +} from '@spartacus/core'; +import { Card, OutletModule, PromotionsModule } from '@spartacus/storefront'; +import { IconTestingModule } from 'projects/storefrontlib/cms-components/misc/icon/testing/icon-testing.module'; +import { BehaviorSubject, EMPTY, Observable, of } from 'rxjs'; +import { ServiceCheckoutReviewSubmitComponent } from './service-checkout-review-submit.component'; + +import createSpy = jasmine.createSpy; +import { + CheckoutServiceDetailsFacade, + CheckoutServiceSchedulePickerService, + ServiceDateTime, +} from '@spartacus/s4-service/root'; + +const mockCart: Cart = { + guid: 'test', + code: 'test', + deliveryItemsQuantity: 123, + totalPrice: { formattedValue: '$999.98' }, +}; +const mockCountry: Country = { isocode: 'JP', name: 'Japan' }; +const mockAddress: Address = { + firstName: 'John', + lastName: 'Doe', + titleCode: 'mr', + line1: 'Toyosaki 2 create on cart', + line2: 'line2', + town: 'town', + region: { isocode: 'JP-27' }, + postalCode: 'zip', + country: mockCountry, +}; +const addressBS = new BehaviorSubject(mockCountry); + +const mockDeliveryMode: DeliveryMode = { + name: 'standard-gross', + description: 'Delivery mode test description', +}; +const deliveryModeBS = new BehaviorSubject>({ + loading: false, + error: false, + data: mockDeliveryMode, +}); + +const mockPaymentDetails: PaymentDetails = { + accountHolderName: 'Name', + cardNumber: '123456789', + cardType: { code: 'Visa', name: 'Visa' }, + expiryMonth: '01', + expiryYear: '2022', + cvn: '123', +}; + +const mockEntries: OrderEntry[] = [{ entryNumber: 123 }, { entryNumber: 456 }]; + +const mockCostCenter: CostCenter = { + code: 'test-cost-center', + name: 'test-cc-name', + unit: { name: 'test-unit-name' }, +}; + +const mockPaymentTypes: PaymentType[] = [ + { code: 'test-account' }, + { code: 'test-card' }, +]; + +const mockScheduledAt = '2024-06-27T09:30:00-04:00'; + +@Component({ + selector: 'cx-card', + template: '', +}) +class MockCardComponent { + @Input() + content: Card; +} + +class MockCheckoutDeliveryAddressService + implements Partial +{ + getDeliveryAddressState(): Observable> { + return of({ + loading: false, + error: false, + data: mockAddress, + }); + } +} + +class MockCheckoutDeliveryModesService + implements Partial +{ + loadSupportedDeliveryModes = createSpy(); + getSelectedDeliveryModeState(): Observable< + QueryState + > { + return deliveryModeBS.asObservable(); + } +} + +class MockCheckoutPaymentService implements Partial { + getPaymentDetailsState(): Observable> { + return of({ loading: false, error: false, data: mockPaymentDetails }); + } + paymentProcessSuccess(): void {} +} + +class MockActiveCartService implements Partial { + getActive(): Observable { + return of(mockCart); + } + getEntries(): Observable { + return of(mockEntries); + } +} + +class MockCheckoutServiceDetails + implements Partial +{ + setServiceScheduleSlot(_scheduledAt: ServiceDateTime) { + return EMPTY; + } + getSelectedServiceDetailsState(): Observable> { + return of({ loading: false, error: false, data: mockScheduledAt }); + } + getServiceProducts(): Observable { + return of([]); + } +} + +const mockCheckoutStep: CheckoutStep = { + id: 'step', + name: 'name', + routeName: '/route', + type: [CheckoutStepType.DELIVERY_MODE], +}; + +class MockCheckoutStepService { + steps$ = of([ + { + id: 'step1', + name: 'step1', + routeName: 'route1', + type: [CheckoutStepType.SERVICE_DETAILS], + }, + { + id: 'step2', + name: 'step2', + routeName: 'route2', + type: [CheckoutStepType.REVIEW_ORDER], + }, + ]); + getCheckoutStep(): CheckoutStep { + return mockCheckoutStep; + } +} + +class MockCheckoutPaymentTypeFacade + implements Partial +{ + getPurchaseOrderNumberState(): Observable> { + return of({ loading: false, error: false, data: 'test-po' }); + } + getSelectedPaymentTypeState(): Observable< + QueryState + > { + return of({ + loading: false, + error: false, + data: { code: mockPaymentTypes[0].code }, + }); + } + isAccountPayment(): Observable { + return of(true); + } +} + +class MockCheckoutCostCenterService + implements Partial +{ + getCostCenterState(): Observable> { + return of({ + loading: false, + error: false, + data: mockCostCenter, + }); + } +} + +class MockUserCostCenterService implements Partial { + getActiveCostCenters(): Observable { + return of([mockCostCenter]); + } +} + +class MockCheckoutServiceSchedulePickerService + implements Partial +{ + convertDateTimeToReadableString(_scheduledAt: string): string { + return '27/06/2024, 09:30'; + } +} + +@Pipe({ + name: 'cxUrl', +}) +class MockUrlPipe implements PipeTransform { + transform(): any {} +} + +describe('ServiceCheckoutReviewSubmitComponent', () => { + let component: ServiceCheckoutReviewSubmitComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + I18nTestingModule, + PromotionsModule, + RouterTestingModule, + IconTestingModule, + OutletModule, + ], + declarations: [ + ServiceCheckoutReviewSubmitComponent, + MockCardComponent, + MockUrlPipe, + ], + providers: [ + { + provide: CheckoutDeliveryAddressFacade, + useClass: MockCheckoutDeliveryAddressService, + }, + { + provide: CheckoutDeliveryModesFacade, + useClass: MockCheckoutDeliveryModesService, + }, + { + provide: CheckoutPaymentFacade, + useClass: MockCheckoutPaymentService, + }, + { provide: ActiveCartFacade, useClass: MockActiveCartService }, + { + provide: CheckoutStepService, + useClass: MockCheckoutStepService, + }, + { + provide: CheckoutPaymentTypeFacade, + useClass: MockCheckoutPaymentTypeFacade, + }, + { + provide: CheckoutCostCenterFacade, + useClass: MockCheckoutCostCenterService, + }, + { + provide: UserCostCenterService, + useClass: MockUserCostCenterService, + }, + { + provide: CheckoutServiceDetailsFacade, + useClass: MockCheckoutServiceDetails, + }, + { + provide: CheckoutServiceSchedulePickerService, + useClass: MockCheckoutServiceSchedulePickerService, + }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ServiceCheckoutReviewSubmitComponent); + component = fixture.componentInstance; + + addressBS.next(mockCountry); + deliveryModeBS.next({ + loading: false, + error: false, + data: mockDeliveryMode, + }); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); + + it('should be able to get cart', () => { + let cart: Cart | undefined; + component.cart$.subscribe((data: Cart) => { + cart = data; + }); + + expect(cart).toEqual(mockCart); + }); + + it('should be able to get entries', () => { + let entries: OrderEntry[] | undefined; + component.entries$.subscribe((data: OrderEntry[]) => { + entries = data; + }); + + expect(entries).toEqual(mockEntries); + }); + + it('should be able to get steps', () => { + let steps: CheckoutStep[] | undefined; + component.steps$.subscribe((data) => (steps = data)); + + expect(steps?.[0]).toEqual({ + id: 'step1', + name: 'step1', + routeName: 'route1', + type: [CheckoutStepType.SERVICE_DETAILS], + }); + expect(steps?.[1]).toEqual({ + id: 'step2', + name: 'step2', + routeName: 'route2', + type: [CheckoutStepType.REVIEW_ORDER], + }); + }); + + it('should be able to get deliveryAddress', () => { + let deliveryAddress: Address | undefined; + component.deliveryAddress$.subscribe((data) => { + deliveryAddress = data; + }); + + expect(deliveryAddress).toEqual(mockAddress); + }); + + it('should be able to get paymentDetails', () => { + let paymentDetails: PaymentDetails | undefined; + component.paymentDetails$.subscribe((data) => { + paymentDetails = data; + }); + + expect(paymentDetails).toEqual(mockPaymentDetails); + }); + + it('should be able to get deliveryMode if a mode is selected', () => { + let deliveryMode: DeliveryMode | undefined; + component.deliveryMode$.subscribe((data) => { + deliveryMode = data; + }); + + expect(deliveryMode).toEqual(mockDeliveryMode); + }); + + it('should be able to get po number', () => { + let po: string | undefined; + component.poNumber$.subscribe((data) => { + po = data; + }); + + expect(po).toEqual('test-po'); + }); + + it('should be able to get cost center', () => { + let costCenter: CostCenter | undefined; + component.costCenter$.subscribe((data) => { + costCenter = data; + }); + + expect(costCenter).toEqual(mockCostCenter); + }); + + it('should get selected payment type', () => { + let paymentType: PaymentType | undefined; + component.paymentType$.subscribe((data) => { + paymentType = data; + }); + + expect(paymentType).toEqual(mockPaymentTypes[0]); + }); + + it('should be able to get service details', () => { + let serviceDetails: string | undefined; + component.scheduledAt$.subscribe((data) => { + serviceDetails = data; + }); + expect(serviceDetails).toEqual(mockScheduledAt); + }); + + it('should call getDeliveryAddressCard(deliveryAddress, countryName) to get address card data', () => { + component + .getDeliveryAddressCard(mockAddress, 'Canada') + .subscribe((card) => { + expect(card.title).toEqual('addressCard.shipTo'); + expect(card.textBold).toEqual('John Doe'); + expect(card.text).toEqual([ + 'Toyosaki 2 create on cart', + 'line2', + 'town, JP-27, Canada', + 'zip', + undefined, + ]); + }); + }); + + it('should call getDeliveryModeCard(deliveryMode) to get delivery mode card data', () => { + const selectedMode: DeliveryMode = { + code: 'standard-gross', + name: 'Standard gross', + description: 'Standard Delivery description', + deliveryCost: { + formattedValue: '$9.99', + }, + }; + component.getDeliveryModeCard(selectedMode).subscribe((card) => { + expect(card.title).toEqual('checkoutMode.deliveryMethod'); + expect(card.textBold).toEqual('Standard gross'); + expect(card.text).toEqual(['Standard Delivery description', '$9.99']); + }); + }); + + it('should call getPaymentMethodCard(paymentDetails) to get payment card data', () => { + component.getPaymentMethodCard(mockPaymentDetails).subscribe((card) => { + expect(card.title).toEqual('paymentForm.payment'); + expect(card.textBold).toEqual(mockPaymentDetails.accountHolderName); + expect(card.text).toEqual([ + mockPaymentDetails.cardNumber, + `paymentCard.expires month:${mockPaymentDetails.expiryMonth} year:${mockPaymentDetails.expiryYear}`, + ]); + }); + }); + + it('should call getPoNumberCard(po) to get po card data', () => { + component.getPoNumberCard('test-po').subscribe((card) => { + expect(card.title).toEqual('checkoutB2B.review.poNumber'); + expect(card.textBold).toEqual('test-po'); + }); + }); + + it('should call getCostCenter(costCenter) to get cost center ard data', () => { + component.getCostCenterCard(mockCostCenter).subscribe((card) => { + expect(card.title).toEqual('checkoutB2B.costCenter'); + expect(card.textBold).toEqual(mockCostCenter.name); + expect(card.text).toEqual(['(' + mockCostCenter.unit?.name + ')']); + }); + }); + + it('should call getPaymentTypeCard(paymentType) to get payment type data', () => { + component.getPaymentTypeCard(mockPaymentTypes[0]).subscribe((card) => { + expect(card.title).toEqual('checkoutB2B.progress.methodOfPayment'); + expect(card.textBold).toEqual('paymentTypes.paymentType_test-account'); + }); + }); + it('should call getServiceDetailsCard() to get service details', (done) => { + component.getServiceDetailsCard(mockScheduledAt).subscribe((card) => { + expect(card.title).toEqual('serviceOrderCheckout.serviceDetails'); + expect(card.textBold).toEqual('serviceOrderCheckout.cardLabel'); + expect(card.text).toEqual(['27/06/2024, 09:30']); + done(); + }); + }); + + it('should call getEmptyServiceDetailsCard() to get empty card', (done) => { + component.getServiceDetailsCard(null).subscribe((card) => { + expect(card.title).toEqual('serviceOrderCheckout.serviceDetails'); + expect(card.textBold).toEqual( + 'serviceOrderCheckout.emptyServiceDetailsCard' + ); + done(); + }); + }); + + it('should get checkout step url', () => { + expect( + component.getCheckoutStepUrl(CheckoutStepType.DELIVERY_MODE) + ).toEqual(mockCheckoutStep.routeName); + }); + + describe('UI cart total section', () => { + const getCartTotalText = () => + fixture.debugElement.query(By.css('.cx-review-cart-total')).nativeElement + .textContent; + + it('should contain total number of items', () => { + fixture.detectChanges(); + expect(getCartTotalText()).toContain(123); + }); + + it('should contain total price', () => { + fixture.detectChanges(); + expect(getCartTotalText()).toContain('$999.98'); + }); + }); +}); diff --git a/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.component.ts b/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.component.ts new file mode 100644 index 00000000000..479b13362fc --- /dev/null +++ b/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.component.ts @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ActiveCartFacade } from '@spartacus/cart/base/root'; +import { B2BCheckoutReviewSubmitComponent } from '@spartacus/checkout/b2b/components'; +import { + CheckoutCostCenterFacade, + CheckoutPaymentTypeFacade, +} from '@spartacus/checkout/b2b/root'; +import { CheckoutStepService } from '@spartacus/checkout/base/components'; +import { + CheckoutDeliveryAddressFacade, + CheckoutDeliveryModesFacade, + CheckoutPaymentFacade, + CheckoutStepType, +} from '@spartacus/checkout/base/root'; +import { TranslationService, UserCostCenterService } from '@spartacus/core'; +import { Card } from '@spartacus/storefront'; +import { combineLatest, Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { + CheckoutServiceDetailsFacade, + CheckoutServiceSchedulePickerService, +} from '@spartacus/s4-service/root'; + +@Component({ + selector: 'cx-review-submit', + templateUrl: './service-checkout-review-submit.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ServiceCheckoutReviewSubmitComponent extends B2BCheckoutReviewSubmitComponent { + checkoutStepTypeServiceDetails = CheckoutStepType.SERVICE_DETAILS; + protected checkoutServiceDetailsFacade = inject(CheckoutServiceDetailsFacade); + protected checkoutServiceSchedulePickerService = inject( + CheckoutServiceSchedulePickerService + ); + + constructor( + protected checkoutDeliveryAddressFacade: CheckoutDeliveryAddressFacade, + protected checkoutPaymentFacade: CheckoutPaymentFacade, + protected activeCartFacade: ActiveCartFacade, + protected translationService: TranslationService, + protected checkoutStepService: CheckoutStepService, + protected checkoutDeliveryModesFacade: CheckoutDeliveryModesFacade, + protected checkoutPaymentTypeFacade: CheckoutPaymentTypeFacade, + protected checkoutCostCenterFacade: CheckoutCostCenterFacade, + protected userCostCenterService: UserCostCenterService + ) { + super( + checkoutDeliveryAddressFacade, + checkoutPaymentFacade, + activeCartFacade, + translationService, + checkoutStepService, + checkoutDeliveryModesFacade, + checkoutPaymentTypeFacade, + checkoutCostCenterFacade, + userCostCenterService + ); + } + + get scheduledAt$(): Observable { + return this.checkoutServiceDetailsFacade + .getSelectedServiceDetailsState() + .pipe( + filter((state) => !state.loading && !state.error), + map((state) => { + return state.data; + }) + ); + } + protected getCheckoutDeliverySteps(): Array { + return [ + CheckoutStepType.DELIVERY_ADDRESS, + CheckoutStepType.DELIVERY_MODE, + CheckoutStepType.SERVICE_DETAILS, + ]; + } + + getServiceDetailsCard( + scheduledAt: string | null | undefined + ): Observable { + return combineLatest([ + this.translationService.translate('serviceOrderCheckout.serviceDetails'), + this.translationService.translate('serviceOrderCheckout.cardLabel'), + this.translationService.translate( + 'serviceOrderCheckout.emptyServiceDetailsCard' + ), + ]).pipe( + map(([textTitle, textLabel, emptyTextLabel]) => { + if (scheduledAt) { + scheduledAt = + this.checkoutServiceSchedulePickerService.convertDateTimeToReadableString( + scheduledAt + ); + } + + return { + title: textTitle, + textBold: scheduledAt ? textLabel : emptyTextLabel, + text: scheduledAt ? [scheduledAt] : undefined, + }; + }) + ); + } +} diff --git a/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.module.ts b/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.module.ts new file mode 100644 index 00000000000..56365e6f748 --- /dev/null +++ b/integration-libs/s4-service/checkout/components/checkout-review-submit/service-checkout-review-submit.module.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { + CartNotEmptyGuard, + CheckoutAuthGuard, +} from '@spartacus/checkout/base/components'; +import { + CmsConfig, + I18nModule, + UrlModule, + provideDefaultConfig, +} from '@spartacus/core'; +import { + CardModule, + IconModule, + OutletModule, + PromotionsModule, +} from '@spartacus/storefront'; +import { ServiceCheckoutReviewSubmitComponent } from './service-checkout-review-submit.component'; + +@NgModule({ + imports: [ + CommonModule, + CardModule, + I18nModule, + UrlModule, + RouterModule, + PromotionsModule, + IconModule, + OutletModule, + ], + providers: [ + provideDefaultConfig({ + cmsComponents: { + CheckoutReviewOrder: { + component: ServiceCheckoutReviewSubmitComponent, + guards: [CheckoutAuthGuard, CartNotEmptyGuard], + }, + }, + }), + ], + declarations: [ServiceCheckoutReviewSubmitComponent], + exports: [ServiceCheckoutReviewSubmitComponent], +}) +export class ServiceCheckoutReviewSubmitModule {} diff --git a/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.component.html b/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.component.html new file mode 100644 index 00000000000..65f5484eaaa --- /dev/null +++ b/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.component.html @@ -0,0 +1,84 @@ +

+ {{ 'serviceOrderCheckout.heading' | cxTranslate }} +

+ + + + + {{ 'serviceOrderCheckout.clickContinue' | cxTranslate }} + + + + +
+
+ +
+
+ +
+
+ + +
+ + + +
+
diff --git a/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.component.spec.ts b/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.component.spec.ts new file mode 100644 index 00000000000..5f47c717cb9 --- /dev/null +++ b/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.component.spec.ts @@ -0,0 +1,161 @@ +import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; +import { CheckoutServiceDetailsComponent } from './checkout-service-details.component'; +import { + GlobalMessageService, + I18nTestingModule, + QueryState, +} from '@spartacus/core'; +import { UntypedFormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { CheckoutStepService } from '@spartacus/checkout/base/components'; +import { + CheckoutServiceDetailsFacade, + CheckoutServiceSchedulePickerService, + ServiceDateTime, +} from '@spartacus/s4-service/root'; +import { Observable, of, throwError } from 'rxjs'; +import createSpy = jasmine.createSpy; +const mockScheduledAt = '2024-06-27T09:30:00-04:00'; +class MockActivatedRoute implements Partial {} +class MockCheckoutStepService implements Partial { + getBackBntText = createSpy().and.returnValue('common.back'); + next = createSpy().and.callThrough(); + back = createSpy().and.callThrough(); +} +class MockGlobalMessageService implements Partial { + add = createSpy().and.callThrough(); +} +class MockCheckoutServiceDetailsFacade { + getSelectedServiceDetailsState(): Observable< + QueryState + > { + return of({ loading: false, error: false, data: mockScheduledAt }); + } + + getServiceProducts(): Observable { + return of(['product1', 'product2']); + } + + setServiceScheduleSlot(_scheduledAt: ServiceDateTime) { + return of({ success: true }); + } +} +class MockCheckoutServiceSchedulePickerService { + // implements Partial + getMinDateForService = createSpy().and.returnValue(of('2024-06-25')); + getScheduledServiceTimes = createSpy().and.returnValue( + of(['8:30', '9:30', '10:30']) + ); + convertDateTimeToReadableString = + createSpy().and.returnValue('27/06/2024, 9:30'); + getServiceDetailsFromDateTime = createSpy().and.returnValue({ + date: '27/06/2024', + time: '09:30', + }); + convertToDateTime = createSpy().and.returnValue('2024-06-27T09:30:00-04:00'); +} +describe('CheckoutServiceDetailsComponent', () => { + let component: CheckoutServiceDetailsComponent; + let fixture: ComponentFixture; + let checkoutServiceDetailsFacade: CheckoutServiceDetailsFacade; + let pickerService: CheckoutServiceSchedulePickerService; + let checkoutStepService: CheckoutStepService; + let messageService: GlobalMessageService; + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [I18nTestingModule], + declarations: [CheckoutServiceDetailsComponent], + providers: [ + { + provide: ActivatedRoute, + useClass: MockActivatedRoute, + }, + { + provide: CheckoutStepService, + useClass: MockCheckoutStepService, + }, + { + provide: GlobalMessageService, + useClass: MockGlobalMessageService, + }, + { + provide: CheckoutServiceDetailsFacade, + useClass: MockCheckoutServiceDetailsFacade, + }, + UntypedFormBuilder, + { + provide: CheckoutServiceSchedulePickerService, + useClass: MockCheckoutServiceSchedulePickerService, + }, + ], + }).compileComponents(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(CheckoutServiceDetailsComponent); + checkoutServiceDetailsFacade = TestBed.inject(CheckoutServiceDetailsFacade); + pickerService = TestBed.inject(CheckoutServiceSchedulePickerService); + checkoutStepService = TestBed.inject(CheckoutStepService); + messageService = TestBed.inject(GlobalMessageService); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should be created', () => { + expect(component).toBeTruthy(); + }); + it('should call ngOnInit and set already selected date and time', () => { + component.ngOnInit(); + expect(component.form?.get('scheduleDate')?.value).toEqual('27/06/2024'); + expect(component.form?.get('scheduleTime')?.value).toEqual('09:30'); + expect(pickerService.convertDateTimeToReadableString).toHaveBeenCalled(); + expect(pickerService.getServiceDetailsFromDateTime).toHaveBeenCalled(); + }); + it('should get back button text', () => { + component.back(); + expect(checkoutStepService.getBackBntText).toHaveBeenCalled(); + expect(component.backBtnText).toEqual('common.back'); + }); + it('should set schedule time', () => { + component.setScheduleTime({ + target: { value: '10:30' }, + } as unknown as Event); + expect(component.form?.get('scheduleTime')?.value).toEqual('10:30'); + }); + it('should update service details when service products are available in cart', () => { + spyOn(checkoutServiceDetailsFacade, 'getServiceProducts').and.returnValue( + of(['123', '456']) + ); + spyOn( + checkoutServiceDetailsFacade, + 'setServiceScheduleSlot' + ).and.callThrough(); + spyOn( + checkoutServiceDetailsFacade, + 'getSelectedServiceDetailsState' + ).and.callThrough(); + + component.next(); + expect( + checkoutServiceDetailsFacade.setServiceScheduleSlot + ).toHaveBeenCalled(); + expect(checkoutStepService.next).toHaveBeenCalled(); + }); + it('should move to next step when no service products are available in cart', () => { + spyOn(checkoutServiceDetailsFacade, 'getServiceProducts').and.returnValue( + of([]) + ); + component.next(); + expect(checkoutStepService.next).toHaveBeenCalled(); + }); + it('should show error if any error throw', () => { + spyOn(checkoutServiceDetailsFacade, 'getServiceProducts').and.returnValue( + of(['3435']) + ); + spyOn( + checkoutServiceDetailsFacade, + 'setServiceScheduleSlot' + ).and.returnValue(throwError('Throwing Error message')); + component.next(); + expect(checkoutStepService.next).not.toHaveBeenCalled(); + expect(messageService.add).toHaveBeenCalled(); + }); +}); diff --git a/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.component.ts b/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.component.ts new file mode 100644 index 00000000000..f52818244b5 --- /dev/null +++ b/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.component.ts @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + Component, + OnInit, + OnDestroy, + inject, + ChangeDetectionStrategy, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { CheckoutStepService } from '@spartacus/checkout/base/components'; +import { GlobalMessageService, GlobalMessageType } from '@spartacus/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Subscription, BehaviorSubject, Observable, combineLatest } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { + CheckoutServiceDetailsFacade, + CheckoutServiceSchedulePickerService, +} from '@spartacus/s4-service/root'; + +@Component({ + selector: 'cx-service-details', + templateUrl: './checkout-service-details.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CheckoutServiceDetailsComponent implements OnInit, OnDestroy { + protected activatedRoute = inject(ActivatedRoute); + protected checkoutStepService = inject(CheckoutStepService); + protected globalMessageService = inject(GlobalMessageService); + protected checkoutServiceDetailsFacade = inject(CheckoutServiceDetailsFacade); + protected fb = inject(FormBuilder); + protected checkoutServiceSchedulePickerService = inject( + CheckoutServiceSchedulePickerService + ); + + minServiceDate$: Observable = + this.checkoutServiceSchedulePickerService.getMinDateForService(); + scheduleTimes$: Observable = + this.checkoutServiceSchedulePickerService.getScheduledServiceTimes(); + form: FormGroup = this.fb.group({ + scheduleDate: [null, Validators.required], + scheduleTime: [null, Validators.required], + }); + + protected subscription = new Subscription(); + + selectedServiceDetails$ = this.checkoutServiceDetailsFacade + .getSelectedServiceDetailsState() + .pipe( + filter((state) => !state.loading), + map((state) => state.data) + ); + + ngOnInit(): void { + this.subscription.add( + this.selectedServiceDetails$.subscribe((selectedServiceDetails) => { + if (selectedServiceDetails && selectedServiceDetails !== '') { + const scheduledAt = + this.checkoutServiceSchedulePickerService.convertDateTimeToReadableString( + selectedServiceDetails + ); + const info = + this.checkoutServiceSchedulePickerService.getServiceDetailsFromDateTime( + scheduledAt + ); + this.form.patchValue({ + scheduleDate: info.date, + scheduleTime: info.time, + }); + } else { + combineLatest([this.minServiceDate$, this.scheduleTimes$]).subscribe( + ([minDate, scheduleTime]) => { + this.form.patchValue({ + scheduleDate: minDate, + scheduleTime: scheduleTime[0], + }); + } + ); + } + }) + ); + } + + setScheduleTime(event: Event): void { + const target = event.target as HTMLSelectElement; + const value = target.value; + this.form.patchValue({ + scheduleTime: value, + }); + } + + get backBtnText(): string { + return this.checkoutStepService.getBackBntText(this.activatedRoute); + } + + protected readonly isSetServiceDetailsHttpErrorSub = new BehaviorSubject( + false + ); + isSetServiceDetailsHttpError$ = + this.isSetServiceDetailsHttpErrorSub.asObservable(); + + next(): void { + this.subscription.add( + this.serviceProducts$.subscribe((products) => { + if (products.length > 0) { + const scheduleDate = this.form?.get('scheduleDate')?.value || ''; + const scheduleTime = this.form?.get('scheduleTime')?.value || ''; + const scheduleDateTime = + this.checkoutServiceSchedulePickerService.convertToDateTime( + scheduleDate, + scheduleTime + ); + + this.checkoutServiceDetailsFacade + .setServiceScheduleSlot(scheduleDateTime) + .subscribe({ + next: () => { + this.onSuccess(); + this.checkoutStepService.next(this.activatedRoute); + }, + error: () => this.onError(), + }); + } else { + this.checkoutStepService.next(this.activatedRoute); + } + }) + ); + } + + back(): void { + this.checkoutStepService.back(this.activatedRoute); + } + + serviceProducts$ = this.checkoutServiceDetailsFacade.getServiceProducts(); + + protected onSuccess(): void { + this.isSetServiceDetailsHttpErrorSub.next(false); + } + + protected onError(): void { + this.globalMessageService?.add( + { key: 'serviceOrderCheckout.unknownError' }, + GlobalMessageType.MSG_TYPE_ERROR + ); + + this.isSetServiceDetailsHttpErrorSub.next(true); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} diff --git a/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.module.ts b/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.module.ts new file mode 100644 index 00000000000..121f1e45478 --- /dev/null +++ b/integration-libs/s4-service/checkout/components/checkout-service-details/checkout-service-details.module.ts @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { + CartNotEmptyGuard, + CheckoutAuthGuard, +} from '@spartacus/checkout/base/components'; +import { CmsConfig, I18nModule, provideDefaultConfig } from '@spartacus/core'; +import { CheckoutServiceDetailsComponent } from './checkout-service-details.component'; +import { DatePickerModule } from '@spartacus/storefront'; +import { ReactiveFormsModule } from '@angular/forms'; + +@NgModule({ + imports: [CommonModule, I18nModule, DatePickerModule, ReactiveFormsModule], + providers: [ + provideDefaultConfig({ + cmsComponents: { + CheckoutServiceDetails: { + component: CheckoutServiceDetailsComponent, + guards: [CheckoutAuthGuard, CartNotEmptyGuard], + }, + }, + }), + ], + exports: [CheckoutServiceDetailsComponent], + declarations: [CheckoutServiceDetailsComponent], +}) +export class CheckoutServiceDetailsModule {} diff --git a/integration-libs/s4-service/checkout/components/guards/checkout-service-order-steps-set.guard.spec.ts b/integration-libs/s4-service/checkout/components/guards/checkout-service-order-steps-set.guard.spec.ts new file mode 100644 index 00000000000..8fe509b3b7b --- /dev/null +++ b/integration-libs/s4-service/checkout/components/guards/checkout-service-order-steps-set.guard.spec.ts @@ -0,0 +1,330 @@ +import { TestBed } from '@angular/core/testing'; +import { CheckoutServiceOrderStepsSetGuard } from './checkout-service-order-steps-set.guard'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { CheckoutServiceDetailsFacade } from '@spartacus/s4-service/root'; +import { + Address, + CostCenter, + PaymentDetails, + QueryState, + RouteConfig, + RoutingConfigService, +} from '@spartacus/core'; +import { + CheckoutDeliveryAddressFacade, + CheckoutDeliveryModesFacade, + CheckoutPaymentFacade, + CheckoutStep, + CheckoutStepType, +} from '@spartacus/checkout/base/root'; +import { StoreModule } from '@ngrx/store'; +import { CheckoutB2BStepsSetGuard } from '@spartacus/checkout/b2b/components'; +import { + PaymentType, + DeliveryMode, + ActiveCartFacade, +} from '@spartacus/cart/base/root'; +import { + CheckoutCostCenterFacade, + CheckoutPaymentTypeFacade, +} from '@spartacus/checkout/b2b/root'; +import { CheckoutStepService } from '@spartacus/checkout/base/components'; +import createSpy = jasmine.createSpy; + +class MockRoutingConfigService implements Partial { + getRouteConfig(stepRoute: string): RouteConfig | undefined { + if (stepRoute === 'route0') { + return { paths: ['checkout/route0'] }; + } else if (stepRoute === 'route1') { + return { paths: ['checkout/route1'] }; + } else if (stepRoute === 'route2') { + return { paths: ['checkout/route2'] }; + } else if (stepRoute === 'route3') { + return { paths: ['checkout/route3'] }; + } else if (stepRoute === 'route4') { + return { paths: ['checkout/route4'] }; + } else if (stepRoute === 'checkout') { + return { paths: ['checkout'] }; + } + return undefined; + } +} + +const mockCheckoutSteps: Array = [ + { + id: 'step0', + name: 'step 0', + routeName: 'route0', + type: [CheckoutStepType.PAYMENT_TYPE], + }, + { + id: 'step1', + name: 'step 1', + routeName: 'route1', + type: [CheckoutStepType.DELIVERY_ADDRESS], + }, + { + id: 'step2', + name: 'step 2', + routeName: 'route2', + type: [CheckoutStepType.DELIVERY_MODE], + }, + { + id: 'step3', + name: 'step 3', + routeName: 'route3', + type: [CheckoutStepType.PAYMENT_DETAILS], + }, + { + id: 'step4', + name: 'step 4', + routeName: 'route4', + type: [CheckoutStepType.DELIVERY_MODE], + }, +]; + +class MockCheckoutStepService implements Partial { + steps$: BehaviorSubject = new BehaviorSubject( + mockCheckoutSteps + ); + disableEnableStep = createSpy(); + getCheckoutStep = createSpy().and.returnValue({}); +} + +class MockCheckoutCostCenterService + implements Partial +{ + getCostCenterState(): Observable> { + return of({ loading: false, error: false, data: undefined }); + } +} + +// initial it as ACCOUNT payment +const isAccount: BehaviorSubject = new BehaviorSubject(true); +class MockCheckoutPaymentTypeService + implements Partial +{ + isAccountPayment(): Observable { + return isAccount; + } + getSelectedPaymentTypeState(): Observable< + QueryState + > { + return of({ loading: false, error: false, data: undefined }); + } +} + +class MockCheckoutDeliveryAddressFacade + implements Partial +{ + getDeliveryAddressState(): Observable> { + return of({ loading: false, error: false, data: undefined }); + } +} + +class MockCheckoutDeliveryModeFacade + implements Partial +{ + getSelectedDeliveryModeState(): Observable< + QueryState + > { + return of({ loading: false, error: false, data: undefined }); + } +} + +class MockCheckoutPaymentFacade implements Partial { + getPaymentDetailsState(): Observable> { + return of({ loading: false, error: false, data: undefined }); + } +} + +class MockCartService implements Partial { + hasDeliveryItems = createSpy().and.returnValue(of(false)); +} +const mockScheduledAt = '2024-06-27T09:30:00-04:00'; +class MockCheckoutServiceDetailsFacade + implements Partial +{ + getSelectedServiceDetailsState(): Observable> { + return of({ loading: false, error: false, data: mockScheduledAt }); + } + getServiceProducts(): Observable { + return of(['456']); + } +} + +describe('CheckoutServiceOrderStepsSetGuard', () => { + let guard: CheckoutServiceOrderStepsSetGuard; + let facade: CheckoutServiceDetailsFacade; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [StoreModule.forRoot({})], + providers: [ + { + provide: CheckoutServiceDetailsFacade, + useClass: MockCheckoutServiceDetailsFacade, + }, + CheckoutB2BStepsSetGuard, + { provide: CheckoutStepService, useClass: MockCheckoutStepService }, + { + provide: CheckoutPaymentTypeFacade, + useClass: MockCheckoutPaymentTypeService, + }, + { + provide: CheckoutCostCenterFacade, + useClass: MockCheckoutCostCenterService, + }, + { + provide: CheckoutDeliveryModesFacade, + useClass: MockCheckoutDeliveryModeFacade, + }, + { + provide: CheckoutDeliveryAddressFacade, + useClass: MockCheckoutDeliveryAddressFacade, + }, + { + provide: CheckoutPaymentFacade, + useClass: MockCheckoutPaymentFacade, + }, + { provide: RoutingConfigService, useClass: MockRoutingConfigService }, + { provide: ActiveCartFacade, useClass: MockCartService }, + ], + }); + guard = TestBed.inject(CheckoutServiceOrderStepsSetGuard); + facade = TestBed.inject(CheckoutServiceDetailsFacade); + }); + it('should be created', () => { + expect(guard).toBeTruthy(); + }); + it('should move to next step once service details are set', () => { + spyOn(facade, 'getSelectedServiceDetailsState').and.callThrough(); + spyOn(facade, 'getServiceProducts').and.callThrough(); + (guard as any) + .isServiceDetailsSet({ + type: CheckoutStepType.SERVICE_DETAILS, + routeName: 'test', + }) + .subscribe((response: any) => { + expect(response).toEqual(true); + }); + }); + it('should move to next step once service details are set', () => { + spyOn(facade, 'getSelectedServiceDetailsState').and.returnValue( + of({ loading: false, error: false, data: undefined }) + ); + spyOn(facade, 'getServiceProducts').and.returnValue(of(['a', 'b'])); + spyOn(guard as any, 'getUrl').and.returnValue('/'); + (guard as any) + .isServiceDetailsSet({ + type: CheckoutStepType.SERVICE_DETAILS, + routeName: 'test', + }) + .subscribe((response: any) => { + expect(response).toEqual('/'); + expect((guard as any).getUrl).toHaveBeenCalled(); + }); + }); + describe('isB2BStepSet', () => { + it('should check if payment type is set', (done) => { + spyOn(guard as any, 'isPaymentTypeSet').and.returnValue(of(true)); + (guard as any) + .isB2BStepSet( + { disabled: false, type: [CheckoutStepType.PAYMENT_TYPE] }, + true + ) + .subscribe(() => { + expect((guard as any).isPaymentTypeSet).toHaveBeenCalledWith({ + disabled: false, + type: [CheckoutStepType.PAYMENT_TYPE], + }); + done(); + }); + }); + it('should check if delivery address is set', (done) => { + spyOn(guard as any, 'isDeliveryAddressAndCostCenterSet').and.returnValue( + of(true) + ); + (guard as any) + .isB2BStepSet( + { disabled: false, type: [CheckoutStepType.DELIVERY_ADDRESS] }, + true + ) + .subscribe(() => { + expect( + (guard as any).isDeliveryAddressAndCostCenterSet + ).toHaveBeenCalledWith( + { disabled: false, type: [CheckoutStepType.DELIVERY_ADDRESS] }, + true + ); + done(); + }); + }); + it('should check if delivery mode is set', (done) => { + spyOn(guard as any, 'isDeliveryModeSet').and.returnValue(of(true)); + (guard as any) + .isB2BStepSet( + { disabled: false, type: [CheckoutStepType.DELIVERY_MODE] }, + true + ) + .subscribe(() => { + expect((guard as any).isDeliveryModeSet).toHaveBeenCalledWith({ + disabled: false, + type: [CheckoutStepType.DELIVERY_MODE], + }); + done(); + }); + }); + it('should check if service details is set', (done) => { + spyOn(guard as any, 'isServiceDetailsSet').and.returnValue(of(true)); + (guard as any) + .isB2BStepSet( + { disabled: false, type: [CheckoutStepType.SERVICE_DETAILS] }, + true + ) + .subscribe(() => { + expect((guard as any).isServiceDetailsSet).toHaveBeenCalledWith({ + disabled: false, + type: [CheckoutStepType.SERVICE_DETAILS], + }); + done(); + }); + }); + it('should check if payment details is set', (done) => { + spyOn(guard as any, 'isPaymentDetailsSet').and.returnValue(of(true)); + (guard as any) + .isB2BStepSet( + { disabled: false, type: [CheckoutStepType.PAYMENT_DETAILS] }, + true + ) + .subscribe(() => { + expect((guard as any).isPaymentDetailsSet).toHaveBeenCalledWith({ + disabled: false, + type: [CheckoutStepType.PAYMENT_DETAILS], + }); + done(); + }); + }); + it('should check if review order is reached', (done) => { + (guard as any) + .isB2BStepSet( + { disabled: false, type: [CheckoutStepType.REVIEW_ORDER] }, + true + ) + .subscribe((response: any) => { + expect(response).toEqual(true); + done(); + }); + }); + it('should return true if step is disabled', (done) => { + (guard as any) + .isB2BStepSet( + { disabled: true, type: [CheckoutStepType.PAYMENT_DETAILS] }, + true + ) + .subscribe((response: any) => { + expect(response).toEqual(true); + done(); + }); + }); + }); +}); diff --git a/integration-libs/s4-service/checkout/components/guards/checkout-service-order-steps-set.guard.ts b/integration-libs/s4-service/checkout/components/guards/checkout-service-order-steps-set.guard.ts new file mode 100644 index 00000000000..a5740dc0a45 --- /dev/null +++ b/integration-libs/s4-service/checkout/components/guards/checkout-service-order-steps-set.guard.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable, inject } from '@angular/core'; +import { UrlTree } from '@angular/router'; +import { CheckoutB2BStepsSetGuard } from '@spartacus/checkout/b2b/components'; +import { CheckoutStep, CheckoutStepType } from '@spartacus/checkout/base/root'; +import { CheckoutServiceDetailsFacade } from '@spartacus/s4-service/root'; +import { Observable, filter, map, of, switchMap } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class CheckoutServiceOrderStepsSetGuard extends CheckoutB2BStepsSetGuard { + protected checkoutServiceDetailsFacade = inject(CheckoutServiceDetailsFacade); + protected isServiceDetailsSet( + step: CheckoutStep + ): Observable { + return this.checkoutServiceDetailsFacade + .getSelectedServiceDetailsState() + .pipe( + filter((state) => !state.loading && !state.error), + switchMap((selectedServiceDetails) => + this.checkoutServiceDetailsFacade + .getServiceProducts() + .pipe( + map((products) => + (products.length > 0 && selectedServiceDetails.data) || + products.length === 0 + ? true + : this.getUrl(step.routeName) + ) + ) + ) + ); + } + + protected override isB2BStepSet( + step: CheckoutStep, + isAccountPayment: boolean + ): Observable { + if (step && !step.disabled) { + switch (step.type[0]) { + case CheckoutStepType.PAYMENT_TYPE: + return this.isPaymentTypeSet(step); + case CheckoutStepType.DELIVERY_ADDRESS: + return this.isDeliveryAddressAndCostCenterSet(step, isAccountPayment); + case CheckoutStepType.DELIVERY_MODE: + return this.isDeliveryModeSet(step); + case CheckoutStepType.SERVICE_DETAILS: + return this.isServiceDetailsSet(step); + case CheckoutStepType.PAYMENT_DETAILS: + return this.isPaymentDetailsSet(step); + case CheckoutStepType.REVIEW_ORDER: + break; + } + } + return of(true); + } +} diff --git a/integration-libs/s4-service/checkout/components/guards/index.ts b/integration-libs/s4-service/checkout/components/guards/index.ts new file mode 100644 index 00000000000..2f6a735e45a --- /dev/null +++ b/integration-libs/s4-service/checkout/components/guards/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './checkout-service-order-steps-set.guard'; diff --git a/integration-libs/s4-service/checkout/components/index.ts b/integration-libs/s4-service/checkout/components/index.ts new file mode 100644 index 00000000000..13076b3165a --- /dev/null +++ b/integration-libs/s4-service/checkout/components/index.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './checkout-review-submit/service-checkout-review-submit.component'; +export * from './checkout-review-submit/service-checkout-review-submit.module'; + +export * from './guards/index'; + +export * from './checkout-service-details/checkout-service-details.component'; +export * from './checkout-service-details/checkout-service-details.module'; + +export * from './s4-service-checkout-component.module'; diff --git a/integration-libs/s4-service/checkout/components/s4-service-checkout-component.module.ts b/integration-libs/s4-service/checkout/components/s4-service-checkout-component.module.ts new file mode 100644 index 00000000000..b8403f25189 --- /dev/null +++ b/integration-libs/s4-service/checkout/components/s4-service-checkout-component.module.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { CheckoutStepsSetGuard } from '@spartacus/checkout/base/components'; +import { CheckoutServiceOrderStepsSetGuard } from './guards'; +import { ServiceCheckoutReviewSubmitModule } from './checkout-review-submit/service-checkout-review-submit.module'; +import { CheckoutServiceDetailsModule } from './checkout-service-details/checkout-service-details.module'; + +@NgModule({ + imports: [ServiceCheckoutReviewSubmitModule, CheckoutServiceDetailsModule], + providers: [ + { + provide: CheckoutStepsSetGuard, + useExisting: CheckoutServiceOrderStepsSetGuard, + }, + ], +}) +export class S4ServiceCheckoutComponentModule {} diff --git a/integration-libs/s4-service/checkout/core/connector/checkout-service-details.adapter.ts b/integration-libs/s4-service/checkout/core/connector/checkout-service-details.adapter.ts new file mode 100644 index 00000000000..18946f0892a --- /dev/null +++ b/integration-libs/s4-service/checkout/core/connector/checkout-service-details.adapter.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ServiceDetails } from '@spartacus/s4-service/root'; +import { Observable } from 'rxjs'; + +export abstract class CheckoutServiceDetailsAdapter { + /** + * Abstract method used to set service details to cart + * + * @param userId + * @param cartId + * @param scheduledAt : date and time of the service + */ + abstract setServiceScheduleSlot( + userId: string, + cartId: string, + scheduledAt: ServiceDetails + ): Observable; +} diff --git a/integration-libs/s4-service/checkout/core/connector/checkout-service-details.connector.spec.ts b/integration-libs/s4-service/checkout/core/connector/checkout-service-details.connector.spec.ts new file mode 100644 index 00000000000..52b4804045b --- /dev/null +++ b/integration-libs/s4-service/checkout/core/connector/checkout-service-details.connector.spec.ts @@ -0,0 +1,60 @@ +import { Type } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { CheckoutServiceDetailsAdapter } from './checkout-service-details.adapter'; +import { CheckoutServiceDetailsConnector } from './checkout-service-details.connector'; +import { ServiceDetails } from '@spartacus/s4-service/root'; +import createSpy = jasmine.createSpy; + +class MockServiceDetailsAdapter + implements Partial +{ + setServiceScheduleSlot = createSpy().and.returnValue(of([])); +} + +describe('CheckoutServiceDetailsConnector', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + let service: CheckoutServiceDetailsConnector; + let adapter: CheckoutServiceDetailsAdapter; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + CheckoutServiceDetailsConnector, + { + provide: CheckoutServiceDetailsAdapter, + useClass: MockServiceDetailsAdapter, + }, + ], + }); + + service = TestBed.inject( + CheckoutServiceDetailsConnector as Type + ); + adapter = TestBed.inject( + CheckoutServiceDetailsAdapter as Type + ); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('setServiceScheduleSlot should call adapter', () => { + service + .setServiceScheduleSlot( + 'userId', + 'cartId', + 'dd/mm/yyyy' as ServiceDetails + ) + .pipe(take(1)) + .subscribe(); + expect(adapter.setServiceScheduleSlot).toHaveBeenCalledWith( + 'userId', + 'cartId', + 'dd/mm/yyyy' as ServiceDetails + ); + }); +}); diff --git a/integration-libs/s4-service/checkout/core/connector/checkout-service-details.connector.ts b/integration-libs/s4-service/checkout/core/connector/checkout-service-details.connector.ts new file mode 100644 index 00000000000..092508854d0 --- /dev/null +++ b/integration-libs/s4-service/checkout/core/connector/checkout-service-details.connector.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable, inject } from '@angular/core'; +import { Observable } from 'rxjs'; +import { CheckoutServiceDetailsAdapter } from './checkout-service-details.adapter'; +import { ServiceDetails } from '@spartacus/s4-service/root'; + +@Injectable() +export class CheckoutServiceDetailsConnector { + protected adapter = inject(CheckoutServiceDetailsAdapter); + setServiceScheduleSlot( + userId: string, + cartId: string, + scheduledAt: ServiceDetails + ): Observable { + return this.adapter.setServiceScheduleSlot(userId, cartId, scheduledAt); + } +} diff --git a/integration-libs/s4-service/checkout/core/connector/index.ts b/integration-libs/s4-service/checkout/core/connector/index.ts new file mode 100644 index 00000000000..f6439411fb4 --- /dev/null +++ b/integration-libs/s4-service/checkout/core/connector/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './checkout-service-details.adapter'; +export * from './checkout-service-details.connector'; diff --git a/integration-libs/s4-service/checkout/core/facade/checkout-service-details.service.spec.ts b/integration-libs/s4-service/checkout/core/facade/checkout-service-details.service.spec.ts new file mode 100644 index 00000000000..8be6c8ff048 --- /dev/null +++ b/integration-libs/s4-service/checkout/core/facade/checkout-service-details.service.spec.ts @@ -0,0 +1,205 @@ +import { TestBed } from '@angular/core/testing'; +import { ActiveCartFacade } from '@spartacus/cart/base/root'; +import { + CheckoutQueryFacade, + CheckoutState, +} from '@spartacus/checkout/base/root'; +import { + EventService, + OCC_USER_ID_CURRENT, + QueryState, + UserIdService, +} from '@spartacus/core'; +import { of } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { CheckoutServiceDetailsConnector } from '../connector'; +import { CheckoutServiceDetailsService } from './checkout-service-details.service'; +import { + CheckoutServiceDetailsSetEvent, + ServiceDateTime, +} from '@spartacus/s4-service/root'; +import createSpy = jasmine.createSpy; + +const mockData = `2222-90-89T67:89:00-04:00`; +const mockUserId = OCC_USER_ID_CURRENT; +const mockCartId = 'cartID'; + +class MockActiveCartService implements Partial { + takeActiveCartId = createSpy().and.returnValue(of(mockCartId)); + isGuestCart = createSpy().and.returnValue(of(false)); + getEntries() { + return of([ + { + product: { + productTypes: 'NON-SERVICE', + name: 'non-service 1', + code: 'non-service 1', + }, + }, + { + product: { + productTypes: 'NON-SERVICE', + name: 'non-service 2', + code: 'non-service 2', + }, + }, + { + product: { + productTypes: 'SERVICE', + name: 'service 1', + code: 'service 1', + }, + }, + ]); + } +} + +class MockUserIdService implements Partial { + takeUserId() { + return of(mockUserId); + } +} + +class MockEventService implements Partial { + dispatch = createSpy(); +} + +class MockCheckoutServiceDetailsConnector + implements Partial +{ + setServiceScheduleSlot = createSpy().and.returnValue(of('service-details')); +} + +class MockCheckoutQueryFacade implements Partial { + getCheckoutDetailsState = createSpy().and.returnValue( + of({ loading: false, error: false, data: undefined }) + ); +} + +describe(`CheckoutServiceDetailsService`, () => { + let service: CheckoutServiceDetailsService; + let connector: CheckoutServiceDetailsConnector; + let checkoutQuery: CheckoutQueryFacade; + let eventService: EventService; + let userService: UserIdService; + let cartService: ActiveCartFacade; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + CheckoutServiceDetailsService, + { provide: ActiveCartFacade, useClass: MockActiveCartService }, + { provide: UserIdService, useClass: MockUserIdService }, + { provide: EventService, useClass: MockEventService }, + { + provide: CheckoutServiceDetailsConnector, + useClass: MockCheckoutServiceDetailsConnector, + }, + { provide: CheckoutQueryFacade, useClass: MockCheckoutQueryFacade }, + ], + }); + + service = TestBed.inject(CheckoutServiceDetailsService); + connector = TestBed.inject(CheckoutServiceDetailsConnector); + checkoutQuery = TestBed.inject(CheckoutQueryFacade); + userService = TestBed.inject(UserIdService); + eventService = TestBed.inject(EventService); + cartService = TestBed.inject(ActiveCartFacade); + }); + + it(`should be created`, () => { + expect(service).toBeTruthy(); + }); + describe(`getSelectedServiceDetailsState`, () => { + it(`should return the service detail`, (done) => { + checkoutQuery.getCheckoutDetailsState = createSpy().and.returnValue( + of(>{ + loading: false, + error: false, + data: { + servicedAt: mockData, + }, + }) + ); + + service + .getSelectedServiceDetailsState() + .pipe(take(1)) + .subscribe((result) => { + expect(result).toEqual(>{ + loading: false, + error: false, + data: mockData, + }); + done(); + }); + }); + it(`should return undefined service detail if no service products are available in cart`, (done) => { + checkoutQuery.getCheckoutDetailsState = createSpy().and.returnValue( + of(>{ + loading: false, + error: false, + data: { + servicedAt: mockData, + }, + }) + ); + spyOn(cartService, 'getEntries').and.returnValue(of([])); + + service + .getSelectedServiceDetailsState() + .pipe(take(1)) + .subscribe((result) => { + expect(result).toEqual(>{ + loading: false, + error: false, + data: undefined, + }); + done(); + }); + }); + }); + + describe(`setServiceScheduleSlot`, () => { + it(`should throw an error if the checkout condition is not met`, (done) => { + spyOn(userService, 'takeUserId').and.returnValue(of(undefined)); + service + .setServiceScheduleSlot(mockData) + .pipe(take(1)) + .subscribe({ + error: (error) => { + expect(error).toEqual(new Error('Checkout conditions not met')); + done(); + }, + }); + }); + + it(`should call checkoutServiceDetailsConnector.setServiceScheduleSlot`, () => { + service.setServiceScheduleSlot(mockData); + + expect(connector.setServiceScheduleSlot).toHaveBeenCalledWith( + mockUserId, + mockCartId, + { + scheduledAt: mockData, + } + ); + }); + + it(`should dispatch CheckoutServiceDetailsSetEvent`, () => { + service.setServiceScheduleSlot(mockData); + + expect(eventService.dispatch).toHaveBeenCalledWith( + { userId: mockUserId, cartId: mockCartId, scheduledAt: mockData }, + CheckoutServiceDetailsSetEvent + ); + }); + }); + it(`should return the service products if any`, (done) => { + spyOn(cartService, 'getEntries').and.callThrough(); + service.getServiceProducts().subscribe((result) => { + expect(result).toEqual(['service 1']); + done(); + }); + }); +}); diff --git a/integration-libs/s4-service/checkout/core/facade/checkout-service-details.service.ts b/integration-libs/s4-service/checkout/core/facade/checkout-service-details.service.ts new file mode 100644 index 00000000000..0a18fbe6e6a --- /dev/null +++ b/integration-libs/s4-service/checkout/core/facade/checkout-service-details.service.ts @@ -0,0 +1,123 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable, inject } from '@angular/core'; +import { ActiveCartFacade, OrderEntry } from '@spartacus/cart/base/root'; +import { CheckoutQueryFacade } from '@spartacus/checkout/base/root'; +import { + UserIdService, + CommandService, + EventService, + OCC_USER_ID_ANONYMOUS, + Command, + CommandStrategy, + QueryState, +} from '@spartacus/core'; +import { Observable, combineLatest, take, map, switchMap, tap } from 'rxjs'; +import { + CheckoutServiceDetailsFacade, + CheckoutServiceDetailsSetEvent, + ServiceDateTime, +} from '@spartacus/s4-service/root'; +import { CheckoutServiceDetailsConnector } from '../connector/checkout-service-details.connector'; + +@Injectable() +export class CheckoutServiceDetailsService + implements CheckoutServiceDetailsFacade +{ + protected activeCartFacade = inject(ActiveCartFacade); + protected userIdService = inject(UserIdService); + protected commandService = inject(CommandService); + protected serviceDetailsConnector = inject(CheckoutServiceDetailsConnector); + protected eventService = inject(EventService); + protected checkoutQueryFacade = inject(CheckoutQueryFacade); + + protected setServiceScheduleSlotCommand: Command = + this.commandService.create( + (scheduledAt) => + this.checkoutPreconditions().pipe( + switchMap(([userId, cartId]) => + this.serviceDetailsConnector + .setServiceScheduleSlot(userId, cartId, { + scheduledAt, + }) + .pipe( + tap(() => + this.eventService.dispatch( + { + userId, + cartId, + scheduledAt, + }, + CheckoutServiceDetailsSetEvent + ) + ) + ) + ) + ), + { + strategy: CommandStrategy.CancelPrevious, + } + ); + + protected checkoutPreconditions(): Observable<[string, string]> { + return combineLatest([ + this.userIdService.takeUserId(), + this.activeCartFacade.takeActiveCartId(), + this.activeCartFacade.isGuestCart(), + ]).pipe( + take(1), + map(([userId, cartId, isGuestCart]) => { + if ( + !userId || + !cartId || + (userId === OCC_USER_ID_ANONYMOUS && !isGuestCart) + ) { + throw new Error('Checkout conditions not met'); + } + return [userId, cartId]; + }) + ); + } + + setServiceScheduleSlot(scheduledAt: ServiceDateTime): Observable { + return this.setServiceScheduleSlotCommand.execute(scheduledAt); + } + + getSelectedServiceDetailsState(): Observable< + QueryState + > { + return this.checkoutQueryFacade.getCheckoutDetailsState().pipe( + switchMap((state) => { + return this.getServiceProducts().pipe( + map((products) => { + if (products.length > 0) { + return { ...state, data: state.data?.servicedAt }; + } else { + return { ...state, data: undefined }; + } + }) + ); + }) + ); + } + + getServiceProducts(): Observable { + return this.activeCartFacade.getEntries().pipe( + map((entries: OrderEntry[]) => { + return entries + .map((entry: OrderEntry) => { + if (entry.product?.productTypes === 'SERVICE') { + return entry.product?.code; + } else { + return ''; + } + }) + .filter((name): name is string => name !== ''); + }) + ); + } +} diff --git a/integration-libs/s4-service/checkout/core/facade/index.ts b/integration-libs/s4-service/checkout/core/facade/index.ts new file mode 100644 index 00000000000..84bcc317efd --- /dev/null +++ b/integration-libs/s4-service/checkout/core/facade/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './checkout-service-details.service'; diff --git a/integration-libs/s4-service/checkout/core/index.ts b/integration-libs/s4-service/checkout/core/index.ts new file mode 100644 index 00000000000..512e138f7b6 --- /dev/null +++ b/integration-libs/s4-service/checkout/core/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './connector/index'; +export * from './facade/index'; +export * from './s4-service-checkout-core.module'; diff --git a/integration-libs/s4-service/checkout/core/s4-service-checkout-core.module.ts b/integration-libs/s4-service/checkout/core/s4-service-checkout-core.module.ts new file mode 100644 index 00000000000..aab2c682d9d --- /dev/null +++ b/integration-libs/s4-service/checkout/core/s4-service-checkout-core.module.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { CxDatePipe } from '@spartacus/core'; +import { CheckoutServiceDetailsConnector } from './connector'; +import { CheckoutServiceDetailsService } from './facade'; +import { CheckoutServiceDetailsFacade } from '@spartacus/s4-service/root'; + +@NgModule({ + providers: [ + CheckoutServiceDetailsService, + { + provide: CheckoutServiceDetailsFacade, + useExisting: CheckoutServiceDetailsService, + }, + CheckoutServiceDetailsConnector, + CxDatePipe, + ], +}) +export class S4ServiceCheckoutCoreModule {} diff --git a/integration-libs/s4-service/checkout/ng-package.json b/integration-libs/s4-service/checkout/ng-package.json new file mode 100644 index 00000000000..38e01ac17de --- /dev/null +++ b/integration-libs/s4-service/checkout/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/integration-libs/s4-service/checkout/occ/adapter/index.ts b/integration-libs/s4-service/checkout/occ/adapter/index.ts new file mode 100644 index 00000000000..133877a6a77 --- /dev/null +++ b/integration-libs/s4-service/checkout/occ/adapter/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './occ-checkout-service-details.adapter'; diff --git a/integration-libs/s4-service/checkout/occ/adapter/occ-checkout-service-details.adapter.spec.ts b/integration-libs/s4-service/checkout/occ/adapter/occ-checkout-service-details.adapter.spec.ts new file mode 100644 index 00000000000..adc03cf0edb --- /dev/null +++ b/integration-libs/s4-service/checkout/occ/adapter/occ-checkout-service-details.adapter.spec.ts @@ -0,0 +1,75 @@ +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { LoggerService, OccEndpointsService } from '@spartacus/core'; +import { ServiceDetails } from '@spartacus/s4-service/root'; +import { OccCheckoutServiceDetailsAdapter } from './occ-checkout-service-details.adapter'; +import createSpy = jasmine.createSpy; +const mockUrl = + 'testUrl/setServiceScheduleSlot?userId=testUserId&cartId=testCartId'; +export class MockOccEndpointsService implements Partial { + buildUrl = createSpy().and.returnValue(mockUrl); +} +describe('OccCheckoutServiceDetailsAdapter', () => { + let adapter: OccCheckoutServiceDetailsAdapter; + let httpMock: HttpTestingController; + let loggerService: jasmine.SpyObj; + let occEndpointsService: jasmine.SpyObj; + + const mockServiceDetails: ServiceDetails = { + scheduledAt: '2021-12-31T23:59:59Z', + }; + + const userId = 'user123'; + const cartId = 'cart456'; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + OccCheckoutServiceDetailsAdapter, + { provide: OccEndpointsService, useClass: MockOccEndpointsService }, + { provide: LoggerService, useValue: loggerService }, + ], + }); + adapter = TestBed.inject(OccCheckoutServiceDetailsAdapter); + httpMock = TestBed.inject(HttpTestingController); + loggerService = jasmine.createSpyObj('LoggerService', ['error']); + occEndpointsService = jasmine.createSpyObj('OccEndpointsService', [ + 'buildUrl', + ]); + occEndpointsService.buildUrl.and.returnValue(mockUrl); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should send a PATCH request to the correct URL with the correct headers', () => { + adapter + .setServiceScheduleSlot(userId, cartId, mockServiceDetails) + .subscribe(); + const req = httpMock.expectOne(mockUrl); + expect(req.request.method).toBe('PATCH'); + expect(req.request.headers.get('Content-Type')).toBe('application/json'); + req.flush({}); + }); + + it('should handle errors and call normalizeHttpError', () => { + const mockError = { + status: 500, + statusText: 'We are getting an internal server error', + }; + adapter + .setServiceScheduleSlot(userId, cartId, mockServiceDetails) + .subscribe({ + error: (error) => { + expect(error.statusText).toEqual(mockError.statusText); + }, + }); + const req = httpMock.expectOne(mockUrl); + req.flush(mockError, mockError); + }); +}); diff --git a/integration-libs/s4-service/checkout/occ/adapter/occ-checkout-service-details.adapter.ts b/integration-libs/s4-service/checkout/occ/adapter/occ-checkout-service-details.adapter.ts new file mode 100644 index 00000000000..6fb839a319a --- /dev/null +++ b/integration-libs/s4-service/checkout/occ/adapter/occ-checkout-service-details.adapter.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable, inject } from '@angular/core'; +import { + LoggerService, + OccEndpointsService, + normalizeHttpError, +} from '@spartacus/core'; +import { ServiceDetails } from '@spartacus/s4-service/root'; +import { Observable, catchError } from 'rxjs'; +import { CheckoutServiceDetailsAdapter } from '../../core/connector/checkout-service-details.adapter'; + +const CONTENT_TYPE_JSON_HEADER = { 'Content-Type': 'application/json' }; + +@Injectable() +export class OccCheckoutServiceDetailsAdapter + implements CheckoutServiceDetailsAdapter +{ + protected http = inject(HttpClient); + protected logger = inject(LoggerService); + protected occEndpoints = inject(OccEndpointsService); + setServiceScheduleSlot( + userId: string, + cartId: string, + scheduledAt: ServiceDetails + ): Observable { + const url = this.occEndpoints.buildUrl('setServiceScheduleSlot', { + urlParams: { userId, cartId }, + }); + const headers = new HttpHeaders({ + ...CONTENT_TYPE_JSON_HEADER, + }); + //check if we need to add serializer and normalizer. + return this.http.patch(url, scheduledAt, { headers }).pipe( + catchError((error: any) => { + throw normalizeHttpError(error, this.logger); + }) + ); + } +} diff --git a/integration-libs/s4-service/checkout/occ/config/default-occ-checkout-s4-service-config.ts b/integration-libs/s4-service/checkout/occ/config/default-occ-checkout-s4-service-config.ts new file mode 100644 index 00000000000..3af2881fcc2 --- /dev/null +++ b/integration-libs/s4-service/checkout/occ/config/default-occ-checkout-s4-service-config.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CheckoutOccEndpoints } from '@spartacus/checkout/base/occ'; +import { OccConfig } from '@spartacus/core'; + +const defaultServiceOrderCheckoutDetailsOccEndpoint: CheckoutOccEndpoints = { + getCheckoutDetails: + 'users/${userId}/carts/${cartId}?fields=deliveryAddress(FULL),deliveryMode(FULL),paymentInfo(FULL),costCenter(FULL),purchaseOrderNumber,paymentType(FULL),servicedAt', +}; + +export const defaultOccCheckoutServiceOrderConfig: OccConfig = { + backend: { + occ: { + endpoints: { + ...defaultServiceOrderCheckoutDetailsOccEndpoint, + setServiceScheduleSlot: + 'users/${userId}/carts/${cartId}/serviceOrder/serviceScheduleSlot', + }, + }, + }, +}; diff --git a/integration-libs/s4-service/checkout/occ/index.ts b/integration-libs/s4-service/checkout/occ/index.ts new file mode 100644 index 00000000000..55dee8cce10 --- /dev/null +++ b/integration-libs/s4-service/checkout/occ/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './s4-service-checkout-occ.module'; +export * from './adapter/index'; diff --git a/integration-libs/s4-service/checkout/occ/s4-service-checkout-occ.module.ts b/integration-libs/s4-service/checkout/occ/s4-service-checkout-occ.module.ts new file mode 100644 index 00000000000..82473a711d8 --- /dev/null +++ b/integration-libs/s4-service/checkout/occ/s4-service-checkout-occ.module.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { provideDefaultConfig } from '@spartacus/core'; +import { CheckoutServiceDetailsAdapter } from '../core/connector'; +import { OccCheckoutServiceDetailsAdapter } from './adapter'; +import { defaultOccCheckoutServiceOrderConfig } from './config/default-occ-checkout-s4-service-config'; + +@NgModule({ + providers: [ + provideDefaultConfig(defaultOccCheckoutServiceOrderConfig), + { + provide: CheckoutServiceDetailsAdapter, + useClass: OccCheckoutServiceDetailsAdapter, + }, + ], +}) +export class S4ServiceCheckoutOccModule {} diff --git a/integration-libs/s4-service/checkout/public_api.ts b/integration-libs/s4-service/checkout/public_api.ts new file mode 100644 index 00000000000..b7d49ca8afa --- /dev/null +++ b/integration-libs/s4-service/checkout/public_api.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './s4-service-checkout.module'; +export * from './components/index'; +export * from './core/index'; +export * from './occ/index'; diff --git a/integration-libs/s4-service/checkout/s4-service-checkout.module.ts b/integration-libs/s4-service/checkout/s4-service-checkout.module.ts new file mode 100644 index 00000000000..33499e28ef2 --- /dev/null +++ b/integration-libs/s4-service/checkout/s4-service-checkout.module.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { S4ServiceCheckoutComponentModule } from './components/s4-service-checkout-component.module'; +import { S4ServiceCheckoutCoreModule } from './core/s4-service-checkout-core.module'; +import { S4ServiceCheckoutOccModule } from './occ/s4-service-checkout-occ.module'; + +@NgModule({ + imports: [ + S4ServiceCheckoutComponentModule, + S4ServiceCheckoutCoreModule, + S4ServiceCheckoutOccModule, + ], +}) +export class S4ServiceCheckoutModule {} diff --git a/integration-libs/s4-service/jest.schematics.config.js b/integration-libs/s4-service/jest.schematics.config.js new file mode 100644 index 00000000000..ec05fc49003 --- /dev/null +++ b/integration-libs/s4-service/jest.schematics.config.js @@ -0,0 +1,34 @@ +const { pathsToModuleNameMapper } = require('ts-jest'); +const { compilerOptions } = require('./tsconfig.schematics.json'); +const { defaultTransformerOptions } = require('jest-preset-angular/presets'); + +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ +module.exports = { + preset: 'jest-preset-angular', + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, { + prefix: '/', + }), + setupFilesAfterEnv: ['/setup-jest.ts'], + testMatch: ['**/+(*_)+(spec).+(ts)'], + transform: { + '^.+\\.(ts|js|mjs|html|svg)$': [ + 'jest-preset-angular', + { + ...defaultTransformerOptions, + tsconfig: '/tsconfig.schematics.json', + }, + ], + }, + + collectCoverage: false, + coverageReporters: ['json', 'lcov', 'text', 'clover'], + coverageDirectory: '/../../coverage/s4-service/schematics', + coverageThreshold: { + global: { + statements: 90, + branches: 90, + functions: 90, + lines: 90, + }, + }, +}; diff --git a/integration-libs/s4-service/karma.conf.js b/integration-libs/s4-service/karma.conf.js new file mode 100644 index 00000000000..49199625186 --- /dev/null +++ b/integration-libs/s4-service/karma.conf.js @@ -0,0 +1,49 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-coverage'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('@angular-devkit/build-angular/plugins/karma'), + require('karma-junit-reporter'), + ], + client: { + clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active + }, + reporters: ['progress', 'kjhtml', 'dots', 'junit'], + junitReporter: { + outputFile: 'unit-test-s4-service.xml', + outputDir: require('path').join(__dirname, '../../unit-tests-reports'), + useBrowserName: false, + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/s4-service'), + reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], + check: { + global: { + statements: 80, + lines: 80, + branches: 70, + functions: 70, + }, + }, + }, + captureTimeout: 210000, + browserDisconnectTolerance: 3, + browserDisconnectTimeout: 210000, + browserNoActivityTimeout: 210000, + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true, + }); +}; diff --git a/integration-libs/s4-service/ng-package.json b/integration-libs/s4-service/ng-package.json new file mode 100644 index 00000000000..1f94b8e5ab8 --- /dev/null +++ b/integration-libs/s4-service/ng-package.json @@ -0,0 +1,8 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/s4-service", + "lib": { + "entryFile": "./public_api.ts" + }, + "assets": ["**/*.scss", "schematics/**/*.json", "schematics/**/*.js"] +} diff --git a/integration-libs/s4-service/order/components/index.ts b/integration-libs/s4-service/order/components/index.ts new file mode 100644 index 00000000000..419f36c9d9e --- /dev/null +++ b/integration-libs/s4-service/order/components/index.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './s4-service-components.module'; + +export * from './order-summary/service-details-card.component'; +export * from './order-summary/service-details-card.module'; diff --git a/integration-libs/s4-service/order/components/order-summary/service-details-card.component.html b/integration-libs/s4-service/order/components/order-summary/service-details-card.component.html new file mode 100644 index 00000000000..797bd05afc5 --- /dev/null +++ b/integration-libs/s4-service/order/components/order-summary/service-details-card.component.html @@ -0,0 +1,5 @@ + + + diff --git a/integration-libs/s4-service/order/components/order-summary/service-details-card.component.spec.ts b/integration-libs/s4-service/order/components/order-summary/service-details-card.component.spec.ts new file mode 100644 index 00000000000..a0aab7d7f4c --- /dev/null +++ b/integration-libs/s4-service/order/components/order-summary/service-details-card.component.spec.ts @@ -0,0 +1,86 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslationService } from '@spartacus/core'; +import { CheckoutServiceSchedulePickerService } from '@spartacus/s4-service/root'; +import { OutletContextData } from '@spartacus/storefront'; +import { ServiceDetailsCardComponent } from './service-details-card.component'; +import { of } from 'rxjs'; + +class MockTranslationService { + translate() {} +} +class MockCheckoutServiceSchedulePickerService { + convertDateTimeToReadableString() {} +} + +describe('ServiceDetailsCardComponent', () => { + let component: ServiceDetailsCardComponent; + let fixture: ComponentFixture; + let translateService: TranslationService; + let pickerService: CheckoutServiceSchedulePickerService; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ServiceDetailsCardComponent], + providers: [ + { provide: TranslationService, useClass: MockTranslationService }, + { provide: CheckoutServiceSchedulePickerService, useValue: {} }, + { provide: OutletContextData, useValue: {} }, + { + provide: CheckoutServiceSchedulePickerService, + useClass: MockCheckoutServiceSchedulePickerService, + }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(ServiceDetailsCardComponent); + component = fixture.componentInstance; + translateService = TestBed.inject(TranslationService); + pickerService = TestBed.inject(CheckoutServiceSchedulePickerService); + spyOn(translateService, 'translate') + .withArgs('serviceOrderCheckout.serviceDetails') + .and.returnValue(of('card title')) + .withArgs('serviceOrderCheckout.cardLabel') + .and.returnValue(of('card bold text')) + .withArgs('serviceOrderCheckout.emptyServiceDetailsCard') + .and.returnValue(of('card empty')); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should return card with details', () => { + spyOn(pickerService, 'convertDateTimeToReadableString').and.returnValue( + '2023/12/12, 12:00' + ); + component.getServiceDetailsCard('2023/12/12TY12:00').subscribe((card) => { + expect(card).toEqual({ + title: 'card title', + textBold: 'card bold text', + text: ['2023/12/12, 12:00'], + }); + }); + }); + it('should return card with details', () => { + component.getServiceDetailsCard(undefined).subscribe((card) => { + expect(card).toEqual({ + title: 'card title', + text: ['card empty'], + }); + }); + }); + + it('should call ngOnDestroy', () => { + spyOn(component['subscription'], 'unsubscribe'); + component.ngOnDestroy(); + expect(component['subscription'].unsubscribe).toHaveBeenCalled(); + }); + + it('should set order property when context changes', () => { + const order = { code: '123', servicedAt: '2023/12/12TY12:00' } as any; + component['orderOutlet'] = { + context$: of({ item: order }), + } as OutletContextData; + component.ngOnInit(); + expect(component.order).toEqual(order); + }); +}); diff --git a/integration-libs/s4-service/order/components/order-summary/service-details-card.component.ts b/integration-libs/s4-service/order/components/order-summary/service-details-card.component.ts new file mode 100644 index 00000000000..23a6ec7d119 --- /dev/null +++ b/integration-libs/s4-service/order/components/order-summary/service-details-card.component.ts @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, OnDestroy, OnInit, Optional, inject } from '@angular/core'; +import { TranslationService } from '@spartacus/core'; +import { Order } from '@spartacus/order/root'; +import { + ServiceDateTime, + CheckoutServiceSchedulePickerService, +} from '@spartacus/s4-service/root'; +import { Card, OutletContextData } from '@spartacus/storefront'; +import { Observable, Subscription, combineLatest, map } from 'rxjs'; + +@Component({ + selector: 'cx-card-service-details', + templateUrl: './service-details-card.component.html', +}) +export class ServiceDetailsCardComponent implements OnInit, OnDestroy { + protected translationService = inject(TranslationService); + protected checkoutServiceSchedulePickerService = inject( + CheckoutServiceSchedulePickerService + ); + @Optional() protected orderOutlet = inject(OutletContextData); + protected subscription = new Subscription(); + order: Order; + ngOnInit(): void { + if (this.orderOutlet?.context$) { + this.subscription.add( + this.orderOutlet.context$.subscribe( + (context) => (this.order = context?.item) + ) + ); + } + } + + getServiceDetailsCard( + servicedAt: ServiceDateTime | undefined + ): Observable { + const titleTranslation$ = this.translationService.translate( + 'serviceOrderCheckout.serviceDetails' + ); + if (servicedAt) { + const labelTranslation$ = this.translationService.translate( + 'serviceOrderCheckout.cardLabel' + ); + return combineLatest([titleTranslation$, labelTranslation$]).pipe( + map(([textTitle, textLabel]) => { + const text = + this.checkoutServiceSchedulePickerService.convertDateTimeToReadableString( + servicedAt ?? '' + ); + return { + title: textTitle, + textBold: textLabel, + text: [text], + }; + }) + ); + } else { + const emptyTextTranslation$ = this.translationService.translate( + 'serviceOrderCheckout.emptyServiceDetailsCard' + ); + return combineLatest([titleTranslation$, emptyTextTranslation$]).pipe( + map(([textTitle, text]) => { + return { + title: textTitle, + text: [text], + }; + }) + ); + } + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} diff --git a/integration-libs/s4-service/order/components/order-summary/service-details-card.module.ts b/integration-libs/s4-service/order/components/order-summary/service-details-card.module.ts new file mode 100644 index 00000000000..1cee6d12b5b --- /dev/null +++ b/integration-libs/s4-service/order/components/order-summary/service-details-card.module.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { CardModule, provideOutlet } from '@spartacus/storefront'; +import { CommonModule } from '@angular/common'; +import { I18nModule } from '@spartacus/core'; +import { ServiceDetailsCardComponent } from './service-details-card.component'; +import { OrderOutlets } from '@spartacus/order/root'; + +@NgModule({ + imports: [CardModule, CommonModule, I18nModule], + providers: [ + provideOutlet({ + id: OrderOutlets.SERVICE_DETAILS, + component: ServiceDetailsCardComponent, + }), + ], + exports: [ServiceDetailsCardComponent], + declarations: [ServiceDetailsCardComponent], +}) +export class ServiceDetailsCardModule {} diff --git a/integration-libs/s4-service/order/components/s4-service-components.module.ts b/integration-libs/s4-service/order/components/s4-service-components.module.ts new file mode 100644 index 00000000000..38b17355ed0 --- /dev/null +++ b/integration-libs/s4-service/order/components/s4-service-components.module.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { ServiceDetailsCardModule } from './order-summary/service-details-card.module'; +@NgModule({ + imports: [ServiceDetailsCardModule], +}) +export class S4ServiceComponentsModule {} diff --git a/integration-libs/s4-service/order/ng-package.json b/integration-libs/s4-service/order/ng-package.json new file mode 100644 index 00000000000..38e01ac17de --- /dev/null +++ b/integration-libs/s4-service/order/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/integration-libs/s4-service/order/public_api.ts b/integration-libs/s4-service/order/public_api.ts new file mode 100644 index 00000000000..3951d1b34d9 --- /dev/null +++ b/integration-libs/s4-service/order/public_api.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './s4-service-order.module'; +export * from './components/index'; diff --git a/integration-libs/s4-service/order/s4-service-order.module.ts b/integration-libs/s4-service/order/s4-service-order.module.ts new file mode 100644 index 00000000000..faa4ce787ce --- /dev/null +++ b/integration-libs/s4-service/order/s4-service-order.module.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { S4ServiceComponentsModule } from './components/s4-service-components.module'; + +@NgModule({ + imports: [S4ServiceComponentsModule], +}) +export class S4ServiceOrderModule {} diff --git a/integration-libs/s4-service/package.json b/integration-libs/s4-service/package.json new file mode 100644 index 00000000000..a6e6407e7aa --- /dev/null +++ b/integration-libs/s4-service/package.json @@ -0,0 +1,45 @@ +{ + "name": "@spartacus/s4-service", + "version": "2211.25.1", + "description": "S/4HANA Service Integration Integration library for Spartacus", + "keywords": [ + "spartacus", + "framework", + "storefront", + "S/4HANA Service Integration" + ], + "homepage": "https://github.com/SAP/spartacus", + "repository": "https://github.com/SAP/spartacus/tree/develop/integration-libs/s4-service", + "license": "Apache-2.0", + "exports": { + ".": { + "sass": "./_index.scss" + } + }, + "scripts": { + "build:schematics": "npm run clean:schematics && ../../node_modules/.bin/tsc -p ./tsconfig.schematics.json", + "clean:schematics": "../../node_modules/.bin/rimraf --glob \"schematics/**/*.js\" \"schematics/**/*.js.map\" \"schematics/**/*.d.ts\"", + "test:schematics": "npm --prefix ../../projects/schematics/ run clean && npm run clean:schematics && ../../node_modules/.bin/jest --config ./jest.schematics.config.js" + }, + "dependencies": { + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@angular-devkit/schematics": "^17.0.5", + "@angular/common": "^17.0.5", + "@angular/core": "^17.0.5", + "@angular/forms": "^17.0.5", + "@angular/router": "^17.0.5", + "@spartacus/cart": "2211.25.1", + "@spartacus/checkout": "2211.25.1", + "@spartacus/core": "2211.25.1", + "@spartacus/order": "2211.25.1", + "@spartacus/schematics": "2211.25.1", + "@spartacus/storefront": "2211.25.1", + "rxjs": "^7.8.0" + }, + "publishConfig": { + "access": "public" + }, + "schematics": "./schematics/collection.json" +} diff --git a/integration-libs/s4-service/project.json b/integration-libs/s4-service/project.json new file mode 100644 index 00000000000..db117d72491 --- /dev/null +++ b/integration-libs/s4-service/project.json @@ -0,0 +1,47 @@ +{ + "name": "s4-service", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "integration-libs/s4-service", + "prefix": "cx", + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:ng-packagr", + "options": { + "tsConfig": "integration-libs/s4-service/tsconfig.lib.json", + "project": "integration-libs/s4-service/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "integration-libs/s4-service/tsconfig.lib.prod.json" + } + } + }, + "test": { + "executor": "@angular-devkit/build-angular:karma", + "options": { + "main": "integration-libs/s4-service/test.ts", + "tsConfig": "integration-libs/s4-service/tsconfig.spec.json", + "polyfills": ["zone.js", "zone.js/testing"], + "karmaConfig": "integration-libs/s4-service/karma.conf.js" + } + }, + "test-jest": { + "executor": "nx:run-commands", + "options": { + "command": "npm run test:schematics", + "cwd": "integration-libs/s4-service" + } + }, + "lint": { + "executor": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "integration-libs/s4-service/**/*.ts", + "integration-libs/s4-service/**/*.html" + ] + } + } + }, + "tags": ["type:feature", "type:integration"] +} diff --git a/integration-libs/s4-service/public_api.ts b/integration-libs/s4-service/public_api.ts new file mode 100644 index 00000000000..3cf6a3279d3 --- /dev/null +++ b/integration-libs/s4-service/public_api.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './s4-service.module'; diff --git a/integration-libs/s4-service/root/config/default-checkout-service-details-routing-config.ts b/integration-libs/s4-service/root/config/default-checkout-service-details-routing-config.ts new file mode 100644 index 00000000000..1d18bcb4851 --- /dev/null +++ b/integration-libs/s4-service/root/config/default-checkout-service-details-routing-config.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RoutingConfig } from '@spartacus/core'; + +export const defaultCheckoutServiceDetailsRoutingConfig: RoutingConfig = { + routing: { + routes: { + checkoutServiceDetails: { paths: ['checkout/service-details'] }, + }, + }, +}; diff --git a/integration-libs/s4-service/root/config/default-service-details-checkout-config.ts b/integration-libs/s4-service/root/config/default-service-details-checkout-config.ts new file mode 100644 index 00000000000..bec851b7f8f --- /dev/null +++ b/integration-libs/s4-service/root/config/default-service-details-checkout-config.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + CheckoutConfig, + CheckoutStepType, + DeliveryModePreferences, +} from '@spartacus/checkout/base/root'; + +export const defaultServiceDetailsCheckoutConfig: CheckoutConfig = { + checkout: { + steps: [ + { + id: 'paymentType', + name: 'checkoutB2B.progress.methodOfPayment', + routeName: 'checkoutPaymentType', + type: [CheckoutStepType.PAYMENT_TYPE], + }, + { + id: 'deliveryAddress', + name: 'checkoutProgress.deliveryAddress', + routeName: 'checkoutDeliveryAddress', + type: [CheckoutStepType.DELIVERY_ADDRESS], + }, + { + id: 'deliveryMode', + name: 'checkoutProgress.deliveryMode', + routeName: 'checkoutDeliveryMode', + type: [CheckoutStepType.DELIVERY_MODE], + }, + { + id: 'paymentDetails', + name: 'checkoutProgress.paymentDetails', + routeName: 'checkoutPaymentDetails', + type: [CheckoutStepType.PAYMENT_DETAILS], + }, + { + id: 'serviceDetails', + name: 'serviceOrderCheckout.serviceDetails', + routeName: 'checkoutServiceDetails', + type: [CheckoutStepType.SERVICE_DETAILS], + }, + { + id: 'reviewOrder', + name: 'checkoutProgress.reviewOrder', + routeName: 'checkoutReviewOrder', + type: [CheckoutStepType.REVIEW_ORDER], + }, + ], + express: false, + defaultDeliveryMode: [DeliveryModePreferences.FREE], + guest: false, + }, +}; diff --git a/integration-libs/s4-service/root/config/index.ts b/integration-libs/s4-service/root/config/index.ts new file mode 100644 index 00000000000..c4cc9152dec --- /dev/null +++ b/integration-libs/s4-service/root/config/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './default-checkout-service-details-routing-config'; +export * from './default-service-details-checkout-config'; diff --git a/integration-libs/s4-service/root/events/checkout-service-details-event.listener.spec.ts b/integration-libs/s4-service/root/events/checkout-service-details-event.listener.spec.ts new file mode 100644 index 00000000000..2cda6b8d8c1 --- /dev/null +++ b/integration-libs/s4-service/root/events/checkout-service-details-event.listener.spec.ts @@ -0,0 +1,57 @@ +import { TestBed } from '@angular/core/testing'; +import { ActiveCartFacade } from '@spartacus/cart/base/root'; +import { createFrom, CxEvent, EventService } from '@spartacus/core'; +import { Subject } from 'rxjs'; + +import createSpy = jasmine.createSpy; +import { CheckoutQueryResetEvent } from '@spartacus/checkout/base/root'; +import { CheckoutServiceDetailsEventListener } from './checkout-service-details-event.listener'; +import { CheckoutServiceDetailsSetEvent } from './checkout-service-details.events'; + +const mockUserId = 'test-user-id'; +const mockCartId = 'test-cart-id'; +const mockData = 'test-schedule-detail'; + +const mockEventStream$ = new Subject(); + +class MockEventService implements Partial { + get = createSpy().and.returnValue(mockEventStream$.asObservable()); + dispatch = createSpy(); +} + +describe(`CheckoutServiceDetailsEventListener`, () => { + let eventService: EventService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + CheckoutServiceDetailsEventListener, + { + provide: EventService, + useClass: MockEventService, + }, + ], + }); + + TestBed.inject(CheckoutServiceDetailsEventListener); + eventService = TestBed.inject(EventService); + TestBed.inject(ActiveCartFacade); + }); + beforeEach(() => { + mockEventStream$.next( + createFrom(CheckoutServiceDetailsSetEvent, { + userId: mockUserId, + cartId: mockCartId, + cartCode: mockCartId, + scheduledAt: mockData, + }) + ); + }); + + it(`should dispatch CheckoutQueryResetEvent when service details is set`, () => { + expect(eventService.dispatch).toHaveBeenCalledWith( + {}, + CheckoutQueryResetEvent + ); + }); +}); diff --git a/integration-libs/s4-service/root/events/checkout-service-details-event.listener.ts b/integration-libs/s4-service/root/events/checkout-service-details-event.listener.ts new file mode 100644 index 00000000000..2c079df4d2b --- /dev/null +++ b/integration-libs/s4-service/root/events/checkout-service-details-event.listener.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable, OnDestroy } from '@angular/core'; +import { EventService } from '@spartacus/core'; +import { Subscription } from 'rxjs'; +import { CheckoutServiceDetailsSetEvent } from './checkout-service-details.events'; +import { CheckoutQueryResetEvent } from '@spartacus/checkout/base/root'; + +@Injectable({ + providedIn: 'root', +}) +export class CheckoutServiceDetailsEventListener implements OnDestroy { + protected subscriptions = new Subscription(); + + constructor(protected eventService: EventService) { + this.onServiceDetailsSet(); + } + + protected onServiceDetailsSet(): void { + this.subscriptions.add( + this.eventService.get(CheckoutServiceDetailsSetEvent).subscribe(() => { + this.eventService.dispatch({}, CheckoutQueryResetEvent); + }) + ); + } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } +} diff --git a/integration-libs/s4-service/root/events/checkout-service-details-event.module.ts b/integration-libs/s4-service/root/events/checkout-service-details-event.module.ts new file mode 100644 index 00000000000..73b31732a5b --- /dev/null +++ b/integration-libs/s4-service/root/events/checkout-service-details-event.module.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { CheckoutServiceDetailsEventListener } from './checkout-service-details-event.listener'; + +@NgModule({}) +export class CheckoutServiceDetailsEventModule { + constructor( + _checkoutServiceDetailsEventListener: CheckoutServiceDetailsEventListener + ) { + // Intentional empty constructor + } +} diff --git a/integration-libs/s4-service/root/events/checkout-service-details.events.ts b/integration-libs/s4-service/root/events/checkout-service-details.events.ts new file mode 100644 index 00000000000..ebfe7d34276 --- /dev/null +++ b/integration-libs/s4-service/root/events/checkout-service-details.events.ts @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CheckoutEvent } from '@spartacus/checkout/base/root'; +import { ServiceDateTime } from '../model/checkout-service-details.model'; + +/** + * An abstract event for all the service details related events. + */ +export abstract class CheckoutServiceDetailsEvent extends CheckoutEvent {} + +/** + * Fired when the service details has been set + */ +export class CheckoutServiceDetailsSetEvent extends CheckoutServiceDetailsEvent { + /** + * Event's type + */ + static readonly type = 'CheckoutServiceDetailsSetEvent'; + + /** + * scheduled time and date + */ + scheduledAt?: ServiceDateTime; +} diff --git a/integration-libs/s4-service/root/events/index.ts b/integration-libs/s4-service/root/events/index.ts new file mode 100644 index 00000000000..824041a7c74 --- /dev/null +++ b/integration-libs/s4-service/root/events/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './checkout-service-details-event.listener'; +export * from './checkout-service-details-event.module'; +export * from './checkout-service-details.events'; diff --git a/integration-libs/s4-service/root/facade/checkout-service-details.facade.ts b/integration-libs/s4-service/root/facade/checkout-service-details.facade.ts new file mode 100644 index 00000000000..190cc64d4ba --- /dev/null +++ b/integration-libs/s4-service/root/facade/checkout-service-details.facade.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { CHECKOUT_CORE_FEATURE } from '@spartacus/checkout/base/root'; +import { QueryState, facadeFactory } from '@spartacus/core'; +import { Observable } from 'rxjs'; +import { ServiceDateTime } from '../model/checkout-service-details.model'; + +@Injectable({ + providedIn: 'root', + useFactory: () => + facadeFactory({ + facade: CheckoutServiceDetailsFacade, + feature: CHECKOUT_CORE_FEATURE, + methods: [ + 'setServiceScheduleSlot', + 'getSelectedServiceDetailsState', + 'getServiceProducts', + ], + }), +}) +export abstract class CheckoutServiceDetailsFacade { + /** + * Set service schedule DateTime for the cart + */ + abstract setServiceScheduleSlot( + scheduledAt: ServiceDateTime + ): Observable; + + /** + * Get the selected scheduled DateTime + */ + abstract getSelectedServiceDetailsState(): Observable< + QueryState + >; + + /** + * Get the name of products of type SERVICE in the active cart + */ + abstract getServiceProducts(): Observable; +} diff --git a/integration-libs/s4-service/root/facade/checkout-service-schedule-picker.service.spec.ts b/integration-libs/s4-service/root/facade/checkout-service-schedule-picker.service.spec.ts new file mode 100644 index 00000000000..275b606e530 --- /dev/null +++ b/integration-libs/s4-service/root/facade/checkout-service-schedule-picker.service.spec.ts @@ -0,0 +1,107 @@ +import { TestBed } from '@angular/core/testing'; +import { CheckoutServiceSchedulePickerService } from './checkout-service-schedule-picker.service'; +import { + BaseSiteService, + CxDatePipe, + I18nTestingModule, + LanguageService, + TimeUtils, + TranslationService, +} from '@spartacus/core'; +import { of } from 'rxjs'; +import createSpy = jasmine.createSpy; +const mockLanguageService = { + getActive: () => {}, +}; +const mockBaseSite = { + baseStore: { + serviceOrderConfiguration: { + leadDays: 3, + serviceScheduleTimes: ['08:00', '12:00', '16:00'], + }, + }, +}; + +const translationServiceMock = { + translate: jasmine + .createSpy('translate') + .and.returnValue(of('Delivery Date')), +}; + +class MockBaseSiteService implements Partial { + get = createSpy().and.returnValue(of(mockBaseSite)); +} +class MockCxDatePipe { + transform(value: any, format?: string): string | null { + return value instanceof Date && format === 'yyyy-MM-dd' + ? '2024-07-11' + : null; + } +} +describe('CheckoutServiceSchedulePickerService', () => { + let service: CheckoutServiceSchedulePickerService; + let pipe: CxDatePipe; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [I18nTestingModule], + providers: [ + CheckoutServiceSchedulePickerService, + { provide: BaseSiteService, useClass: MockBaseSiteService }, + { provide: CxDatePipe, useClass: MockCxDatePipe }, + { provide: LanguageService, useValue: mockLanguageService }, + { provide: TranslationService, useValue: translationServiceMock }, + ], + }); + + service = TestBed.inject(CheckoutServiceSchedulePickerService); + pipe = TestBed.inject(CxDatePipe); + }); + + it('should return minimum date for service scheduling', (done) => { + spyOn(service, 'getServiceOrderConfiguration').and.callThrough(); + spyOn(pipe, 'transform').and.callThrough(); + service.getMinDateForService().subscribe((result) => { + expect(service.getServiceOrderConfiguration).toHaveBeenCalled(); + expect(pipe.transform).toHaveBeenCalled(); + expect(result).toEqual('2024-07-11'); + done(); + }); + }); + + it('should return the scheduled service times from the configuration', (done) => { + spyOn(service, 'getServiceOrderConfiguration').and.callThrough(); + service.getScheduledServiceTimes().subscribe((result) => { + expect(service.getServiceOrderConfiguration).toHaveBeenCalled(); + expect(result).toEqual( + mockBaseSite.baseStore.serviceOrderConfiguration.serviceScheduleTimes + ); + done(); + }); + }); + + it('should convert date and time to a DateTime string with timezone offset', () => { + spyOn(TimeUtils, 'getLocalTimezoneOffset').and.returnValue('+05:30'); + const result = service.convertToDateTime('2024-07-11', '14:30'); + expect(result).toEqual('2024-07-11T14:30:00+05:30'); + }); + + it('should convert dateTime string to a readable string', () => { + const dateTime = '2024-07-11T14:30:00+05:30'; + const date = new Date(dateTime); + const result = service.convertDateTimeToReadableString(dateTime); + expect(result).toEqual(date.toLocaleString().slice(0, -3)); + }); + + it('should convert dateTime string to an object with separate date and time properties', () => { + spyOn(pipe, 'transform').and.callThrough(); + const result = service.getServiceDetailsFromDateTime('11/07/2024, 14:30'); + expect(result).toEqual({ date: '2024-07-11', time: '14:30' }); + }); + it('should return service order configuration', (done) => { + service.getServiceOrderConfiguration().subscribe((result) => { + expect(result).toEqual(mockBaseSite.baseStore.serviceOrderConfiguration); + done(); + }); + }); +}); diff --git a/integration-libs/s4-service/root/facade/checkout-service-schedule-picker.service.ts b/integration-libs/s4-service/root/facade/checkout-service-schedule-picker.service.ts new file mode 100644 index 00000000000..44d4b2a46e8 --- /dev/null +++ b/integration-libs/s4-service/root/facade/checkout-service-schedule-picker.service.ts @@ -0,0 +1,107 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable, inject } from '@angular/core'; +import { BaseSiteService, CxDatePipe, TimeUtils } from '@spartacus/core'; +import { map, take } from 'rxjs/operators'; +import { ServiceOrderConfig } from '../model/checkout-service-details.model'; +import { Observable } from 'rxjs'; + +const dateFormat = 'yyyy-MM-dd'; + +@Injectable() +export class CheckoutServiceSchedulePickerService { + protected baseSiteService = inject(BaseSiteService); + protected datePipe = inject(CxDatePipe); + + /** + * Returns the minimum date for scheduling a service. + * It is the current date + lead days from the base site configuration. + */ + getMinDateForService(): Observable { + return this.getServiceOrderConfiguration().pipe( + map((config) => { + const minDate = new Date(); + minDate.setDate(minDate.getDate() + (config?.leadDays ?? 0) + 1); + return this.datePipe.transform(minDate, dateFormat) ?? ''; + }) + ); + } + + /** + * Returns an array of service schedule times in HH:MM format (24-hour clock). + * Example: ['08:00', '12:00', '16:00'] + */ + getScheduledServiceTimes(): Observable { + return this.getServiceOrderConfiguration().pipe( + map((config) => config?.serviceScheduleTimes ?? []) + ); + } + + /** + * Retrieves the Service Order Configuration object with lead days and service schedule times. + * This method returns an observable since it depends on asynchronous data. + */ + getServiceOrderConfiguration(): Observable { + return this.baseSiteService.get().pipe( + take(1), + map((baseSite) => { + const config = { leadDays: 0, serviceScheduleTimes: [] as string[] }; + config.leadDays = + baseSite?.baseStore?.serviceOrderConfiguration?.leadDays ?? 0; + config.serviceScheduleTimes = + baseSite?.baseStore?.serviceOrderConfiguration + ?.serviceScheduleTimes ?? ([] as string[]); + return config; + }) + ); + } + + /** + * Converts a date and time string into DateTime format including timezone offset. + * @param date Date in YYYY-MM-DD format. Example: '2024-06-27' + * @param time Time in HH:MM format (24-hour clock). Example: '14:30' + * @returns String in DateTime format with timezone offset. + * Example: 2024-06-27T14:30:00±HH:MM (based on your local timezone offset) + */ + convertToDateTime(date: string, time: string): string { + const dateTimeString = `${date}T${time}:00`; + const timezoneOffset = TimeUtils.getLocalTimezoneOffset(); + return `${dateTimeString}${timezoneOffset}`; + } + + /** + * Converts a DateTime string with timezone offset into a readable string format. + * @param dateTime String in DateTime format with timezone offset. + * Example: 2024-07-11T14:30:00+05:30 + * @returns Readable string format. Example: 11/07/2024, 14:30 + */ + convertDateTimeToReadableString(dateTime: string): string { + const date = new Date(dateTime); + const secondsDigits = -3; + return date.toLocaleString().slice(0, secondsDigits); + } + + /** + * Converts a string containing both date and time into an object with separate properties - date and time. + * @param input Date and time in format `DD/MM/YYYY, HH:mm:ss` + * @returns Object with date and time separately as { date: 'YYYY-MM-DD', time: 'HH:mm' } + */ + getServiceDetailsFromDateTime(input: string): { date: string; time: string } { + const [datePart, timePart] = input.split(', '); + const [day, month, year] = datePart.split('/'); + const date = new Date( + parseInt(year, 10), + parseInt(month, 10) - 1, + parseInt(day, 10) + ); + const [hours, minutes] = timePart.split(':'); + return { + date: this.datePipe.transform(date, dateFormat) ?? '', + time: `${hours}:${minutes}`, + }; + } +} diff --git a/integration-libs/s4-service/root/facade/index.ts b/integration-libs/s4-service/root/facade/index.ts new file mode 100644 index 00000000000..b1d0b144960 --- /dev/null +++ b/integration-libs/s4-service/root/facade/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './checkout-service-details.facade'; +export * from './checkout-service-schedule-picker.service'; diff --git a/integration-libs/s4-service/root/feature-name.ts b/integration-libs/s4-service/root/feature-name.ts new file mode 100644 index 00000000000..8220880c718 --- /dev/null +++ b/integration-libs/s4-service/root/feature-name.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export const S4_SERVICE_FEATURE = 's4-service'; diff --git a/integration-libs/s4-service/root/model/augmented-types.model.ts b/integration-libs/s4-service/root/model/augmented-types.model.ts new file mode 100644 index 00000000000..1b6acaf104a --- /dev/null +++ b/integration-libs/s4-service/root/model/augmented-types.model.ts @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import '@spartacus/checkout/base/root'; +import { OccEndpoint } from '@spartacus/core'; +import '@spartacus/order/root'; +import { ServiceDateTime } from './checkout-service-details.model'; + +export abstract class ServiceOrderConfiguration { + serviceOrderConfiguration?: { + leadDays?: number; + serviceScheduleTimes?: string[]; + }; +} + +export interface CheckoutServiceOrderOccEndpoints { + /** + * Sets the service details for the checkout cart + */ + setServiceScheduleSlot?: string | OccEndpoint; +} + +declare module '@spartacus/order/root' { + interface Order { + servicedAt?: ServiceDateTime; + } +} + +declare module '@spartacus/checkout/base/root' { + const enum CheckoutStepType { + SERVICE_DETAILS = 'serviceDetails', + } + interface CheckoutState { + servicedAt?: ServiceDateTime; //response property name + } +} + +declare module '@spartacus/core' { + interface OccEndpoints extends CheckoutServiceOrderOccEndpoints {} + interface BaseStore extends ServiceOrderConfiguration {} + interface Product { + productTypes?: string; + } +} diff --git a/integration-libs/s4-service/root/model/checkout-service-details.model.ts b/integration-libs/s4-service/root/model/checkout-service-details.model.ts new file mode 100644 index 00000000000..8d03f11939f --- /dev/null +++ b/integration-libs/s4-service/root/model/checkout-service-details.model.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export type ServiceDateTime = string; + +export interface ServiceDetails { + scheduledAt?: ServiceDateTime; //name in request +} + +export interface ServiceOrderConfig { + leadDays?: number; + serviceScheduleTimes?: string[]; +} diff --git a/integration-libs/s4-service/root/model/index.ts b/integration-libs/s4-service/root/model/index.ts new file mode 100644 index 00000000000..c596bd9c15a --- /dev/null +++ b/integration-libs/s4-service/root/model/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import './augmented-types.model'; +export * from './checkout-service-details.model'; diff --git a/integration-libs/s4-service/root/ng-package.json b/integration-libs/s4-service/root/ng-package.json new file mode 100644 index 00000000000..38e01ac17de --- /dev/null +++ b/integration-libs/s4-service/root/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/integration-libs/s4-service/root/public_api.ts b/integration-libs/s4-service/root/public_api.ts new file mode 100644 index 00000000000..0ceeae9e568 --- /dev/null +++ b/integration-libs/s4-service/root/public_api.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './s4-service-root.module'; +export * from './feature-name'; +export * from './config/index'; +export * from './events/index'; +export * from './facade/index'; +export * from './model/index'; diff --git a/integration-libs/s4-service/root/s4-service-root.module.ts b/integration-libs/s4-service/root/s4-service-root.module.ts new file mode 100644 index 00000000000..df48e8b0de7 --- /dev/null +++ b/integration-libs/s4-service/root/s4-service-root.module.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { CHECKOUT_B2B_CMS_COMPONENTS } from '@spartacus/checkout/b2b/root'; +import { + CmsConfig, + CxDatePipe, + provideDefaultConfig, + provideDefaultConfigFactory, +} from '@spartacus/core'; +import { + CHECKOUT_FEATURE, + CheckoutConfig, +} from '@spartacus/checkout/base/root'; +import { + defaultServiceDetailsCheckoutConfig, + defaultCheckoutServiceDetailsRoutingConfig, +} from './config/index'; +import { CheckoutServiceDetailsEventModule } from './events/index'; +import { CheckoutServiceSchedulePickerService } from './facade/index'; + +export const S4_SERVICE_CMS_COMPONENTS: string[] = [ + ...CHECKOUT_B2B_CMS_COMPONENTS, + 'CheckoutServiceDetails', +]; + +export function defaultS4ServiceComponentsConfig() { + const config: CmsConfig = { + featureModules: { + [CHECKOUT_FEATURE]: { + cmsComponents: S4_SERVICE_CMS_COMPONENTS, + }, + }, + }; + return config; +} +@NgModule({ + imports: [CheckoutServiceDetailsEventModule], + providers: [ + { provide: CheckoutConfig, useValue: defaultServiceDetailsCheckoutConfig }, + provideDefaultConfig(defaultCheckoutServiceDetailsRoutingConfig), + provideDefaultConfigFactory(defaultS4ServiceComponentsConfig), + CxDatePipe, + CheckoutServiceSchedulePickerService, + ], +}) +export class S4ServiceRootModule {} diff --git a/integration-libs/s4-service/s4-service.module.ts b/integration-libs/s4-service/s4-service.module.ts new file mode 100644 index 00000000000..9e17ca88243 --- /dev/null +++ b/integration-libs/s4-service/s4-service.module.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; + +@NgModule({ + imports: [], +}) +export class S4ServiceModule {} diff --git a/integration-libs/s4-service/schematics/.gitignore b/integration-libs/s4-service/schematics/.gitignore new file mode 100644 index 00000000000..c88f4d69e15 --- /dev/null +++ b/integration-libs/s4-service/schematics/.gitignore @@ -0,0 +1,18 @@ +# Outputs +**/*.js +**/*.js.map +**/*.d.ts + +# IDEs +.idea/ +jsconfig.json +.vscode/ + +# Misc +node_modules/ +npm-debug.log* +yarn-error.log* + +# Mac OSX Finder files. +**/.DS_Store +.DS_Store diff --git a/integration-libs/s4-service/schematics/add-s4-service/__snapshots__/index_spec.ts.snap b/integration-libs/s4-service/schematics/add-s4-service/__snapshots__/index_spec.ts.snap new file mode 100644 index 00000000000..1180c78dfb7 --- /dev/null +++ b/integration-libs/s4-service/schematics/add-s4-service/__snapshots__/index_spec.ts.snap @@ -0,0 +1,94 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Spartacus S/4HANA Service Integration (S4-Service) Schematics: ng-add S4-Service feature eager loading should import appropriate modules 1`] = ` +"import { NgModule } from '@angular/core'; +import { I18nConfig, provideConfig } from "@spartacus/core"; +import { S4ServiceModule } from "@spartacus/s4-service"; +import { s4ServiceTranslationChunksConfig, s4ServiceTranslations } from "@spartacus/s4-service/assets"; +import { S4ServiceCheckoutModule } from "@spartacus/s4-service/checkout"; +import { S4ServiceOrderModule } from "@spartacus/s4-service/order"; +import { S4ServiceRootModule } from "@spartacus/s4-service/root"; + +@NgModule({ + declarations: [], + imports: [ + S4ServiceRootModule, + S4ServiceModule, + S4ServiceCheckoutModule, + S4ServiceOrderModule + ], + providers: [provideConfig({ + i18n: { + resources: s4ServiceTranslations, + chunks: s4ServiceTranslationChunksConfig, + }, + })] +}) +export class S4ServiceFeatureModule { } +" +`; + +exports[`Spartacus S/4HANA Service Integration (S4-Service) Schematics: ng-add S4-Service feature general setup should add the feature using the lazy loading syntax 1`] = ` +"import { NgModule } from '@angular/core'; +import { CmsConfig, I18nConfig, provideConfig } from "@spartacus/core"; +import { s4ServiceTranslationChunksConfig, s4ServiceTranslations } from "@spartacus/s4-service/assets"; +import { S4_SERVICE_FEATURE, S4ServiceRootModule } from "@spartacus/s4-service/root"; + +@NgModule({ + declarations: [], + imports: [ + S4ServiceRootModule + ], + providers: [provideConfig({ + featureModules: { + [S4_SERVICE_FEATURE]: { + module: () => + import('@spartacus/s4-service').then((m) => m.S4ServiceModule), + }, + } + }), + provideConfig({ + i18n: { + resources: s4ServiceTranslations, + chunks: s4ServiceTranslationChunksConfig, + }, + }) + ] +}) +export class S4ServiceFeatureModule { } +" +`; + +exports[`Spartacus S/4HANA Service Integration (S4-Service) Schematics: ng-add S4-Service feature general setup should install the appropriate dependencies 1`] = ` +"import { NgModule } from '@angular/core'; +import { OrderModule } from "@spartacus/order"; +import { S4ServiceOrderModule } from "@spartacus/s4-service/order"; + +@NgModule({ + declarations: [], + imports: [ + OrderModule, + S4ServiceOrderModule + ] +}) +export class OrderWrapperModule { } +" +`; + +exports[`Spartacus S/4HANA Service Integration (S4-Service) Schematics: ng-add S4-Service feature general setup should install the appropriate dependencies 2`] = ` +"import { NgModule } from '@angular/core'; +import { CheckoutB2BModule } from "@spartacus/checkout/b2b"; +import { CheckoutModule } from "@spartacus/checkout/base"; +import { S4ServiceCheckoutModule } from "@spartacus/s4-service/checkout"; + +@NgModule({ + declarations: [], + imports: [ + CheckoutModule, + CheckoutB2BModule, + S4ServiceCheckoutModule + ] +}) +export class CheckoutWrapperModule { } +" +`; diff --git a/integration-libs/s4-service/schematics/add-s4-service/index.ts b/integration-libs/s4-service/schematics/add-s4-service/index.ts new file mode 100644 index 00000000000..45817c8a7b5 --- /dev/null +++ b/integration-libs/s4-service/schematics/add-s4-service/index.ts @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + chain, + Rule, + SchematicContext, + Tree, +} from '@angular-devkit/schematics'; +import { + addFeatures, + addPackageJsonDependenciesForLibrary, + analyzeApplication, + analyzeCrossFeatureDependencies, + finalizeInstallation, + readPackageJson, + SpartacusOptions as SpartacusS4ServiceOptions, + validateSpartacusInstallation, +} from '@spartacus/schematics'; +import { peerDependencies } from '../../package.json'; + +export function addS4ServiceFeature(options: SpartacusS4ServiceOptions): Rule { + return (tree: Tree, _context: SchematicContext): Rule => { + const packageJson = readPackageJson(tree); + validateSpartacusInstallation(packageJson); + + const features = analyzeCrossFeatureDependencies( + options.features as string[] + ); + + return chain([ + analyzeApplication(options, features), + + addFeatures(options, features), + addPackageJsonDependenciesForLibrary(peerDependencies, options), + + finalizeInstallation(options, features), + ]); + }; +} diff --git a/integration-libs/s4-service/schematics/add-s4-service/index_spec.ts b/integration-libs/s4-service/schematics/add-s4-service/index_spec.ts new file mode 100644 index 00000000000..43ed53b44a2 --- /dev/null +++ b/integration-libs/s4-service/schematics/add-s4-service/index_spec.ts @@ -0,0 +1,221 @@ +/// + +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import { + Schema as ApplicationOptions, + Style, +} from '@schematics/angular/application/schema'; +import { Schema as WorkspaceOptions } from '@schematics/angular/workspace/schema'; +import { + S4_SERVICE_FEATURE_NAME, + SPARTACUS_SCHEMATICS, + SPARTACUS_S4_SERVICE, + LibraryOptions as SpartacusS4ServiceOptions, + SpartacusOptions, + s4ServiceFeatureModulePath, + ORDER_FEATURE_NAME, + SPARTACUS_ORDER, + CHECKOUT_B2B_FEATURE_NAME, + SPARTACUS_CHECKOUT_B2B, + SPARTACUS_CHECKOUT_BASE, + CHECKOUT_BASE_FEATURE_NAME, + orderWrapperModulePath, + checkoutWrapperModulePath, +} from '@spartacus/schematics'; +import * as path from 'path'; +import { peerDependencies } from '../../package.json'; + +const collectionPath = path.join(__dirname, '../collection.json'); + +describe('Spartacus S/4HANA Service Integration (S4-Service) Schematics: ng-add', () => { + const schematicRunner = new SchematicTestRunner( + SPARTACUS_S4_SERVICE, + collectionPath + ); + let appTree: UnitTestTree; + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + version: '0.5.0', + }; + const appOptions: ApplicationOptions = { + name: 'schematics-test', + inlineStyle: false, + inlineTemplate: false, + style: Style.Scss, + skipTests: false, + projectRoot: '', + standalone: false, + }; + const spartacusDefaultOptions: SpartacusOptions = { + project: 'schematics-test', + lazy: true, + features: [], + }; + const libraryNoFeaturesOptions: SpartacusS4ServiceOptions = { + project: 'schematics-test', + lazy: true, + features: [], + }; + const checkoutFeatureOptions: SpartacusS4ServiceOptions = { + ...libraryNoFeaturesOptions, + features: [CHECKOUT_BASE_FEATURE_NAME], + }; + const checkoutB2BFeatureOptions: SpartacusS4ServiceOptions = { + ...libraryNoFeaturesOptions, + features: [CHECKOUT_B2B_FEATURE_NAME], + }; + const orderFeatureOptions: SpartacusS4ServiceOptions = { + ...libraryNoFeaturesOptions, + features: [ORDER_FEATURE_NAME], + }; + const s4ServiceFeatureOptions: SpartacusS4ServiceOptions = { + ...libraryNoFeaturesOptions, + features: [S4_SERVICE_FEATURE_NAME], + }; + beforeEach(async () => { + schematicRunner.registerCollection( + SPARTACUS_SCHEMATICS, + path.join( + __dirname, + '../../../../projects/schematics/src/collection.json' + ) + ); + schematicRunner.registerCollection( + SPARTACUS_CHECKOUT_BASE, + path.join( + __dirname, + '../../../../feature-libs/checkout/schematics/collection.json' + ) + ); + schematicRunner.registerCollection( + SPARTACUS_CHECKOUT_B2B, + path.join( + __dirname, + '../../../../feature-libs/checkout/schematics/collection.json' + ) + ); + schematicRunner.registerCollection( + SPARTACUS_ORDER, + path.join( + __dirname, + '../../../../feature-libs/order/schematics/collection.json' + ) + ); + appTree = await schematicRunner.runExternalSchematic( + '@schematics/angular', + 'workspace', + workspaceOptions + ); + appTree = await schematicRunner.runExternalSchematic( + '@schematics/angular', + 'application', + appOptions, + appTree + ); + appTree = await schematicRunner.runExternalSchematic( + SPARTACUS_SCHEMATICS, + 'ng-add', + { ...spartacusDefaultOptions, name: 'schematics-test' }, + appTree + ); + }); + describe('Without features', () => { + beforeEach(async () => { + appTree = await schematicRunner.runSchematic( + 'ng-add', + libraryNoFeaturesOptions, + appTree + ); + }); + it('should not create any of the feature modules', () => { + expect(appTree.exists(s4ServiceFeatureModulePath)).toBeFalsy(); + }); + }); + describe('S4-Service feature', () => { + describe('general setup', () => { + beforeEach(async () => { + appTree = await schematicRunner.runSchematic( + 'ng-add', + checkoutFeatureOptions, + appTree + ); + appTree = await schematicRunner.runSchematic( + 'ng-add', + checkoutB2BFeatureOptions, + appTree + ); + appTree = await schematicRunner.runSchematic( + 'ng-add', + orderFeatureOptions, + appTree + ); + appTree = await schematicRunner.runSchematic( + 'ng-add', + s4ServiceFeatureOptions, + appTree + ); + }); + it('should install necessary Spartacus libraries', () => { + const packageJson = JSON.parse(appTree.readContent('package.json')); + let dependencies: Record = {}; + dependencies = { ...packageJson.dependencies }; + dependencies = { ...dependencies, ...packageJson.devDependencies }; + + for (const toAdd in peerDependencies) { + if ( + !peerDependencies.hasOwnProperty(toAdd) || + toAdd === SPARTACUS_SCHEMATICS + ) { + continue; + } + const expectedDependency = dependencies[toAdd]; + expect(expectedDependency).toBeTruthy(); + } + }); + it('should add the feature using the lazy loading syntax', async () => { + const module = appTree.readContent(s4ServiceFeatureModulePath); + expect(module).toMatchSnapshot(); + }); + it('should install the appropriate dependencies', async () => { + const orderWrapperModule = appTree.readContent(orderWrapperModulePath); + expect(orderWrapperModule).toMatchSnapshot(); + const checkoutWrapperModule = appTree.readContent( + checkoutWrapperModulePath + ); + expect(checkoutWrapperModule).toMatchSnapshot(); + }); + }); + describe('eager loading', () => { + beforeEach(async () => { + appTree = await schematicRunner.runSchematic( + 'ng-add', + { ...checkoutFeatureOptions, lazy: false }, + appTree + ); + appTree = await schematicRunner.runSchematic( + 'ng-add', + { ...checkoutB2BFeatureOptions, lazy: false }, + appTree + ); + appTree = await schematicRunner.runSchematic( + 'ng-add', + { ...orderFeatureOptions, lazy: false }, + appTree + ); + + appTree = await schematicRunner.runSchematic( + 'ng-add', + { ...s4ServiceFeatureOptions, lazy: false }, + appTree + ); + }); + it('should import appropriate modules', async () => { + const module = appTree.readContent(s4ServiceFeatureModulePath); + expect(module).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/integration-libs/s4-service/schematics/add-s4-service/schema.json b/integration-libs/s4-service/schematics/add-s4-service/schema.json new file mode 100644 index 00000000000..a5d02da99a7 --- /dev/null +++ b/integration-libs/s4-service/schematics/add-s4-service/schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "S4ServiceSchematics", + "title": "S/4HANA Service Integration Schematics", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + } + }, + "debug": { + "description": "Display additional details during the running process.", + "type": "boolean", + "default": false + }, + "lazy": { + "type": "boolean", + "description": "Lazy load the S/4HANA Service Integration feature.", + "default": true + }, + "features": { + "type": "array", + "uniqueItems": true, + "default": ["s4-service"] + } + }, + "required": [] +} diff --git a/integration-libs/s4-service/schematics/collection.json b/integration-libs/s4-service/schematics/collection.json new file mode 100644 index 00000000000..19edb89828b --- /dev/null +++ b/integration-libs/s4-service/schematics/collection.json @@ -0,0 +1,18 @@ +{ + "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json", + "schematics": { + "ng-add": { + "factory": "./add-s4-service/index#addS4ServiceFeature", + "description": "Add and configure Spartacus' S/4HANA Service Integration feature", + "schema": "./add-s4-service/schema.json", + "private": true, + "hidden": true, + "aliases": ["install"] + }, + "add": { + "factory": "./add-s4-service/index#addS4ServiceFeature", + "description": "Add and configure Spartacus' S/4HANA Service Integration feature", + "schema": "./add-s4-service/schema.json" + } + } +} diff --git a/integration-libs/s4-service/setup-jest.ts b/integration-libs/s4-service/setup-jest.ts new file mode 100644 index 00000000000..aeb0a861992 --- /dev/null +++ b/integration-libs/s4-service/setup-jest.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import 'jest-preset-angular/setup-jest'; +import 'zone.js'; diff --git a/integration-libs/s4-service/styles/_index.scss b/integration-libs/s4-service/styles/_index.scss new file mode 100644 index 00000000000..87531ba56a1 --- /dev/null +++ b/integration-libs/s4-service/styles/_index.scss @@ -0,0 +1 @@ +@import './components/checkout-service-details'; diff --git a/integration-libs/s4-service/styles/components/_checkout-service-details.scss b/integration-libs/s4-service/styles/components/_checkout-service-details.scss new file mode 100644 index 00000000000..7a6148135f0 --- /dev/null +++ b/integration-libs/s4-service/styles/components/_checkout-service-details.scss @@ -0,0 +1,22 @@ +%cx-service-details { + @include checkout-media-style(); + + border: 1px solid var(--cx-color-medium); + border-radius: 10px; + padding: 23px 30px 8px; + background-color: var(--cx-color-inverse); + + @include media-breakpoint-down(md) { + background-color: var(--cx-color-transparent); + margin-top: 32px; + padding: 23px 30px 8px; + } + + @include media-breakpoint-up(lg) { + margin: 2rem 0 2rem; + } + &.row { + display: flex; + flex-direction: column; + } +} diff --git a/integration-libs/s4-service/test.ts b/integration-libs/s4-service/test.ts new file mode 100644 index 00000000000..cb29fd468dd --- /dev/null +++ b/integration-libs/s4-service/test.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing'; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { + teardown: { destroyAfterEach: false }, + } +); diff --git a/integration-libs/s4-service/tsconfig.lib.json b/integration-libs/s4-service/tsconfig.lib.json new file mode 100644 index 00000000000..d029528d3bb --- /dev/null +++ b/integration-libs/s4-service/tsconfig.lib.json @@ -0,0 +1,127 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "declarationMap": true, + "module": "es2022", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "importHelpers": true, + "strict": true, + "types": [], + "lib": ["dom", "esnext"], + "paths": { + "@spartacus/core": ["dist/core"], + "@spartacus/cart/base/assets": ["dist/cart/base/assets"], + "@spartacus/cart/base/components/add-to-cart": [ + "dist/cart/base/components/add-to-cart" + ], + "@spartacus/cart/base/components/mini-cart": [ + "dist/cart/base/components/mini-cart" + ], + "@spartacus/cart/base/components": ["dist/cart/base/components"], + "@spartacus/cart/base/core": ["dist/cart/base/core"], + "@spartacus/cart/base": ["dist/cart/base"], + "@spartacus/cart/base/occ": ["dist/cart/base/occ"], + "@spartacus/cart/base/root": ["dist/cart/base/root"], + "@spartacus/cart/import-export/assets": [ + "dist/cart/import-export/assets" + ], + "@spartacus/cart/import-export/components": [ + "dist/cart/import-export/components" + ], + "@spartacus/cart/import-export/core": ["dist/cart/import-export/core"], + "@spartacus/cart/import-export": ["dist/cart/import-export"], + "@spartacus/cart/import-export/root": ["dist/cart/import-export/root"], + "@spartacus/cart": ["dist/cart"], + "@spartacus/cart/quick-order/assets": ["dist/cart/quick-order/assets"], + "@spartacus/cart/quick-order/components": [ + "dist/cart/quick-order/components" + ], + "@spartacus/cart/quick-order/core": ["dist/cart/quick-order/core"], + "@spartacus/cart/quick-order": ["dist/cart/quick-order"], + "@spartacus/cart/quick-order/root": ["dist/cart/quick-order/root"], + "@spartacus/cart/saved-cart/assets": ["dist/cart/saved-cart/assets"], + "@spartacus/cart/saved-cart/components": [ + "dist/cart/saved-cart/components" + ], + "@spartacus/cart/saved-cart/core": ["dist/cart/saved-cart/core"], + "@spartacus/cart/saved-cart": ["dist/cart/saved-cart"], + "@spartacus/cart/saved-cart/occ": ["dist/cart/saved-cart/occ"], + "@spartacus/cart/saved-cart/root": ["dist/cart/saved-cart/root"], + "@spartacus/cart/wish-list/assets": ["dist/cart/wish-list/assets"], + "@spartacus/cart/wish-list/components/add-to-wishlist": [ + "dist/cart/wish-list/components/add-to-wishlist" + ], + "@spartacus/cart/wish-list/components": [ + "dist/cart/wish-list/components" + ], + "@spartacus/cart/wish-list/core": ["dist/cart/wish-list/core"], + "@spartacus/cart/wish-list": ["dist/cart/wish-list"], + "@spartacus/cart/wish-list/root": ["dist/cart/wish-list/root"], + "@spartacus/checkout/b2b/assets": ["dist/checkout/b2b/assets"], + "@spartacus/checkout/b2b/components": ["dist/checkout/b2b/components"], + "@spartacus/checkout/b2b/core": ["dist/checkout/b2b/core"], + "@spartacus/checkout/b2b": ["dist/checkout/b2b"], + "@spartacus/checkout/b2b/occ": ["dist/checkout/b2b/occ"], + "@spartacus/checkout/b2b/root": ["dist/checkout/b2b/root"], + "@spartacus/checkout/base/assets": ["dist/checkout/base/assets"], + "@spartacus/checkout/base/components": ["dist/checkout/base/components"], + "@spartacus/checkout/base/core": ["dist/checkout/base/core"], + "@spartacus/checkout/base": ["dist/checkout/base"], + "@spartacus/checkout/base/occ": ["dist/checkout/base/occ"], + "@spartacus/checkout/base/root": ["dist/checkout/base/root"], + "@spartacus/checkout": ["dist/checkout"], + "@spartacus/checkout/scheduled-replenishment/assets": [ + "dist/checkout/scheduled-replenishment/assets" + ], + "@spartacus/checkout/scheduled-replenishment/components": [ + "dist/checkout/scheduled-replenishment/components" + ], + "@spartacus/checkout/scheduled-replenishment": [ + "dist/checkout/scheduled-replenishment" + ], + "@spartacus/checkout/scheduled-replenishment/root": [ + "dist/checkout/scheduled-replenishment/root" + ], + "@spartacus/storefront": ["dist/storefrontlib"], + "@spartacus/order/assets": ["dist/order/assets"], + "@spartacus/order/components": ["dist/order/components"], + "@spartacus/order/core": ["dist/order/core"], + "@spartacus/order": ["dist/order"], + "@spartacus/order/occ": ["dist/order/occ"], + "@spartacus/order/root": ["dist/order/root"], + "@spartacus/user/account/assets": ["dist/user/account/assets"], + "@spartacus/user/account/components": ["dist/user/account/components"], + "@spartacus/user/account/core": ["dist/user/account/core"], + "@spartacus/user/account": ["dist/user/account"], + "@spartacus/user/account/occ": ["dist/user/account/occ"], + "@spartacus/user/account/root": ["dist/user/account/root"], + "@spartacus/user": ["dist/user"], + "@spartacus/user/profile/assets": ["dist/user/profile/assets"], + "@spartacus/user/profile/components": ["dist/user/profile/components"], + "@spartacus/user/profile/core": ["dist/user/profile/core"], + "@spartacus/user/profile": ["dist/user/profile"], + "@spartacus/user/profile/occ": ["dist/user/profile/occ"], + "@spartacus/user/profile/root": ["dist/user/profile/root"], + "@spartacus/pdf-invoices/assets": ["dist/pdf-invoices/assets"], + "@spartacus/pdf-invoices/components": ["dist/pdf-invoices/components"], + "@spartacus/pdf-invoices/core": ["dist/pdf-invoices/core"], + "@spartacus/pdf-invoices": ["dist/pdf-invoices"], + "@spartacus/pdf-invoices/occ": ["dist/pdf-invoices/occ"], + "@spartacus/pdf-invoices/root": ["dist/pdf-invoices/root"] + } + }, + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true + }, + "exclude": ["test.ts", "setup-jest.ts", "**/*.spec.ts"] +} diff --git a/integration-libs/s4-service/tsconfig.lib.prod.json b/integration-libs/s4-service/tsconfig.lib.prod.json new file mode 100644 index 00000000000..2a2faa884cf --- /dev/null +++ b/integration-libs/s4-service/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/integration-libs/s4-service/tsconfig.schematics.json b/integration-libs/s4-service/tsconfig.schematics.json new file mode 100644 index 00000000000..b4c289b5c84 --- /dev/null +++ b/integration-libs/s4-service/tsconfig.schematics.json @@ -0,0 +1,631 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "module": "CommonJs", + "types": ["jest"], + "paths": { + "@spartacus/schematics": ["../../projects/schematics/index"], + "@spartacus/setup": ["../../core-libs/setup/public_api"], + "@spartacus/setup/ssr": ["../../core-libs/setup/ssr/public_api"], + "@spartacus/asm/assets": ["../../feature-libs/asm/assets/public_api"], + "@spartacus/asm/components": [ + "../../feature-libs/asm/components/public_api" + ], + "@spartacus/asm/core": ["../../feature-libs/asm/core/public_api"], + "@spartacus/asm/customer-360/assets": [ + "../../feature-libs/asm/customer-360/assets/public_api" + ], + "@spartacus/asm/customer-360/components": [ + "../../feature-libs/asm/customer-360/components/public_api" + ], + "@spartacus/asm/customer-360/core": [ + "../../feature-libs/asm/customer-360/core/public_api" + ], + "@spartacus/asm/customer-360": [ + "../../feature-libs/asm/customer-360/public_api" + ], + "@spartacus/asm/customer-360/occ": [ + "../../feature-libs/asm/customer-360/occ/public_api" + ], + "@spartacus/asm/customer-360/root": [ + "../../feature-libs/asm/customer-360/root/public_api" + ], + "@spartacus/asm": ["../../feature-libs/asm/public_api"], + "@spartacus/asm/occ": ["../../feature-libs/asm/occ/public_api"], + "@spartacus/asm/root": ["../../feature-libs/asm/root/public_api"], + "@spartacus/cart/base/assets": [ + "../../feature-libs/cart/base/assets/public_api" + ], + "@spartacus/cart/base/components/add-to-cart": [ + "../../feature-libs/cart/base/components/add-to-cart/public_api" + ], + "@spartacus/cart/base/components/mini-cart": [ + "../../feature-libs/cart/base/components/mini-cart/public_api" + ], + "@spartacus/cart/base/components": [ + "../../feature-libs/cart/base/components/public_api" + ], + "@spartacus/cart/base/core": [ + "../../feature-libs/cart/base/core/public_api" + ], + "@spartacus/cart/base": ["../../feature-libs/cart/base/public_api"], + "@spartacus/cart/base/occ": [ + "../../feature-libs/cart/base/occ/public_api" + ], + "@spartacus/cart/base/root": [ + "../../feature-libs/cart/base/root/public_api" + ], + "@spartacus/cart/import-export/assets": [ + "../../feature-libs/cart/import-export/assets/public_api" + ], + "@spartacus/cart/import-export/components": [ + "../../feature-libs/cart/import-export/components/public_api" + ], + "@spartacus/cart/import-export/core": [ + "../../feature-libs/cart/import-export/core/public_api" + ], + "@spartacus/cart/import-export": [ + "../../feature-libs/cart/import-export/public_api" + ], + "@spartacus/cart/import-export/root": [ + "../../feature-libs/cart/import-export/root/public_api" + ], + "@spartacus/cart": ["../../feature-libs/cart/public_api"], + "@spartacus/cart/quick-order/assets": [ + "../../feature-libs/cart/quick-order/assets/public_api" + ], + "@spartacus/cart/quick-order/components": [ + "../../feature-libs/cart/quick-order/components/public_api" + ], + "@spartacus/cart/quick-order/core": [ + "../../feature-libs/cart/quick-order/core/public_api" + ], + "@spartacus/cart/quick-order": [ + "../../feature-libs/cart/quick-order/public_api" + ], + "@spartacus/cart/quick-order/root": [ + "../../feature-libs/cart/quick-order/root/public_api" + ], + "@spartacus/cart/saved-cart/assets": [ + "../../feature-libs/cart/saved-cart/assets/public_api" + ], + "@spartacus/cart/saved-cart/components": [ + "../../feature-libs/cart/saved-cart/components/public_api" + ], + "@spartacus/cart/saved-cart/core": [ + "../../feature-libs/cart/saved-cart/core/public_api" + ], + "@spartacus/cart/saved-cart": [ + "../../feature-libs/cart/saved-cart/public_api" + ], + "@spartacus/cart/saved-cart/occ": [ + "../../feature-libs/cart/saved-cart/occ/public_api" + ], + "@spartacus/cart/saved-cart/root": [ + "../../feature-libs/cart/saved-cart/root/public_api" + ], + "@spartacus/cart/wish-list/assets": [ + "../../feature-libs/cart/wish-list/assets/public_api" + ], + "@spartacus/cart/wish-list/components/add-to-wishlist": [ + "../../feature-libs/cart/wish-list/components/add-to-wishlist/public_api" + ], + "@spartacus/cart/wish-list/components": [ + "../../feature-libs/cart/wish-list/components/public_api" + ], + "@spartacus/cart/wish-list/core": [ + "../../feature-libs/cart/wish-list/core/public_api" + ], + "@spartacus/cart/wish-list": [ + "../../feature-libs/cart/wish-list/public_api" + ], + "@spartacus/cart/wish-list/root": [ + "../../feature-libs/cart/wish-list/root/public_api" + ], + "@spartacus/checkout/b2b/assets": [ + "../../feature-libs/checkout/b2b/assets/public_api" + ], + "@spartacus/checkout/b2b/components": [ + "../../feature-libs/checkout/b2b/components/public_api" + ], + "@spartacus/checkout/b2b/core": [ + "../../feature-libs/checkout/b2b/core/public_api" + ], + "@spartacus/checkout/b2b": ["../../feature-libs/checkout/b2b/public_api"], + "@spartacus/checkout/b2b/occ": [ + "../../feature-libs/checkout/b2b/occ/public_api" + ], + "@spartacus/checkout/b2b/root": [ + "../../feature-libs/checkout/b2b/root/public_api" + ], + "@spartacus/checkout/base/assets": [ + "../../feature-libs/checkout/base/assets/public_api" + ], + "@spartacus/checkout/base/components": [ + "../../feature-libs/checkout/base/components/public_api" + ], + "@spartacus/checkout/base/core": [ + "../../feature-libs/checkout/base/core/public_api" + ], + "@spartacus/checkout/base": [ + "../../feature-libs/checkout/base/public_api" + ], + "@spartacus/checkout/base/occ": [ + "../../feature-libs/checkout/base/occ/public_api" + ], + "@spartacus/checkout/base/root": [ + "../../feature-libs/checkout/base/root/public_api" + ], + "@spartacus/checkout": ["../../feature-libs/checkout/public_api"], + "@spartacus/checkout/scheduled-replenishment/assets": [ + "../../feature-libs/checkout/scheduled-replenishment/assets/public_api" + ], + "@spartacus/checkout/scheduled-replenishment/components": [ + "../../feature-libs/checkout/scheduled-replenishment/components/public_api" + ], + "@spartacus/checkout/scheduled-replenishment": [ + "../../feature-libs/checkout/scheduled-replenishment/public_api" + ], + "@spartacus/checkout/scheduled-replenishment/root": [ + "../../feature-libs/checkout/scheduled-replenishment/root/public_api" + ], + "@spartacus/customer-ticketing/assets": [ + "../../feature-libs/customer-ticketing/assets/public_api" + ], + "@spartacus/customer-ticketing/components": [ + "../../feature-libs/customer-ticketing/components/public_api" + ], + "@spartacus/customer-ticketing/core": [ + "../../feature-libs/customer-ticketing/core/public_api" + ], + "@spartacus/customer-ticketing": [ + "../../feature-libs/customer-ticketing/public_api" + ], + "@spartacus/customer-ticketing/occ": [ + "../../feature-libs/customer-ticketing/occ/public_api" + ], + "@spartacus/customer-ticketing/root": [ + "../../feature-libs/customer-ticketing/root/public_api" + ], + "@spartacus/estimated-delivery-date/assets": [ + "../../feature-libs/estimated-delivery-date/assets/public_api" + ], + "@spartacus/estimated-delivery-date": [ + "../../feature-libs/estimated-delivery-date/public_api" + ], + "@spartacus/estimated-delivery-date/root": [ + "../../feature-libs/estimated-delivery-date/root/public_api" + ], + "@spartacus/estimated-delivery-date/show-estimated-delivery-date": [ + "../../feature-libs/estimated-delivery-date/show-estimated-delivery-date/public_api" + ], + "@spartacus/order/assets": ["../../feature-libs/order/assets/public_api"], + "@spartacus/order/components": [ + "../../feature-libs/order/components/public_api" + ], + "@spartacus/order/core": ["../../feature-libs/order/core/public_api"], + "@spartacus/order": ["../../feature-libs/order/public_api"], + "@spartacus/order/occ": ["../../feature-libs/order/occ/public_api"], + "@spartacus/order/root": ["../../feature-libs/order/root/public_api"], + "@spartacus/organization/account-summary/assets": [ + "../../feature-libs/organization/account-summary/assets/public_api" + ], + "@spartacus/organization/account-summary/components": [ + "../../feature-libs/organization/account-summary/components/public_api" + ], + "@spartacus/organization/account-summary/core": [ + "../../feature-libs/organization/account-summary/core/public_api" + ], + "@spartacus/organization/account-summary": [ + "../../feature-libs/organization/account-summary/public_api" + ], + "@spartacus/organization/account-summary/occ": [ + "../../feature-libs/organization/account-summary/occ/public_api" + ], + "@spartacus/organization/account-summary/root": [ + "../../feature-libs/organization/account-summary/root/public_api" + ], + "@spartacus/organization/administration/assets": [ + "../../feature-libs/organization/administration/assets/public_api" + ], + "@spartacus/organization/administration/components": [ + "../../feature-libs/organization/administration/components/public_api" + ], + "@spartacus/organization/administration/core": [ + "../../feature-libs/organization/administration/core/public_api" + ], + "@spartacus/organization/administration": [ + "../../feature-libs/organization/administration/public_api" + ], + "@spartacus/organization/administration/occ": [ + "../../feature-libs/organization/administration/occ/public_api" + ], + "@spartacus/organization/administration/root": [ + "../../feature-libs/organization/administration/root/public_api" + ], + "@spartacus/organization": ["../../feature-libs/organization/public_api"], + "@spartacus/organization/order-approval/assets": [ + "../../feature-libs/organization/order-approval/assets/public_api" + ], + "@spartacus/organization/order-approval": [ + "../../feature-libs/organization/order-approval/public_api" + ], + "@spartacus/organization/order-approval/root": [ + "../../feature-libs/organization/order-approval/root/public_api" + ], + "@spartacus/organization/unit-order/assets": [ + "../../feature-libs/organization/unit-order/assets/public_api" + ], + "@spartacus/organization/unit-order/components": [ + "../../feature-libs/organization/unit-order/components/public_api" + ], + "@spartacus/organization/unit-order/core": [ + "../../feature-libs/organization/unit-order/core/public_api" + ], + "@spartacus/organization/unit-order": [ + "../../feature-libs/organization/unit-order/public_api" + ], + "@spartacus/organization/unit-order/occ": [ + "../../feature-libs/organization/unit-order/occ/public_api" + ], + "@spartacus/organization/unit-order/root": [ + "../../feature-libs/organization/unit-order/root/public_api" + ], + "@spartacus/organization/user-registration/assets": [ + "../../feature-libs/organization/user-registration/assets/public_api" + ], + "@spartacus/organization/user-registration/components": [ + "../../feature-libs/organization/user-registration/components/public_api" + ], + "@spartacus/organization/user-registration/core": [ + "../../feature-libs/organization/user-registration/core/public_api" + ], + "@spartacus/organization/user-registration": [ + "../../feature-libs/organization/user-registration/public_api" + ], + "@spartacus/organization/user-registration/occ": [ + "../../feature-libs/organization/user-registration/occ/public_api" + ], + "@spartacus/organization/user-registration/root": [ + "../../feature-libs/organization/user-registration/root/public_api" + ], + "@spartacus/pdf-invoices/assets": [ + "../../feature-libs/pdf-invoices/assets/public_api" + ], + "@spartacus/pdf-invoices/components": [ + "../../feature-libs/pdf-invoices/components/public_api" + ], + "@spartacus/pdf-invoices/core": [ + "../../feature-libs/pdf-invoices/core/public_api" + ], + "@spartacus/pdf-invoices": ["../../feature-libs/pdf-invoices/public_api"], + "@spartacus/pdf-invoices/occ": [ + "../../feature-libs/pdf-invoices/occ/public_api" + ], + "@spartacus/pdf-invoices/root": [ + "../../feature-libs/pdf-invoices/root/public_api" + ], + "@spartacus/pickup-in-store/assets": [ + "../../feature-libs/pickup-in-store/assets/public_api" + ], + "@spartacus/pickup-in-store/components": [ + "../../feature-libs/pickup-in-store/components/public_api" + ], + "@spartacus/pickup-in-store/core": [ + "../../feature-libs/pickup-in-store/core/public_api" + ], + "@spartacus/pickup-in-store": [ + "../../feature-libs/pickup-in-store/public_api" + ], + "@spartacus/pickup-in-store/occ": [ + "../../feature-libs/pickup-in-store/occ/public_api" + ], + "@spartacus/pickup-in-store/root": [ + "../../feature-libs/pickup-in-store/root/public_api" + ], + "@spartacus/product-configurator/common/assets": [ + "../../feature-libs/product-configurator/common/assets/public_api" + ], + "@spartacus/product-configurator/common": [ + "../../feature-libs/product-configurator/common/public_api" + ], + "@spartacus/product-configurator": [ + "../../feature-libs/product-configurator/public_api" + ], + "@spartacus/product-configurator/rulebased/cpq": [ + "../../feature-libs/product-configurator/rulebased/cpq/public_api" + ], + "@spartacus/product-configurator/rulebased": [ + "../../feature-libs/product-configurator/rulebased/public_api" + ], + "@spartacus/product-configurator/rulebased/root": [ + "../../feature-libs/product-configurator/rulebased/root/public_api" + ], + "@spartacus/product-configurator/textfield": [ + "../../feature-libs/product-configurator/textfield/public_api" + ], + "@spartacus/product-configurator/textfield/root": [ + "../../feature-libs/product-configurator/textfield/root/public_api" + ], + "@spartacus/product/bulk-pricing/assets": [ + "../../feature-libs/product/bulk-pricing/assets/public_api" + ], + "@spartacus/product/bulk-pricing/components": [ + "../../feature-libs/product/bulk-pricing/components/public_api" + ], + "@spartacus/product/bulk-pricing/core": [ + "../../feature-libs/product/bulk-pricing/core/public_api" + ], + "@spartacus/product/bulk-pricing": [ + "../../feature-libs/product/bulk-pricing/public_api" + ], + "@spartacus/product/bulk-pricing/occ": [ + "../../feature-libs/product/bulk-pricing/occ/public_api" + ], + "@spartacus/product/bulk-pricing/root": [ + "../../feature-libs/product/bulk-pricing/root/public_api" + ], + "@spartacus/product/future-stock/assets": [ + "../../feature-libs/product/future-stock/assets/public_api" + ], + "@spartacus/product/future-stock/components": [ + "../../feature-libs/product/future-stock/components/public_api" + ], + "@spartacus/product/future-stock/core": [ + "../../feature-libs/product/future-stock/core/public_api" + ], + "@spartacus/product/future-stock": [ + "../../feature-libs/product/future-stock/public_api" + ], + "@spartacus/product/future-stock/occ": [ + "../../feature-libs/product/future-stock/occ/public_api" + ], + "@spartacus/product/future-stock/root": [ + "../../feature-libs/product/future-stock/root/public_api" + ], + "@spartacus/product/image-zoom/assets": [ + "../../feature-libs/product/image-zoom/assets/public_api" + ], + "@spartacus/product/image-zoom/components": [ + "../../feature-libs/product/image-zoom/components/public_api" + ], + "@spartacus/product/image-zoom": [ + "../../feature-libs/product/image-zoom/public_api" + ], + "@spartacus/product/image-zoom/root": [ + "../../feature-libs/product/image-zoom/root/public_api" + ], + "@spartacus/product": ["../../feature-libs/product/public_api"], + "@spartacus/product/variants/assets": [ + "../../feature-libs/product/variants/assets/public_api" + ], + "@spartacus/product/variants/components": [ + "../../feature-libs/product/variants/components/public_api" + ], + "@spartacus/product/variants": [ + "../../feature-libs/product/variants/public_api" + ], + "@spartacus/product/variants/occ": [ + "../../feature-libs/product/variants/occ/public_api" + ], + "@spartacus/product/variants/root": [ + "../../feature-libs/product/variants/root/public_api" + ], + "@spartacus/qualtrics/components": [ + "../../feature-libs/qualtrics/components/public_api" + ], + "@spartacus/qualtrics": ["../../feature-libs/qualtrics/public_api"], + "@spartacus/qualtrics/root": [ + "../../feature-libs/qualtrics/root/public_api" + ], + "@spartacus/quote/assets": ["../../feature-libs/quote/assets/public_api"], + "@spartacus/quote/components/cart-guard": [ + "../../feature-libs/quote/components/cart-guard/public_api" + ], + "@spartacus/quote/components": [ + "../../feature-libs/quote/components/public_api" + ], + "@spartacus/quote/components/request-button": [ + "../../feature-libs/quote/components/request-button/public_api" + ], + "@spartacus/quote/core": ["../../feature-libs/quote/core/public_api"], + "@spartacus/quote": ["../../feature-libs/quote/public_api"], + "@spartacus/quote/occ": ["../../feature-libs/quote/occ/public_api"], + "@spartacus/quote/root": ["../../feature-libs/quote/root/public_api"], + "@spartacus/requested-delivery-date/assets": [ + "../../feature-libs/requested-delivery-date/assets/public_api" + ], + "@spartacus/requested-delivery-date/core": [ + "../../feature-libs/requested-delivery-date/core/public_api" + ], + "@spartacus/requested-delivery-date": [ + "../../feature-libs/requested-delivery-date/public_api" + ], + "@spartacus/requested-delivery-date/occ": [ + "../../feature-libs/requested-delivery-date/occ/public_api" + ], + "@spartacus/requested-delivery-date/root": [ + "../../feature-libs/requested-delivery-date/root/public_api" + ], + "@spartacus/smartedit/core": [ + "../../feature-libs/smartedit/core/public_api" + ], + "@spartacus/smartedit": ["../../feature-libs/smartedit/public_api"], + "@spartacus/smartedit/root": [ + "../../feature-libs/smartedit/root/public_api" + ], + "@spartacus/storefinder/assets": [ + "../../feature-libs/storefinder/assets/public_api" + ], + "@spartacus/storefinder/components": [ + "../../feature-libs/storefinder/components/public_api" + ], + "@spartacus/storefinder/core": [ + "../../feature-libs/storefinder/core/public_api" + ], + "@spartacus/storefinder": ["../../feature-libs/storefinder/public_api"], + "@spartacus/storefinder/occ": [ + "../../feature-libs/storefinder/occ/public_api" + ], + "@spartacus/storefinder/root": [ + "../../feature-libs/storefinder/root/public_api" + ], + "@spartacus/tracking": ["../../feature-libs/tracking/public_api"], + "@spartacus/tracking/personalization/core": [ + "../../feature-libs/tracking/personalization/core/public_api" + ], + "@spartacus/tracking/personalization": [ + "../../feature-libs/tracking/personalization/public_api" + ], + "@spartacus/tracking/personalization/root": [ + "../../feature-libs/tracking/personalization/root/public_api" + ], + "@spartacus/tracking/tms/aep": [ + "../../feature-libs/tracking/tms/aep/public_api" + ], + "@spartacus/tracking/tms/core": [ + "../../feature-libs/tracking/tms/core/public_api" + ], + "@spartacus/tracking/tms/gtm": [ + "../../feature-libs/tracking/tms/gtm/public_api" + ], + "@spartacus/tracking/tms": ["../../feature-libs/tracking/tms/public_api"], + "@spartacus/user/account/assets": [ + "../../feature-libs/user/account/assets/public_api" + ], + "@spartacus/user/account/components": [ + "../../feature-libs/user/account/components/public_api" + ], + "@spartacus/user/account/core": [ + "../../feature-libs/user/account/core/public_api" + ], + "@spartacus/user/account": ["../../feature-libs/user/account/public_api"], + "@spartacus/user/account/occ": [ + "../../feature-libs/user/account/occ/public_api" + ], + "@spartacus/user/account/root": [ + "../../feature-libs/user/account/root/public_api" + ], + "@spartacus/user": ["../../feature-libs/user/public_api"], + "@spartacus/user/profile/assets": [ + "../../feature-libs/user/profile/assets/public_api" + ], + "@spartacus/user/profile/components": [ + "../../feature-libs/user/profile/components/public_api" + ], + "@spartacus/user/profile/core": [ + "../../feature-libs/user/profile/core/public_api" + ], + "@spartacus/user/profile": ["../../feature-libs/user/profile/public_api"], + "@spartacus/user/profile/occ": [ + "../../feature-libs/user/profile/occ/public_api" + ], + "@spartacus/user/profile/root": [ + "../../feature-libs/user/profile/root/public_api" + ], + "@spartacus/cdc/assets": ["../../integration-libs/cdc/assets/public_api"], + "@spartacus/cdc/components": [ + "../../integration-libs/cdc/components/public_api" + ], + "@spartacus/cdc/core": ["../../integration-libs/cdc/core/public_api"], + "@spartacus/cdc": ["../../integration-libs/cdc/public_api"], + "@spartacus/cdc/organization/administration": [ + "../../integration-libs/cdc/organization/administration/public_api" + ], + "@spartacus/cdc/organization/user-registration": [ + "../../integration-libs/cdc/organization/user-registration/public_api" + ], + "@spartacus/cdc/root": ["../../integration-libs/cdc/root/public_api"], + "@spartacus/cdc/user-account": [ + "../../integration-libs/cdc/user-account/public_api" + ], + "@spartacus/cdc/user-profile": [ + "../../integration-libs/cdc/user-profile/public_api" + ], + "@spartacus/cdp/customer-ticketing": [ + "../../integration-libs/cdp/customer-ticketing/public_api" + ], + "@spartacus/cdp": ["../../integration-libs/cdp/public_api"], + "@spartacus/cds/assets": ["../../integration-libs/cds/assets/public_api"], + "@spartacus/cds": ["../../integration-libs/cds/public_api"], + "@spartacus/cpq-quote/assets": [ + "../../integration-libs/cpq-quote/assets/public_api" + ], + "@spartacus/cpq-quote/cpq-quote-discount": [ + "../../integration-libs/cpq-quote/cpq-quote-discount/public_api" + ], + "@spartacus/cpq-quote": ["../../integration-libs/cpq-quote/public_api"], + "@spartacus/cpq-quote/root": [ + "../../integration-libs/cpq-quote/root/public_api" + ], + "@spartacus/digital-payments/assets": [ + "../../integration-libs/digital-payments/assets/public_api" + ], + "@spartacus/digital-payments": [ + "../../integration-libs/digital-payments/public_api" + ], + "@spartacus/epd-visualization/assets": [ + "../../integration-libs/epd-visualization/assets/public_api" + ], + "@spartacus/epd-visualization/components": [ + "../../integration-libs/epd-visualization/components/public_api" + ], + "@spartacus/epd-visualization/core": [ + "../../integration-libs/epd-visualization/core/public_api" + ], + "@spartacus/epd-visualization/epd-visualization-api": [ + "../../integration-libs/epd-visualization/epd-visualization-api/public_api" + ], + "@spartacus/epd-visualization": [ + "../../integration-libs/epd-visualization/public_api" + ], + "@spartacus/epd-visualization/root": [ + "../../integration-libs/epd-visualization/root/public_api" + ], + "@spartacus/opps": ["../../integration-libs/opps/public_api"], + "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], + "@spartacus/s4om/assets": [ + "../../integration-libs/s4om/assets/public_api" + ], + "@spartacus/s4om": ["../../integration-libs/s4om/public_api"], + "@spartacus/s4om/root": ["../../integration-libs/s4om/root/public_api"], + "@spartacus/segment-refs": [ + "../../integration-libs/segment-refs/public_api" + ], + "@spartacus/segment-refs/root": [ + "../../integration-libs/segment-refs/root/public_api" + ], + "@spartacus/assets": ["../../projects/assets/src/public_api"], + "@spartacus/core": ["../../projects/core/public_api"], + "@spartacus/storefront": ["../../projects/storefrontlib/public_api"] + }, + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "resolveJsonModule": true, + "esModuleInterop": true + }, + "include": ["schematics/**/*.ts"], + "exclude": ["schematics/*/files/**/*", "schematics/**/*_spec.ts"] +} diff --git a/integration-libs/s4-service/tsconfig.spec.json b/integration-libs/s4-service/tsconfig.spec.json new file mode 100644 index 00000000000..733ab743b7d --- /dev/null +++ b/integration-libs/s4-service/tsconfig.spec.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "module": "es2022", + "strict": false, + "types": ["jasmine", "node"] + }, + "files": ["test.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/integration-libs/s4om/tsconfig.schematics.json b/integration-libs/s4om/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/integration-libs/s4om/tsconfig.schematics.json +++ b/integration-libs/s4om/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/integration-libs/segment-refs/tsconfig.schematics.json b/integration-libs/segment-refs/tsconfig.schematics.json index 2e194148e09..b4c289b5c84 100644 --- a/integration-libs/segment-refs/tsconfig.schematics.json +++ b/integration-libs/segment-refs/tsconfig.schematics.json @@ -584,6 +584,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/package.json b/package.json index 46cabfe8023..11c482ee6de 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,11 @@ "build:checkout": "npm --prefix feature-libs/checkout run build:schematics && nx build checkout --configuration production", "build:customer-ticketing": "npm --prefix feature-libs/customer-ticketing run build:schematics && nx build customer-ticketing --configuration production", "build:digital-payments": "npm --prefix integration-libs/digital-payments run build:schematics && nx build digital-payments --configuration production", + "build:s4-service": "npm --prefix integration-libs/s4-service run build:schematics && nx build s4-service --configuration production", "build:epd-visualization": "npm --prefix integration-libs/epd-visualization run build:schematics && nx build epd-visualization --configuration production", "build:estimated-delivery-date": "npm --prefix feature-libs/estimated-delivery-date run build:schematics && nx build estimated-delivery-date --configuration production", "build:order": "npm --prefix feature-libs/order run build:schematics && nx build order --configuration production", - "build:libs": "nx build core --configuration production && nx build storefrontlib --configuration production && concurrently --kill-others-on-fail npm:build:schematics npm:build:user && npm run build:cart && npm run build:pdf-invoices && npm run build:order && npm run build:storefinder && concurrently --kill-others-on-fail npm:build:checkout npm:build:asm npm:build:tracking npm:build:customer-ticketing && concurrently --kill-others-on-fail npm:build:organization npm:build:product npm:build:product-configurator npm:build:requested-delivery-date && concurrently --kill-others-on-fail npm:build:estimated-delivery-date && concurrently --kill-others-on-fail npm:build:smartedit npm:build:qualtrics npm:build:assets npm:build:cds npm:build:cdc npm:build:cdp npm:build:digital-payments npm:build:epd-visualization npm:build:s4om npm:build:cpq-quote npm:build:segment-refs npm:build:opps npm:build:pickup-in-store npm:build:quote && npm run build:setup", + "build:libs": "nx build core --configuration production && nx build storefrontlib --configuration production && concurrently --kill-others-on-fail npm:build:schematics npm:build:user && npm run build:cart && npm run build:pdf-invoices && npm run build:order && npm run build:storefinder && concurrently --kill-others-on-fail npm:build:checkout npm:build:asm npm:build:tracking npm:build:customer-ticketing && concurrently --kill-others-on-fail npm:build:organization npm:build:product npm:build:product-configurator npm:build:requested-delivery-date && concurrently --kill-others-on-fail npm:build:estimated-delivery-date && concurrently --kill-others-on-fail npm:build:smartedit npm:build:qualtrics npm:build:assets npm:build:cds npm:build:cdc npm:build:cdp npm:build:digital-payments npm:build:epd-visualization npm:build:s4om npm:build:cpq-quote npm:build:segment-refs npm:build:opps npm:build:pickup-in-store npm:build:quote && npm run build:setup && npm run build:s4-service", "build:organization": "npm --prefix feature-libs/organization run build:schematics && nx build organization --configuration production", "build:pdf-invoices": "npm --prefix feature-libs/pdf-invoices run build:schematics && nx build pdf-invoices --configuration production", "build:pickup-in-store": "npm --prefix feature-libs/pickup-in-store run build:schematics && nx build pickup-in-store --configuration production", @@ -69,6 +70,7 @@ "e2e:run:ci:cdc": "npm --prefix ./projects/storefrontapp-e2e-cypress run cy:run:ci:cdc", "e2e:run:ci:cdp": "npm --prefix ./projects/storefrontapp-e2e-cypress run cy:run:ci:cdp", "e2e:run:ci:digital-payments": "npm --prefix ./projects/storefrontapp-e2e-cypress run cy:run:ci:digital-payments", + "e2e:run:ci:s4-service": "npm --prefix ./projects/storefrontapp-e2e-cypress run cy:run:ci:s4-service", "e2e:run:ci:epd-visualization": "npm --prefix ./projects/storefrontapp-e2e-cypress run cy:run:ci:epd-visualization", "e2e:run:ci:ccv2": "npm --prefix ./projects/storefrontapp-e2e-cypress run cy:run:ci:ccv2", "e2e:run:ci:ccv2-b2b": "npm --prefix ./projects/storefrontapp-e2e-cypress run cy:run:ci:ccv2-b2b", @@ -97,7 +99,7 @@ "start:pwa": "cd ./dist/storefrontapp/ && http-server --silent --proxy http://localhost:4200? -p 4200", "test": "nx test", "test:all-schematics": "set -e; npm --prefix ./projects/schematics test -- -u; for dir in feature-libs/* integration-libs/*; do (cd $dir && npm run test:schematics -- -u); done", - "test:libs": "concurrently \"nx test core --code-coverage\" \"nx test storefrontlib --code-coverage\" \"nx test cart --code-coverage\" \"nx test organization --code-coverage\" \"nx test storefinder --code-coverage\" \"nx test smartedit --code-coverage\" \"nx test asm --code-coverage\" \"nx test qualtrics --code-coverage\" \"nx test product --code-coverage\" \"nx test product-configurator --code-coverage\" \"nx test customer-ticketing --code-coverage\" \"nx test cdc --code-coverage\" \"nx test cdp --code-coverage\" \"nx test opps --code-coverage\" \"nx test setup --code-coverage\" \"nx test checkout --code-coverage\" \"nx test order --code-coverage\" \"nx test digital-payments --code-coverage\" \"nx test epd-visualization --code-coverage\" \"nx test pickup-in-store --code-coverage\" \"nx test s4om --code-coverage\" \"nx test cpq-quote --code-coverage\" \"nx test requested-delivery-date --code-coverage\" \"nx test estimated-delivery-date --code-coverage\" \"nx test pdf-invoices --code-coverage\" \"nx test quote --code-coverage\"", + "test:libs": "concurrently \"nx test core --code-coverage\" \"nx test storefrontlib --code-coverage\" \"nx test cart --code-coverage\" \"nx test organization --code-coverage\" \"nx test storefinder --code-coverage\" \"nx test smartedit --code-coverage\" \"nx test asm --code-coverage\" \"nx test qualtrics --code-coverage\" \"nx test product --code-coverage\" \"nx test product-configurator --code-coverage\" \"nx test customer-ticketing --code-coverage\" \"nx test cdc --code-coverage\" \"nx test s4-service --code-coverage\" \"nx test cdp --code-coverage\" \"nx test opps --code-coverage\" \"nx test setup --code-coverage\" \"nx test checkout --code-coverage\" \"nx test order --code-coverage\" \"nx test digital-payments --code-coverage\" \"nx test epd-visualization --code-coverage\" \"nx test pickup-in-store --code-coverage\" \"nx test s4om --code-coverage\" \"nx test cpq-quote --code-coverage\" \"nx test requested-delivery-date --code-coverage\" \"nx test estimated-delivery-date --code-coverage\" \"nx test pdf-invoices --code-coverage\" \"nx test quote --code-coverage\"", "test:storefront:lib": "nx test storefrontlib --source-map --code-coverage", "dev:ssr": "env-cmd --no-override -e dev,b2c,$SPA_ENV cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 nx run storefrontapp:serve-ssr", "serve:ssr": "node dist/storefrontapp-server/main.js", diff --git a/projects/schematics/src/add-spartacus/schema.json b/projects/schematics/src/add-spartacus/schema.json index 60335279246..2913e66eea3 100644 --- a/projects/schematics/src/add-spartacus/schema.json +++ b/projects/schematics/src/add-spartacus/schema.json @@ -52,6 +52,7 @@ "Requested-Delivery-Date", "Estimated-Delivery-Date", "S4HANA-Order-Management", + "s4-service", "SmartEdit", "Store-Finder", "Personalization", @@ -223,6 +224,10 @@ "value": "S4HANA-Order-Management", "label": "S/4HANA Order Management (b2b feature)" }, + { + "value": "s4-service", + "label": "S/4HANA Service Integration (b2b feature)" + }, { "value": "SmartEdit", "label": "SmartEdit" diff --git a/projects/schematics/src/dependencies.json b/projects/schematics/src/dependencies.json index 7b31cc823a4..4fe4be86961 100644 --- a/projects/schematics/src/dependencies.json +++ b/projects/schematics/src/dependencies.json @@ -401,6 +401,20 @@ "@spartacus/storefront": "2211.25.1", "rxjs": "^7.8.0" }, + "@spartacus/s4-service": { + "@angular-devkit/schematics": "^17.0.5", + "@angular/common": "^17.0.5", + "@angular/core": "^17.0.5", + "@angular/forms": "^17.0.5", + "@angular/router": "^17.0.5", + "@spartacus/cart": "2211.25.1", + "@spartacus/checkout": "2211.25.1", + "@spartacus/core": "2211.25.1", + "@spartacus/order": "2211.25.1", + "@spartacus/schematics": "2211.25.1", + "@spartacus/storefront": "2211.25.1", + "rxjs": "^7.8.0" + }, "@spartacus/s4om": { "@angular-devkit/schematics": "^17.0.5", "@angular/common": "^17.0.5", diff --git a/projects/schematics/src/shared/lib-configs/integration-libs/index.ts b/projects/schematics/src/shared/lib-configs/integration-libs/index.ts index 192e946efcb..d57a8c8d298 100644 --- a/projects/schematics/src/shared/lib-configs/integration-libs/index.ts +++ b/projects/schematics/src/shared/lib-configs/integration-libs/index.ts @@ -13,3 +13,4 @@ export * from './s4om-schematics-config'; export * from './segment-refs-schematics-config'; export * from './opps-schematics-config'; export * from './cpq-quote-schematics-config'; +export * from './s4-service-schematics-config'; diff --git a/projects/schematics/src/shared/lib-configs/integration-libs/s4-service-schematics-config.ts b/projects/schematics/src/shared/lib-configs/integration-libs/s4-service-schematics-config.ts new file mode 100644 index 00000000000..761e5275e8b --- /dev/null +++ b/projects/schematics/src/shared/lib-configs/integration-libs/s4-service-schematics-config.ts @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + S4_SERVICE_FEATURE_NAME, + SPARTACUS_S4_SERVICE, + SPARTACUS_S4_SERVICE_ROOT, + SPARTACUS_S4_SERVICE_ASSETS, + CHECKOUT_B2B_FEATURE_NAME, + ORDER_FEATURE_NAME, + SPARTACUS_S4_SERVICE_CHECKOUT, + SPARTACUS_S4_SERVICE_ORDER, +} from '../../libs-constants'; +import { SchematicConfig } from '../../utils'; +import { CHECKOUT_B2B_MODULE } from '../checkout-schematics-config'; +import { ORDER_MODULE } from '../order-schematics-config'; + +export const S4_SERVICE_FOLDER_NAME = 's4-service'; +export const S4_SERVICE_MODULE_NAME = 'S4Service'; +export const S4_SERVICE_SCSS_FILE_NAME = 's4-service.scss'; + +export const S4_SERVICE_MODULE = 'S4ServiceModule'; +export const S4_SERVICE_CHECKOUT_MODULE = 'S4ServiceCheckoutModule'; +export const S4_SERVICE_ORDER_MODULE = 'S4ServiceOrderModule'; +export const S4_SERVICE_ROOT_MODULE = 'S4ServiceRootModule'; +export const S4_SERVICE_FEATURE_NAME_CONSTANT = 'S4_SERVICE_FEATURE'; +export const S4_SERVICE_TRANSLATIONS = 's4ServiceTranslations'; +export const S4_SERVICE_TRANSLATION_CHUNKS_CONFIG = + 's4ServiceTranslationChunksConfig'; + +export const S4_SERVICE_SCHEMATICS_CONFIG: SchematicConfig = { + library: { + featureName: S4_SERVICE_FEATURE_NAME, + mainScope: SPARTACUS_S4_SERVICE, + b2b: true, + }, + folderName: S4_SERVICE_FOLDER_NAME, + moduleName: S4_SERVICE_MODULE_NAME, + featureModule: [ + { + name: S4_SERVICE_MODULE, + importPath: SPARTACUS_S4_SERVICE, + }, + { + name: S4_SERVICE_CHECKOUT_MODULE, + importPath: SPARTACUS_S4_SERVICE_CHECKOUT, + }, + { + name: S4_SERVICE_ORDER_MODULE, + importPath: SPARTACUS_S4_SERVICE_ORDER, + }, + ], + rootModule: { + name: S4_SERVICE_ROOT_MODULE, + importPath: SPARTACUS_S4_SERVICE_ROOT, + }, + lazyLoadingChunk: { + moduleSpecifier: SPARTACUS_S4_SERVICE_ROOT, + namedImports: [S4_SERVICE_FEATURE_NAME_CONSTANT], + }, + i18n: { + resources: S4_SERVICE_TRANSLATIONS, + chunks: S4_SERVICE_TRANSLATION_CHUNKS_CONFIG, + importPath: SPARTACUS_S4_SERVICE_ASSETS, + }, + dependencyFeatures: [CHECKOUT_B2B_FEATURE_NAME, ORDER_FEATURE_NAME], + styles: { + scssFileName: S4_SERVICE_SCSS_FILE_NAME, + importStyle: SPARTACUS_S4_SERVICE, + }, + importAfter: [ + { + markerModuleName: CHECKOUT_B2B_MODULE, + featureModuleName: S4_SERVICE_CHECKOUT_MODULE, + }, + { + markerModuleName: ORDER_MODULE, + featureModuleName: S4_SERVICE_ORDER_MODULE, + }, + ], +}; diff --git a/projects/schematics/src/shared/libs-constants.ts b/projects/schematics/src/shared/libs-constants.ts index f5cb0c8bcae..600fc3835d3 100644 --- a/projects/schematics/src/shared/libs-constants.ts +++ b/projects/schematics/src/shared/libs-constants.ts @@ -229,6 +229,12 @@ export const SPARTACUS_OPPS_ROOT = '@spartacus/opps/root'; export const SPARTACUS_SEGMENT_REFS = '@spartacus/segment-refs'; export const SPARTACUS_SEGMENT_REFS_ROOT = `@spartacus/segment-refs/root`; +export const SPARTACUS_S4_SERVICE = '@spartacus/s4-service'; +export const SPARTACUS_S4_SERVICE_ASSETS = `@spartacus/s4-service/assets`; +export const SPARTACUS_S4_SERVICE_ROOT = `@spartacus/s4-service/root`; +export const SPARTACUS_S4_SERVICE_CHECKOUT = `@spartacus/s4-service/checkout`; +export const SPARTACUS_S4_SERVICE_ORDER = `@spartacus/s4-service/order`; + export const SPARTACUS_CUSTOMER_TICKETING_ROOT = `@spartacus/customer-ticketing/root`; export const SPARTACUS_CUSTOMER_TICKETING_ASSETS = `@spartacus/customer-ticketing/assets`; export const SPARTACUS_CUSTOMER_TICKETING = '@spartacus/customer-ticketing'; @@ -315,6 +321,8 @@ export const S4OM_FEATURE_NAME = 'S4HANA-Order-Management'; export const SEGMENT_REFS_FEATURE_NAME = 'Segment-Refs'; +export const S4_SERVICE_FEATURE_NAME = 's4-service'; + export const OPPS_FEATURE_NAME = 'OPPS'; export const CUSTOMER_TICKETING_FEATURE_NAME = 'Customer-Ticketing'; diff --git a/projects/schematics/src/shared/schematics-config-mappings.ts b/projects/schematics/src/shared/schematics-config-mappings.ts index d8573dc333e..030634f9045 100644 --- a/projects/schematics/src/shared/schematics-config-mappings.ts +++ b/projects/schematics/src/shared/schematics-config-mappings.ts @@ -13,6 +13,7 @@ import { CDP_SCHEMATICS_CONFIG, QUOTE_SCHEMATICS_CONFIG, OPPS_SCHEMATICS_CONFIG, + S4_SERVICE_SCHEMATICS_CONFIG, } from './lib-configs'; import { CART_BASE_SCHEMATICS_CONFIG, @@ -152,6 +153,8 @@ export const SCHEMATICS_CONFIGS: SchematicConfig[] = [ S4OM_SCHEMATICS_CONFIG, + S4_SERVICE_SCHEMATICS_CONFIG, + SEGMENT_REFS_SCHEMATICS_CONFIG, CPQ_QUOTE_SCHEMATICS_CONFIG, ]; diff --git a/projects/schematics/src/shared/utils/graph-utils_spec.ts b/projects/schematics/src/shared/utils/graph-utils_spec.ts index c1911935433..67d7a8e1cdb 100644 --- a/projects/schematics/src/shared/utils/graph-utils_spec.ts +++ b/projects/schematics/src/shared/utils/graph-utils_spec.ts @@ -23,6 +23,7 @@ import { SPARTACUS_QUOTE, SPARTACUS_REQUESTED_DELIVERY_DATE, SPARTACUS_S4OM, + SPARTACUS_S4_SERVICE, SPARTACUS_SEGMENT_REFS, SPARTACUS_SMARTEDIT, SPARTACUS_STOREFINDER, @@ -145,6 +146,7 @@ describe('Graph utils', () => { SPARTACUS_ASM, SPARTACUS_SEGMENT_REFS, SPARTACUS_S4OM, + SPARTACUS_S4_SERVICE, SPARTACUS_OPPS, SPARTACUS_EPD_VISUALIZATION, SPARTACUS_DIGITAL_PAYMENTS, @@ -197,6 +199,7 @@ describe('Graph utils', () => { "ASM-Customer-360", "Cpq-Quote", "Segment-Refs", + "s4-service", "S4HANA-Order-Management", "OPPS", "EPD-Visualization", diff --git a/projects/schematics/src/shared/utils/test-utils.ts b/projects/schematics/src/shared/utils/test-utils.ts index 9b23b171f69..bcfe98caedd 100644 --- a/projects/schematics/src/shared/utils/test-utils.ts +++ b/projects/schematics/src/shared/utils/test-utils.ts @@ -48,6 +48,8 @@ export const quoteFeatureModulePath = 'src/app/spartacus/features/quote/quote-feature.module.ts'; export const orderFeatureModulePath = 'src/app/spartacus/features/order/order-feature.module.ts'; +export const orderWrapperModulePath = + 'src/app/spartacus/features/order/order-wrapper.module.ts'; export const organizationAdministrationFeatureModulePath = 'src/app/spartacus/features/organization/organization-administration-feature.module.ts'; export const pickupInStoreFeatureModulePath = @@ -111,6 +113,8 @@ export const segmentRefsFeatureModulePath = 'src/app/spartacus/features/segment-refs/segment-refs-feature.module.ts'; export const oppsFeatureModulePath = 'src/app/spartacus/features/opps/opps-feature.module.ts'; +export const s4ServiceFeatureModulePath = + 'src/app/spartacus/features/s4-service/s4-service-feature.module.ts'; export function writeFile( host: TempScopedNodeJsSyncHost, diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/vendor/s4-service/service-order-checkout-e2e.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/vendor/s4-service/service-order-checkout-e2e.cy.ts new file mode 100644 index 00000000000..e7c24d77c27 --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/vendor/s4-service/service-order-checkout-e2e.cy.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { placeOrder } from '../../../helpers/b2b/b2b-checkout'; +import { loginUser, signOutUser } from '../../../helpers/checkout-flow'; +import { + checkoutForServiceOrder, + nonServiceProduct, + selectAccountDeliveryModeForServiceOrder, + selectAccountPaymentForServiceOrder, + selectAccountShippingAddressForServiceOrder, + selectServiceDetailsForServiceOrder, + serviceProduct, + serviceUser, + verifyServiceOrderConfirmationPage, + verifyServiceOrderReviewOrderPage, +} from '../../../helpers/vendor/s4-service/s4-service'; +import { POWERTOOLS_BASESITE } from '../../../sample-data/b2b-checkout'; + +describe('Service Order Checkout Flow ', () => { + beforeEach(() => { + cy.restoreLocalStorage(); + Cypress.env('BASE_SITE', POWERTOOLS_BASESITE); + cy.visit('/powertools-spa/en/USD/login'); + loginUser(serviceUser); + cy.get('button').contains('Allow All').click(); + }); + it('with service products in cart', () => { + checkoutForServiceOrder(serviceProduct); + selectAccountPaymentForServiceOrder(); + selectAccountShippingAddressForServiceOrder(); + selectAccountDeliveryModeForServiceOrder(); + selectServiceDetailsForServiceOrder(true); + verifyServiceOrderReviewOrderPage(true); + placeOrder('/order-confirmation'); + verifyServiceOrderConfirmationPage(true); + }); + it('without any service products in cart', () => { + checkoutForServiceOrder(nonServiceProduct); + selectAccountPaymentForServiceOrder(); + selectAccountShippingAddressForServiceOrder(); + selectAccountDeliveryModeForServiceOrder(); + selectServiceDetailsForServiceOrder(false); + verifyServiceOrderReviewOrderPage(false); + placeOrder('/order-confirmation'); + verifyServiceOrderConfirmationPage(false); + }); + afterEach(() => { + signOutUser(); + }); +}); diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/vendor/s4-service/s4-service.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/vendor/s4-service/s4-service.ts new file mode 100644 index 00000000000..e3b3b70b84b --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/vendor/s4-service/s4-service.ts @@ -0,0 +1,258 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + POWERTOOLS_BASESITE, + b2bDeliveryAddress, +} from '../../../sample-data/b2b-checkout'; +import { SampleProduct } from '../../../sample-data/checkout-flow'; +import { verifyTabbingOrder } from '../../accessibility/tabbing-order'; +import { + interceptCheckoutB2BDetailsEndpoint, + interceptCostCenterEndpoint, + interceptPaymentTypesEndpoint, + interceptPutDeliveryModeEndpoint, +} from '../../b2b/b2b-checkout'; +import { + waitForProductPage, + addCheapProductToCart, + waitForPage, + verifyReviewOrderPage, +} from '../../checkout-flow'; +import { tabbingOrderConfig as config } from '../../../helpers/accessibility/b2b/tabbing-order.config'; + +export const serviceUser = { + email: 'james.weber@harvestlive.inc', + password: 'welcome', + firstName: 'James', + lastName: 'Weber', + titleCode: 'mr', +}; + +export const serviceProduct: SampleProduct = { + code: 'SRV_01', + name: 'SRV_01', +}; + +export const nonServiceProduct: SampleProduct = { + name: 'D10VC2', + code: '3887119', +}; + +export function checkoutForServiceOrder(product: SampleProduct) { + const productPage = waitForProductPage(product.code, 'getProductPage'); + const getPaymentTypes = interceptPaymentTypesEndpoint(); + + cy.visit(`${POWERTOOLS_BASESITE}/en/USD/product/${product.code}`); + cy.wait(`@${productPage}`).its('response.statusCode').should('eq', 200); + + cy.get('cx-product-intro').within(() => { + cy.get('.code').should('contain', product.code); + }); + + addCheapProductToCart(product); + + const paymentTypePage = waitForPage( + '/checkout/payment-type', + 'getPaymentType' + ); + cy.findByText(/proceed to checkout/i).click(); + cy.wait(`@${paymentTypePage}`).its('response.statusCode').should('eq', 200); + cy.wait(`@${getPaymentTypes}`).its('response.statusCode').should('eq', 200); +} + +export function selectAccountDeliveryModeForServiceOrder() { + const getCheckoutDetails = interceptCheckoutB2BDetailsEndpoint(); + + cy.get('.cx-checkout-title').should('contain', 'Delivery Method'); + cy.get('cx-delivery-mode input').first().should('be.checked'); + cy.get('.cx-checkout-btns button.btn-primary') + .should('be.enabled') + .click({ force: true }); + cy.wait(`@${getCheckoutDetails}`) + .its('response.statusCode') + .should('eq', 200); + + const serviceDetails = waitForPage( + '/checkout/service-details', + 'getServiceDetails' + ); + cy.wait(`@${serviceDetails}`, { timeout: 30000 }) + .its('response.statusCode') + .should('eq', 200); +} + +export function verifyServiceDatePickerExists() { + cy.get('cx-service-details') + .should('exist') + .within(() => { + cy.get('cx-date-picker').should('exist'); + cy.get('input[type="date"]', { timeout: 3000 }).should('not.be.disabled'); + }); +} + +export function interceptPatchServiceDetailsEndpoint() { + const alias = 'patchServiceDetails'; + cy.intercept('PATCH', '**/serviceOrder/serviceScheduleSlot**').as(alias); + return alias; +} + +export function selectServiceDetailsForServiceOrder(serviceOrder: boolean) { + const patchServiceDetails = interceptPatchServiceDetailsEndpoint(); + const orderReview = waitForPage('/checkout/review-order', 'getReviewOrder'); + const getCheckoutDetails = interceptCheckoutB2BDetailsEndpoint(); + + cy.get('.cx-checkout-title').should( + 'contain', + 'Service Schedule - Date and Time' + ); + + if (serviceOrder) { + verifyServiceDatePickerExists(); + cy.get('select[formcontrolname="scheduleTime"]') + .should('exist') + .and('not.be.empty'); + cy.get('.cx-checkout-btns button.btn-primary') + .should('be.enabled') + .click({ force: true }); + cy.wait(`@${patchServiceDetails}`) + .its('response.statusCode') + .should('eq', 200); + cy.wait(`@${getCheckoutDetails}`) + .its('response.statusCode') + .should('eq', 200); + } else { + cy.get('cx-service-details').within(() => { + cy.findByText('No Service details required. Click Continue'); + }); + cy.get('.cx-checkout-btns button.btn-primary') + .should('be.enabled') + .click({ force: true }); + } + + cy.wait(`@${orderReview}`, { timeout: 30000 }) + .its('response.statusCode') + .should('eq', 200); +} + +export function verifyServiceOrderReviewOrderPage(serviceOrder: boolean) { + verifyReviewOrderPage(); + + if (serviceOrder) { + cy.get('.cx-review-summary-card') + .contains('cx-card', 'Service Details') + .find('.cx-card-container') + .within(() => { + cy.findByText('Scheduled At'); + }); + cy.get('.cx-review-summary-card') + .contains('cx-card', 'Service Details') + .find('.cx-card-container .cx-card-label') + .should((div) => { + const text = div.text().trim(); + expect(text).to.match(/\d{2}\/\d{2}\/\d{4}, \d{2}:\d{2}:\d{2}/); + }); + } else { + cy.get('.cx-review-summary-card') + .contains('cx-card', 'Service Details') + .find('.cx-card-container') + .within(() => { + cy.findByText('None'); + }); + } + + cy.findByText('Terms & Conditions') + .should('have.attr', 'target', '_blank') + .should( + 'have.attr', + 'href', + `/${Cypress.env('BASE_SITE')}/en/USD/terms-and-conditions` + ); + cy.get('input[formcontrolname="termsAndConditions"]').check(); +} + +export function verifyServiceOrderConfirmationPage(serviceOrder: boolean) { + if (serviceOrder) { + cy.get('cx-card-service-details') + .contains('cx-card', 'Service Details') + .find('.cx-card-container') + .within(() => { + cy.findByText('Scheduled At'); + }); + cy.get('cx-card-service-details .cx-card-label').should((div) => { + const text = div.text().trim(); + expect(text).to.match(/\d{2}\/\d{2}\/\d{4}, \d{2}:\d{2}:\d{2}/); + }); + } else { + cy.get('cx-card-service-details') + .contains('cx-card', 'Service Details') + .find('.cx-card-container') + .within(() => { + cy.findByText('None'); + }); + } +} + +export function selectAccountPaymentForServiceOrder() { + const getCostCenters = interceptCostCenterEndpoint(); + + cy.get('cx-payment-type').within(() => { + cy.findByText('Account').click({ force: true }); + }); + + cy.intercept( + 'GET', + `${Cypress.env('OCC_PREFIX')}/${Cypress.env('BASE_SITE')}/users/current/carts/*?fields=DEFAULT*` + ).as('getCart'); + const deliveryAddressPage = waitForPage( + '/checkout/delivery-address', + 'getDeliveryPage' + ); + cy.get('button.btn-primary').should('be.enabled').click({ force: true }); + cy.wait(`@${deliveryAddressPage}`) + .its('response.statusCode') + .should('eq', 200); + + cy.wait(`@${getCostCenters}`).then((xhr) => { + if ( + !b2bDeliveryAddress.id && + xhr?.response?.body?.costCenters[0]?.unit?.addresses[0]?.id + ) { + b2bDeliveryAddress.id = + xhr.response.body.costCenters[0].unit.addresses[0].id; + } + }); +} + +export function selectAccountShippingAddressForServiceOrder() { + const putDeliveryMode = interceptPutDeliveryModeEndpoint(); + + cy.get('.cx-checkout-title').should('contain', 'Shipping Address'); + cy.get('cx-order-summary .cx-summary-partials .cx-summary-row') + .first() + .find('.cx-summary-amount') + .should('not.be.empty'); + + cy.get('cx-card').within(() => { + cy.get('.cx-card-label-bold').should('not.be.empty'); + }); + + cy.get('cx-card .card-header').should('contain', 'Selected'); + + const deliveryPage = waitForPage( + '/checkout/delivery-mode', + 'getDeliveryPage' + ); + + verifyTabbingOrder( + 'cx-page-layout.MultiStepCheckoutSummaryPageTemplate', + config.shippingAddressAccount + ); + + cy.get('button.btn-primary').should('be.enabled').click(); + cy.wait(`@${deliveryPage}`).its('response.statusCode').should('eq', 200); + cy.wait(`@${putDeliveryMode}`).its('response.statusCode').should('eq', 200); +} diff --git a/projects/storefrontapp-e2e-cypress/package-lock.json b/projects/storefrontapp-e2e-cypress/package-lock.json index b4c6363d9ce..86dd3321cd1 100644 --- a/projects/storefrontapp-e2e-cypress/package-lock.json +++ b/projects/storefrontapp-e2e-cypress/package-lock.json @@ -14,7 +14,7 @@ "@cypress/webpack-preprocessor": "^6.0.0", "@testing-library/cypress": "10.0.1", "babel-loader": "^9.0.0", - "cypress": "13.8.0", + "cypress": "^13.8.0", "cypress-file-upload": "^5.0.8", "ts-loader": "^9.4.2", "typescript": "^5.0.0", diff --git a/projects/storefrontapp-e2e-cypress/package.json b/projects/storefrontapp-e2e-cypress/package.json index 4b0b0ee772a..446193f5cbd 100644 --- a/projects/storefrontapp-e2e-cypress/package.json +++ b/projects/storefrontapp-e2e-cypress/package.json @@ -32,6 +32,7 @@ "cy:run:ci:cdc-b2b": "cypress run --config-file cypress.ci.json --env BASE_SITE=powertools-spa,OCC_PREFIX_USER_ENDPOINT=orgUsers,API_URL=https://api.cg79x9wuu9-eccommerc1-s1-public.model-t.myhybris.cloud/ --record --key $CYPRESS_KEY --tag \"2211,cdc\" --group CDC --spec \"cypress/integration/vendor/cdc/b2b/*.e2e.cy.ts\"", "cy:run:ci:cdp": "cypress run --config-file cypress.config.ci.ts --env API_URL=https://api.cg79x9wuu9-eccommerc1-s5-public.model-t.myhybris.cloud/ --record --key $CYPRESS_KEY --tag \"2005,cdp\" --group CDP --spec \"cypress/e2e/vendor/cdp/*.e2e.cy.ts\"", "cy:run:ci:digital-payments": "cypress run --config-file cypress.config.ci.ts --env API_URL=https://backoffice.cp96avkh5f-sapcxteam1-d5-public.model-t.cc.commerce.ondemand.com --record --key $CYPRESS_KEY --tag \"2105,digital-payments\" --group DIGITAL-PAYMENTS --spec \"cypress/e2e/vendor/digital-payments/*.e2e.cy.ts\"", + "cy:run:ci:s4-service": "cypress run --config-file cypress.config.ci.ts --env API_URL=https://api.cg79x9wuu9-eccommerc1-s1-public.model-t.myhybris.cloud/ --record --key $CYPRESS_KEY --tag \"s4-service\" --group s4-service --spec \"cypress/e2e/vendor/s4-service/*.e2e.cy.ts\"", "cy:run:ci:epd-visualization": "cypress run --config-file cypress.config.ci.ts --env API_URL=https://api.cp96avkh5f-integrati1-d1-public.model-t.cc.commerce.ondemand.com,BASE_SITE=powertools-epdvisualization-spa --record --key $CYPRESS_KEY --tag \"2105,epd-visualization\" --group EPD_VISUALIZATION --ci-build-id $BUILD_NUMBER --spec \"cypress/e2e/vendor/epd-visualization/**/*.e2e.cy.ts\"", "cy:run:ci:segment-refs": "cypress run --config-file cypress.config.ci.ts --env API_URL=https://api.cg79x9wuu9-eccommerc1-p7-public.model-t.myhybris.cloud/ --record --key $CYPRESS_KEY --tag \"segment-refs\" --group SEGMENT-REFS --spec \"cypress/e2e/vendor/segment-refs/*.e2e.cy.ts\"", "cy:run:ci:opps": "cypress run --config-file cypress.config.ci.ts --env API_URL=https://api.cg79x9wuu9-eccommerc1-s5-public.model-t.myhybris.cloud/ --record --key $CYPRESS_KEY --tag \"opps\" --group OPPS --spec \"cypress/e2e/vendor/opps/*.e2e.cy.ts\"", @@ -46,7 +47,7 @@ "@cypress/webpack-preprocessor": "^6.0.0", "@testing-library/cypress": "10.0.1", "babel-loader": "^9.0.0", - "cypress": "13.8.0", + "cypress": "^13.8.0", "cypress-file-upload": "^5.0.8", "ts-loader": "^9.4.2", "typescript": "^5.0.0", diff --git a/projects/storefrontapp/project.json b/projects/storefrontapp/project.json index dc0422fdac5..b7ae862f2f2 100644 --- a/projects/storefrontapp/project.json +++ b/projects/storefrontapp/project.json @@ -100,6 +100,10 @@ { "input": "projects/storefrontapp/src/styles/lib-quote.scss", "bundleName": "quote" + }, + { + "input": "projects/storefrontapp/src/styles/lib-s4-service.scss", + "bundleName": "s4-service" } ], "ngswConfigPath": "projects/storefrontlib/cms-structure/pwa/ngsw-config.json", diff --git a/projects/storefrontapp/src/app/spartacus/features/checkout/checkout-wrapper.module.ts b/projects/storefrontapp/src/app/spartacus/features/checkout/checkout-wrapper.module.ts index 0891457e076..dba5eedeb96 100644 --- a/projects/storefrontapp/src/app/spartacus/features/checkout/checkout-wrapper.module.ts +++ b/projects/storefrontapp/src/app/spartacus/features/checkout/checkout-wrapper.module.ts @@ -10,6 +10,7 @@ import { CheckoutModule } from '@spartacus/checkout/base'; import { CheckoutScheduledReplenishmentModule } from '@spartacus/checkout/scheduled-replenishment'; import { DigitalPaymentsModule } from '@spartacus/digital-payments'; import { environment } from '../../../../environments/environment'; +import { S4ServiceCheckoutModule } from '@spartacus/s4-service/checkout'; const extensions: Type[] = []; @@ -20,6 +21,9 @@ if (environment.b2b) { if (environment.digitalPayments) { extensions.push(DigitalPaymentsModule); } +if (environment.s4Service) { + extensions.push(S4ServiceCheckoutModule); +} @NgModule({ imports: [CheckoutModule, ...extensions], diff --git a/projects/storefrontapp/src/app/spartacus/features/order/order-feature.module.ts b/projects/storefrontapp/src/app/spartacus/features/order/order-feature.module.ts index f1a52dd0e1c..d29649bfd6b 100644 --- a/projects/storefrontapp/src/app/spartacus/features/order/order-feature.module.ts +++ b/projects/storefrontapp/src/app/spartacus/features/order/order-feature.module.ts @@ -23,7 +23,8 @@ import { environment } from '../../../../environments/environment'; provideConfig({ featureModules: { [ORDER_FEATURE]: { - module: () => import('@spartacus/order').then((m) => m.OrderModule), + module: () => + import('./order-wrapper.module').then((m) => m.OrderWrapperModule), }, }, }), diff --git a/projects/storefrontapp/src/app/spartacus/features/order/order-wrapper.module.ts b/projects/storefrontapp/src/app/spartacus/features/order/order-wrapper.module.ts new file mode 100644 index 00000000000..7b0a5b9953f --- /dev/null +++ b/projects/storefrontapp/src/app/spartacus/features/order/order-wrapper.module.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule, Type } from '@angular/core'; +import { OrderModule } from '@spartacus/order'; +import { environment } from '../../../../environments/environment'; +import { S4ServiceOrderModule } from '@spartacus/s4-service/order'; + +const extensions: Type[] = []; +if (environment.s4Service) { + extensions.push(S4ServiceOrderModule); +} +@NgModule({ + imports: [OrderModule, ...extensions], +}) +export class OrderWrapperModule {} diff --git a/projects/storefrontapp/src/app/spartacus/features/s4-service/s4-service-feature.module.ts b/projects/storefrontapp/src/app/spartacus/features/s4-service/s4-service-feature.module.ts new file mode 100644 index 00000000000..8897da961c1 --- /dev/null +++ b/projects/storefrontapp/src/app/spartacus/features/s4-service/s4-service-feature.module.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { CmsConfig, I18nConfig, provideConfig } from '@spartacus/core'; +import { + s4ServiceTranslationChunksConfig, + s4ServiceTranslations, +} from '@spartacus/s4-service/assets'; +import { + S4ServiceRootModule, + S4_SERVICE_FEATURE, +} from '@spartacus/s4-service/root'; + +@NgModule({ + imports: [S4ServiceRootModule], + providers: [ + provideConfig({ + featureModules: { + [S4_SERVICE_FEATURE]: { + module: () => + import('@spartacus/s4-service').then((m) => m.S4ServiceModule), + }, + }, + }), + provideConfig({ + i18n: { + resources: s4ServiceTranslations, + chunks: s4ServiceTranslationChunksConfig, + fallbackLang: 'en', + }, + }), + ], +}) +export class S4ServiceFeatureModule {} diff --git a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts index f6e9bf3b12e..20c33e9c554 100644 --- a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts +++ b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts @@ -101,6 +101,7 @@ import { SmartEditFeatureModule } from './features/smartedit/smartedit-feature.m import { StorefinderFeatureModule } from './features/storefinder/storefinder-feature.module'; import { TrackingFeatureModule } from './features/tracking/tracking-feature.module'; import { UserFeatureModule } from './features/user/user-feature.module'; +import { S4ServiceFeatureModule } from './features/s4-service/s4-service-feature.module'; import { CpqQuoteFeatureModule } from './features/cpq-quote/cpq-quote-feature.module'; const featureModules = []; @@ -122,7 +123,9 @@ if (environment.b2b) { if (environment.cdc) { featureModules.push(CdcFeatureModule); } - +if (environment.s4Service) { + featureModules.push(S4ServiceFeatureModule); +} if (environment.cds) { featureModules.push(CdsFeatureModule); } diff --git a/projects/storefrontapp/src/environments/environment.prod.ts b/projects/storefrontapp/src/environments/environment.prod.ts index 2681cc00da1..ca3f757651c 100644 --- a/projects/storefrontapp/src/environments/environment.prod.ts +++ b/projects/storefrontapp/src/environments/environment.prod.ts @@ -24,4 +24,5 @@ export const environment: Environment = { estimatedDeliveryDate: buildProcess.env.CX_ESTIMATED_DELIVERY_DATE, pdfInvoices: buildProcess.env.CX_PDF_INVOICES, myAccountV2: buildProcess.env.CX_MY_ACCOUNT_V2 ?? false, + s4Service: buildProcess.env.CX_S4_SERVICE, }; diff --git a/projects/storefrontapp/src/environments/environment.ts b/projects/storefrontapp/src/environments/environment.ts index a398bf8130a..dada8d77550 100644 --- a/projects/storefrontapp/src/environments/environment.ts +++ b/projects/storefrontapp/src/environments/environment.ts @@ -37,4 +37,5 @@ export const environment: Environment = { estimatedDeliveryDate: buildProcess.env.CX_ESTIMATED_DELIVERY_DATE ?? false, pdfInvoices: buildProcess.env.CX_PDF_INVOICES ?? false, myAccountV2: buildProcess.env.CX_MY_ACCOUNT_V2 ?? false, + s4Service: buildProcess.env.CX_S4_SERVICE ?? false, }; diff --git a/projects/storefrontapp/src/environments/models/build.process.env.d.ts b/projects/storefrontapp/src/environments/models/build.process.env.d.ts index e6ebfc54907..f86ee9c9e34 100644 --- a/projects/storefrontapp/src/environments/models/build.process.env.d.ts +++ b/projects/storefrontapp/src/environments/models/build.process.env.d.ts @@ -26,4 +26,5 @@ interface Env { CX_PDF_INVOICES: boolean; CX_MY_ACCOUNT_V2: boolean; CX_ESTIMATED_DELIVERY_DATE: boolean; + CX_S4_SERVICE: boolean; } diff --git a/projects/storefrontapp/src/environments/models/environment.model.ts b/projects/storefrontapp/src/environments/models/environment.model.ts index ceb4b89107e..4ca405f9a7d 100644 --- a/projects/storefrontapp/src/environments/models/environment.model.ts +++ b/projects/storefrontapp/src/environments/models/environment.model.ts @@ -22,4 +22,5 @@ export interface Environment { pdfInvoices: boolean; myAccountV2: boolean; estimatedDeliveryDate: boolean; + s4Service: boolean; } diff --git a/projects/storefrontapp/src/styles/lib-s4-service.scss b/projects/storefrontapp/src/styles/lib-s4-service.scss new file mode 100644 index 00000000000..f9b5ba9a8cc --- /dev/null +++ b/projects/storefrontapp/src/styles/lib-s4-service.scss @@ -0,0 +1,2 @@ +@import '../styles-config'; +@import '@spartacus/s4-service'; diff --git a/projects/storefrontapp/tsconfig.app.prod.json b/projects/storefrontapp/tsconfig.app.prod.json index ceeee2f4f20..335e967af7b 100644 --- a/projects/storefrontapp/tsconfig.app.prod.json +++ b/projects/storefrontapp/tsconfig.app.prod.json @@ -375,6 +375,11 @@ "@spartacus/epd-visualization/root": ["dist/epd-visualization/root"], "@spartacus/opps": ["dist/opps"], "@spartacus/opps/root": ["dist/opps/root"], + "@spartacus/s4-service/assets": ["dist/s4-service/assets"], + "@spartacus/s4-service/checkout": ["dist/s4-service/checkout"], + "@spartacus/s4-service": ["dist/s4-service"], + "@spartacus/s4-service/order": ["dist/s4-service/order"], + "@spartacus/s4-service/root": ["dist/s4-service/root"], "@spartacus/s4om/assets": ["dist/s4om/assets"], "@spartacus/s4om": ["dist/s4om"], "@spartacus/s4om/root": ["dist/s4om/root"], diff --git a/projects/storefrontapp/tsconfig.server.json b/projects/storefrontapp/tsconfig.server.json index 14896d928d8..f8fed2cea6d 100644 --- a/projects/storefrontapp/tsconfig.server.json +++ b/projects/storefrontapp/tsconfig.server.json @@ -587,6 +587,19 @@ ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], + "@spartacus/s4-service/assets": [ + "../../integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "../../integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": ["../../integration-libs/s4-service/public_api"], + "@spartacus/s4-service/order": [ + "../../integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "../../integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "../../integration-libs/s4om/assets/public_api" ], diff --git a/projects/storefrontapp/tsconfig.server.prod.json b/projects/storefrontapp/tsconfig.server.prod.json index 01a2ba6b71b..3c5a3db38a6 100644 --- a/projects/storefrontapp/tsconfig.server.prod.json +++ b/projects/storefrontapp/tsconfig.server.prod.json @@ -432,6 +432,11 @@ ], "@spartacus/opps": ["../../dist/opps"], "@spartacus/opps/root": ["../../dist/opps/root"], + "@spartacus/s4-service/assets": ["../../dist/s4-service/assets"], + "@spartacus/s4-service/checkout": ["../../dist/s4-service/checkout"], + "@spartacus/s4-service": ["../../dist/s4-service"], + "@spartacus/s4-service/order": ["../../dist/s4-service/order"], + "@spartacus/s4-service/root": ["../../dist/s4-service/root"], "@spartacus/s4om/assets": ["../../dist/s4om/assets"], "@spartacus/s4om": ["../../dist/s4om"], "@spartacus/s4om/root": ["../../dist/s4om/root"], diff --git a/scripts/install/config.default.sh b/scripts/install/config.default.sh index b8d06d176d4..42540e6a663 100644 --- a/scripts/install/config.default.sh +++ b/scripts/install/config.default.sh @@ -78,6 +78,7 @@ ADD_OPPS=false # config.epd-visualization.sh contains default values to use in your config.sh when ADD_EPD_VISUALIZATION is true. ADD_EPD_VISUALIZATION=false ADD_S4OM=false +ADD_S4_SERVICE=false # The base URL (origin) of the SAP EPD Visualization Fiori launchpad EPD_VISUALIZATION_BASE_URL= diff --git a/scripts/install/functions.sh b/scripts/install/functions.sh index 2962dd6f3e2..3c870dbd16c 100644 --- a/scripts/install/functions.sh +++ b/scripts/install/functions.sh @@ -154,6 +154,13 @@ function add_s4om { ng add --skip-confirmation @spartacus/s4om@${SPARTACUS_VERSION} --interactive false fi } + +function add_S4_SERVICE { + if [ "$ADD_S4_SERVICE" = true ] ; then + ng add --skip-confirmation @spartacus/s4-service@${SPARTACUS_VERSION} --interactive false + fi +} + function add_cpq-quote { if [ "$ADD_CPQ_QUOTE" = true ] ; then ng add --skip-confirmation @spartacus/cpq-quote@${SPARTACUS_VERSION} --interactive false @@ -205,6 +212,7 @@ function add_spartacus_csr { add_product_configurator add_quote add_s4om + add_S4_SERVICE add_requested_delivery_date add_estimated_delivery_date add_cpq-quote @@ -232,6 +240,7 @@ function add_spartacus_ssr { add_product_configurator add_quote add_s4om + add_S4_SERVICE add_requested_delivery_date add_estimated_delivery_date add_cpq-quote @@ -257,6 +266,7 @@ function add_spartacus_ssr_pwa { add_epd_visualization add_product_configurator add_s4om + add_S4_SERVICE add_requested_delivery_date add_estimated_delivery_date add_cpq-quote @@ -794,6 +804,11 @@ function parseInstallArgs { echo "➖ Added S4OM" shift ;; + s4Service) + ADD_S4_SERVICE=true + echo "➖ Added S/4HANA Service Integration" + shift + ;; cpq-quote) ADD_CPQ_QUOTE=true echo "➖ Added CPQ_QUOTE" diff --git a/tools/schematics/testing.ts b/tools/schematics/testing.ts index 60a00a8705f..1bfe4ad1084 100644 --- a/tools/schematics/testing.ts +++ b/tools/schematics/testing.ts @@ -40,6 +40,7 @@ const integrationLibsFolders: string[] = [ 's4om', 'segment-refs', 'opps', + 's4-service', 'cpq-quote', ]; @@ -52,6 +53,7 @@ const commands = [ 'build checkout/schematics', 'build quote/schematics', 'build cdc/schematics', + 'build s4-service/schematics', 'build cds/schematics', 'build digital-payments/schematics', 'build epd-visualization/schematics', @@ -207,6 +209,7 @@ async function executeCommand(command: Command): Promise { case 'build quote/schematics': case 'build cpq-quote/schematics': case 'build cdc/schematics': + case 'build s4-service/schematics': case 'build cds/schematics': case 'build digital-payments/schematics': case 'build epd-visualization/schematics': diff --git a/tsconfig.compodoc.json b/tsconfig.compodoc.json index 2e089629368..7dba3b28271 100644 --- a/tsconfig.compodoc.json +++ b/tsconfig.compodoc.json @@ -674,6 +674,21 @@ "@spartacus/opps/root": [ "integration-libs/opps/root/public_api" ], + "@spartacus/s4-service/assets": [ + "integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": [ + "integration-libs/s4-service/public_api" + ], + "@spartacus/s4-service/order": [ + "integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "integration-libs/s4om/assets/public_api" ], diff --git a/tsconfig.json b/tsconfig.json index e05863b223e..1a6bd4fd094 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -678,6 +678,21 @@ "@spartacus/opps/root": [ "integration-libs/opps/root/public_api" ], + "@spartacus/s4-service/assets": [ + "integration-libs/s4-service/assets/public_api" + ], + "@spartacus/s4-service/checkout": [ + "integration-libs/s4-service/checkout/public_api" + ], + "@spartacus/s4-service": [ + "integration-libs/s4-service/public_api" + ], + "@spartacus/s4-service/order": [ + "integration-libs/s4-service/order/public_api" + ], + "@spartacus/s4-service/root": [ + "integration-libs/s4-service/root/public_api" + ], "@spartacus/s4om/assets": [ "integration-libs/s4om/assets/public_api" ],