Skip to content

Commit

Permalink
feat: support for configurable products via Tacton CPQ integration (#329
Browse files Browse the repository at this point in the history
)

see https://github.com/intershop/intershop-pwa/blob/develop/docs/guides/tacton-product-configuration.md

Co-authored-by: Silke Grueber <SGrueber@intershop.com>
Co-authored-by: Susanne Schneider <s.schneider@intershop.de>
  • Loading branch information
3 people authored Aug 24, 2020
1 parent 2ae504e commit 2a60f9d
Show file tree
Hide file tree
Showing 108 changed files with 3,745 additions and 127 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/demo-server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
GROUP: ${{ secrets.AZURE_DEMO_RESOURCEGROUP }}
run: |
az webapp config container set --resource-group $GROUP --name $APP --docker-registry-server-user ${{ secrets.DOCKER_REGISTRY_USERNAME }} --docker-registry-server-password ${{ secrets.DOCKER_REGISTRY_PASSWORD }} --docker-custom-image-name $DOCKER_IMAGE_UNIVERSAL || az webapp create --resource-group $GROUP --plan ${{ secrets.AZURE_DEMO_APPSERVICEPLAN }} --name $APP --docker-registry-server-user ${{ secrets.DOCKER_REGISTRY_USERNAME }} --docker-registry-server-password ${{ secrets.DOCKER_REGISTRY_PASSWORD }} --deployment-container-image-name $DOCKER_IMAGE_UNIVERSAL
az webapp config appsettings set -g $GROUP -n $APP --settings LOGGING=true ICM_BASE_URL=$ICM_BASE_URL ICM_CHANNEL=inSPIRED-inTRONICS_Business-Site THEME=blue\|688dc3 FEATURES=compare,recently,tracking,sentry,advancedVariationHandling,businessCustomerRegistration,quoting,quickorder,orderTemplates
az webapp config appsettings set -g $GROUP -n $APP --settings LOGGING=true ICM_BASE_URL=$ICM_BASE_URL ICM_CHANNEL=inSPIRED-inTRONICS_Business-Site THEME=blue\|688dc3 FEATURES=compare,recently,tracking,sentry,advancedVariationHandling,businessCustomerRegistration,quoting,quickorder,orderTemplates TACTON='${{ secrets.TACTON }}'
az webapp deployment container config -g $GROUP -n $APP --enable-cd true
echo "B2B channel: http://$APP.azurewebsites.net"
Expand Down
12 changes: 8 additions & 4 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ deploy_demo:
--env LOGGING=true
--env PROXY_ICM=true
--env SENTRY_DSN=${SENTRY_DSN}
--env TACTON="${TACTON}"
--add-host $DEMO_SERVER_NAME:$DEMO_SERVER_IP
--add-host b2b.$DEMO_SERVER_NAME:$DEMO_SERVER_IP
--add-host b2c.$DEMO_SERVER_NAME:$DEMO_SERVER_IP
Expand Down Expand Up @@ -240,9 +241,10 @@ deploy_demo_b2b:
--env LOGGING=true
--env PROXY_ICM=true
--env THEME="blue|688dc3"
--env FEATURES=quoting,quickorder,orderTemplates,compare,recently,businessCustomerRegistration,advancedVariationHandling,sentry
--env FEATURES=quoting,quickorder,orderTemplates,compare,recently,businessCustomerRegistration,advancedVariationHandling,sentry,tacton
--env ICM_CHANNEL=inSPIRED-inTRONICS_Business-Site
--env SENTRY_DSN=${SENTRY_DSN}
--env TACTON="${TACTON}"
${IMAGE}
environment:
name: demo-b2b
Expand Down Expand Up @@ -281,7 +283,7 @@ deploy_demo_nginx:
-e PWA_2_LANG=de_DE
-e PWA_3_TOPLEVELDOMAIN=com
-e PWA_3_CHANNEL=inSPIRED-inTRONICS_Business-Site
-e PWA_3_FEATURES=quoting,quickorder,orderTemplates,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry
-e PWA_3_FEATURES=quoting,quickorder,orderTemplates,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry,tacton
-e PWA_3_THEME="blue|688dc3"
-e PWA_4_TOPLEVELDOMAIN=fr
-e PWA_4_LANG=fr_FR
Expand Down Expand Up @@ -383,6 +385,7 @@ deploy_review_b2c:
-e LOGGING=true
-e PROXY_ICM=true
-e SENTRY_DSN=${SENTRY_DSN}
-e TACTON="${TACTON}"
-e ICM_BASE_URL=${ICM_BASE_URL}
--add-host $ICM_HOST:$ICM_IP
--add-host b2b.$CI_COMMIT_REF_SLUG.$DEMO_SERVER_NAME:$DEMO_SERVER_IP
Expand Down Expand Up @@ -412,7 +415,7 @@ deploy_review_b2c:
-e PWA_2_LANG=de_DE
-e PWA_3_TOPLEVELDOMAIN=com
-e PWA_3_CHANNEL=inSPIRED-inTRONICS_Business-Site
-e PWA_3_FEATURES=quoting,quickorder,orderTemplates,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry
-e PWA_3_FEATURES=quoting,quickorder,orderTemplates,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry,tacton
-e PWA_3_THEME="blue|688dc3"
-e PWA_4_TOPLEVELDOMAIN=fr
-e PWA_4_LANG=fr_FR
Expand Down Expand Up @@ -481,10 +484,11 @@ deploy_review_b2b:
-e LOGGING=true
-e PROXY_ICM=true
-e SENTRY_DSN=${SENTRY_DSN}
-e TACTON="${TACTON}"
-e ICM_BASE_URL=${ICM_BASE_URL}
-e THEME="blue|688dc3"
-e ICM_CHANNEL=inSPIRED-inTRONICS_Business-Site
-e FEATURES=quoting,quickorder,orderTemplates,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry
-e FEATURES=quoting,quickorder,orderTemplates,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry,tacton
--add-host $ICM_HOST:$ICM_IP
--add-host $DEMO_SERVER_NAME:$DEMO_SERVER_IP
${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}-${CI_BUILD_REF}
Expand Down
7 changes: 4 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ kb_sync_latest_only
- [Guide - SSR Parameters](./guides/ssr-startup.md)
- [Concept - Hybrid Approach](./concepts/hybrid-approach.md)
- [Guide - Hybrid Approach and ICM URL Rewriting](./guides/hybrid-approach-icm-url-rewriting.md)
- [Concept - Logging](./concepts/logging.md)
- [Guide - CI](./guides/continuous-integration.md)
- [Guide - Google Tag Manager](./guides/google-tag-manager.md)

### Monitoring
### Third-party Integrations

- [Concept - Logging](./concepts/logging.md)
- [Guide - Google Tag Manager](./guides/google-tag-manager.md)
- [Guide - Client-Side Error Monitoring with Sentry](./guides/sentry-error-monitoring.md)
- [Guide - Extended Product Configurations with Tacton](./guides/tacton-product-configuration.md)
3 changes: 2 additions & 1 deletion docs/check-dead-links.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,11 @@ glob('**/*.md')
.filter((val, idx, arr) => arr.indexOf(val) === idx)
.filter(
link =>
!link.includes('github.com') &&
!link.includes('tacton.com') &&
!link.includes('repository.intershop.de') &&
!link.includes('support.intershop.com') &&
!link.includes('azurewebsites.net') &&
!link.includes('github.com') &&
!link.includes('github.com/intershop/intershop-pwa/commit')
)
.sort();
Expand Down
58 changes: 58 additions & 0 deletions docs/guides/tacton-product-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!--
kb_guide
kb_pwa
kb_everyone
kb_sync_latest_only
-->

# Extended Product Configurations with Tacton

We integrated [Tacton CPQ](https://www.tacton.com/solutions/tacton-cpq) for handling complex product configuration scenarios.

The PWA uses the self-service API to interactively configure a product and submit a firm proposal to Tacton CPQ.

## Setup

First, activate the feature toggle `tacton`.
You will also have to provide an endpoint and mapping configuration.
This can be done by defining it in [Angular CLI environment](../concepts/configuration.md#angular-cli-environments) files:

```typescript
export const environment: Environment = {
...ENVIRONMENT_DEFAULTS,

tacton: {
selfService: {
endPoint: '<tacton-endpoint>', // without '/self-service-api'
apiKey: '<self-service API key>'
},
productMappings: {
'<ICM-SKU>': '<tab-category>/<tacton-product-id>',
...
}
},
};
```

This configuration can also be supplied via environment variable `TACTON` as stringified JSON:

```text
TACTON='{ "selfService": { "endPoint": "<tacton-endpoint>", "apiKey": "<self-service API key>" }, "productMappings": { "<ICM-SKU>": "<tab-category>/<tacton-product-id>", ... } }';
```

## Product Mappings

Currently we only support product mappings via configuration.
In the future we will consider supporting specific custom attributes configurable via ICM back office.

## Workflow

When encountering a configurable product, the PWA directs to the configuration page, where the product can be composed.
The PWA supports committing and un-committing various parameters with available UI elements.
When encountering configuration conflicts, the user is queried for accepting a given conflict resolution or discarding the last changes.
Upon completing the last step, the user can submit the configuration.
This creates a cart with required user attributes on the Tacton CPQ side and submits a Firm Proposal.

## Further References

- [Concept - Configuration](../concepts/configuration.md)
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CoreModule } from 'ish-core/core.module';

import { AppComponent } from './app.component';
import { QuickorderRoutingModule } from './extensions/quickorder/pages/quickorder-routing.module';
import { TactonRoutingModule } from './extensions/tacton/pages/tacton-routing.module';
import { AppLastRoutingModule } from './pages/app-last-routing.module';
import { AppRoutingModule } from './pages/app-routing.module';
import { ShellModule } from './shell/shell.module';
Expand All @@ -19,6 +20,7 @@ import { ShellModule } from './shell/shell.module';
ShellModule,
AppRoutingModule,
QuickorderRoutingModule,
TactonRoutingModule,
AppLastRoutingModule,
],
bootstrap: [AppComponent],
Expand Down
24 changes: 20 additions & 4 deletions src/app/core/directives/feature-toggle.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,35 @@ import { FeatureToggleService } from 'ish-core/utils/feature-toggle/feature-togg
selector: '[ishFeature]',
})
export class FeatureToggleDirective {
// tslint:disable-next-line: no-any
private otherTemplateRef: TemplateRef<any>;
private feature: string;

constructor(
private templateRef: TemplateRef<unknown>,
private viewContainer: ViewContainerRef,
private featureToggle: FeatureToggleService
) {}

@Input() set ishFeature(val) {
const enabled = this.featureToggle.enabled(val);
@Input() set ishFeature(feature: string) {
this.feature = feature;
this.updateView();
}

// tslint:disable-next-line: no-any
@Input() set ishFeatureElse(otherTemplateRef: TemplateRef<any>) {
this.otherTemplateRef = otherTemplateRef;
this.updateView();
}

private updateView() {
const enabled = this.featureToggle.enabled(this.feature);

this.viewContainer.clear();
if (enabled) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
} else if (this.otherTemplateRef) {
this.viewContainer.createEmbeddedView(this.otherTemplateRef);
}
}
}
4 changes: 2 additions & 2 deletions src/app/core/guards/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ export class AuthGuard implements CanActivate, CanActivateChild {
) {}

canActivate(snapshot: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.guardAccess({ ...snapshot.queryParams, returnUrl: state.url });
return this.guardAccess({ ...snapshot.data?.queryParams, ...snapshot.queryParams, returnUrl: state.url });
}

canActivateChild(snapshot: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.guardAccess({ ...snapshot.queryParams, returnUrl: state.url });
return this.guardAccess({ ...snapshot.data?.queryParams, ...snapshot.queryParams, returnUrl: state.url });
}

private guardAccess(queryParams: Params): Observable<boolean | UrlTree> {
Expand Down
6 changes: 6 additions & 0 deletions src/app/core/icon.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import {
faAngleDown,
faAngleRight,
faAngleUp,
faArrowAltCircleRight,
faArrowsAlt,
faBars,
faCheck,
faCog,
faCogs,
faColumns,
faFastForward,
faGlobeAmericas,
Expand All @@ -36,6 +38,7 @@ import {
faTimes,
faTimesCircle,
faTrashAlt,
faUndo,
faUser,
} from '@fortawesome/free-solid-svg-icons';

Expand All @@ -52,9 +55,11 @@ export class IconModule {
faAngleRight,
faAngleUp,
faArrowsAlt,
faArrowAltCircleRight,
faBars,
faCheck,
faCog,
faCogs,
faColumns,
faGlobeAmericas,
faHome,
Expand All @@ -77,6 +82,7 @@ export class IconModule {
faTimes,
faTimesCircle,
faTrashAlt,
faUndo,
faUser,
faStar,
faStarHalf,
Expand Down
27 changes: 26 additions & 1 deletion src/app/core/utils/meta-reducers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Action, ActionReducer } from '@ngrx/store';
import { Action, ActionReducer, MetaReducer } from '@ngrx/store';
import { isEqual } from 'lodash-es';

import { logoutUser } from 'ish-core/store/customer/user';

Expand All @@ -10,3 +11,27 @@ export function resetOnLogoutMeta(reducer: ActionReducer<{}>): ActionReducer<{}>
return reducer(state, action);
};
}

export function localStorageSaveMeta<S>(prefix: string, key: keyof S & string): MetaReducer<S, Action> {
if (!key?.startsWith('_')) {
console.warn('localStorageSaveMeta:', `store key ${prefix}-${key} is not excluded from universal state transfer.`);
}
const item = `${prefix}-${key}`;
return (reducer): ActionReducer<S> => (state: S, action: Action) => {
if (typeof window !== 'undefined' && action.type !== '@ngrx/store-devtools/recompute') {
let incomingState = state;
if (!incomingState?.[key]) {
const fromStorage = localStorage.getItem(item);
if (fromStorage) {
incomingState = { ...state, [key]: JSON.parse(fromStorage) };
}
}
const newState = reducer(incomingState, action);
if (newState?.[key] !== state?.[key] && !isEqual(newState?.[key], state?.[key])) {
localStorage.setItem(item, JSON.stringify(newState?.[key]));
}
return newState;
}
return reducer(state, action);
};
}
1 change: 1 addition & 0 deletions src/app/extensions/quoting/store/quote/quote.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export class QuoteEffects {
routeListenerForSelectingQuote$ = createEffect(() =>
this.store.pipe(
select(selectRouteParam('quoteId')),
whenTruthy(),
map(id => selectQuote({ id }))
)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ChangeDetectorRef, Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
import { ReplaySubject, Subject } from 'rxjs';
import { map, switchMap, takeUntil } from 'rxjs/operators';

import { ProductView } from 'ish-core/models/product-view/product-view.model';

import { TactonFacade } from '../facades/tacton.facade';

@Directive({
selector: '[ishIsTactonProduct]',
})
export class IsTactonProductDirective implements OnDestroy {
private otherTemplateRef: TemplateRef<unknown>;
private sku$ = new ReplaySubject<string>(1);
private trigger$ = new Subject<void>();

private destroy$ = new Subject();

constructor(
private templateRef: TemplateRef<unknown>,
private viewContainer: ViewContainerRef,
private cdRef: ChangeDetectorRef,
tactonFacade: TactonFacade
) {
this.trigger$
.pipe(
switchMap(() => tactonFacade.getTactonProductForSKU$(this.sku$).pipe(map(x => !!x))),
takeUntil(this.destroy$)
)
.subscribe(exists => this.updateView(exists));
}

@Input() set ishIsTactonProduct(product: ProductView) {
this.sku$.next(product?.sku);
this.trigger$.next();
}

@Input() set ishIsTactonProductElse(otherTemplateRef: TemplateRef<unknown>) {
this.otherTemplateRef = otherTemplateRef;
this.trigger$.next();
}

private updateView(exists: boolean) {
this.viewContainer.clear();
if (exists) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else if (this.otherTemplateRef) {
this.viewContainer.createEmbeddedView(this.otherTemplateRef);
}
this.cdRef.markForCheck();
}

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
1 change: 1 addition & 0 deletions src/app/extensions/tacton/exports/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*/**/*.*
22 changes: 22 additions & 0 deletions src/app/extensions/tacton/exports/tacton-exports.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';

import { FeatureToggleModule } from 'ish-core/feature-toggle.module';
import { LAZY_FEATURE_MODULE } from 'ish-core/utils/module-loader/module-loader.service';

import { IsTactonProductDirective } from '../directives/is-tacton-product.directive';

import { LazyTactonConfigureProductComponent } from './lazy-tacton-configure-product/lazy-tacton-configure-product.component';

@NgModule({
imports: [FeatureToggleModule],
providers: [
{
provide: LAZY_FEATURE_MODULE,
useValue: { feature: 'tacton', location: import('../store/tacton-store.module') },
multi: true,
},
],
declarations: [IsTactonProductDirective, LazyTactonConfigureProductComponent],
exports: [IsTactonProductDirective, LazyTactonConfigureProductComponent],
})
export class TactonExportsModule {}
Loading

0 comments on commit 2a60f9d

Please sign in to comment.