Skip to content

Commit

Permalink
feat: support for cXML punchout self service configuration (#1683)
Browse files Browse the repository at this point in the history
Co-authored-by: Stefan Hauke <[email protected]>
Co-authored-by: Silke <[email protected]>
  • Loading branch information
3 people committed Aug 20, 2024
1 parent abba089 commit 74bb9cf
Show file tree
Hide file tree
Showing 40 changed files with 1,161 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('Punchout MyAccount Functionality', () => {
page.submit();
});
at(PunchoutOverviewPage, page => {
page.selectOciTab();
page.userList.should('contain', `${_.punchoutUser2.login}`);
page.userList.should('not.contain', `Inactive`);
page.successMessage.message.should('contain', 'created');
Expand Down
4 changes: 4 additions & 0 deletions src/app/core/icon.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
faCalendarDay,
faCheck,
faCheckCircle,
faChevronDown,
faChevronUp,
faCog,
faCogs,
faEnvelope,
Expand Down Expand Up @@ -77,6 +79,8 @@ export class IconModule {
faCalendarDay,
faCheck,
faCheckCircle,
faChevronDown,
faChevronUp,
faCog,
faCogs,
faEnvelope,
Expand Down
56 changes: 56 additions & 0 deletions src/app/core/interceptors/icm-error-mapper.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,62 @@ describe('Icm Error Mapper Interceptor', () => {
);
});

it('should convert ICM errors format with cause (new ADR) to simplified format concatenating all causes', done => {
http.get('some').subscribe({
next: fail,
error: error => {
expect(error).toMatchInlineSnapshot(`
{
"errors": [
{
"causes": [
{
"code": "intershop.cxml.punchout.unitmapping.value.invalid",
"message": "The value must be a tab-separated list of 'value1;value2' pairs.",
},
{
"code": "intershop.cxml.punchout.punchout.locale.value.invalid",
"message": "The value must be two lowercase letters for language and two uppercase letters for region.",
},
],
"code": "intershop.cxml.punchout.configuration.error",
"level": "ERROR",
"status": "400",
},
],
"message": "<div>The value must be a tab-separated list of 'value1;value2' pairs.</div><div>The value must be two lowercase letters for language and two uppercase letters for region.</div>",
"name": "HttpErrorResponse",
"status": 422,
}
`);
done();
},
});

httpController.expectOne('some').flush(
{
messages: [
{
causes: [
{
code: 'intershop.cxml.punchout.unitmapping.value.invalid',
message: "The value must be a tab-separated list of 'value1;value2' pairs.",
},
{
code: 'intershop.cxml.punchout.punchout.locale.value.invalid',
message: 'The value must be two lowercase letters for language and two uppercase letters for region.',
},
],
code: 'intershop.cxml.punchout.configuration.error',
level: 'ERROR',
status: '400',
},
],
},
{ status: 422, statusText: 'Unprocessable Entity' }
);
});

it('should convert ICM errors format to simplified format', done => {
http.get('some').subscribe({
next: fail,
Expand Down
24 changes: 24 additions & 0 deletions src/app/core/interceptors/icm-error-mapper.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,30 @@ export class ICMErrorMapperInterceptor implements HttpInterceptor {
errors: httpError.error?.errors,
};
}
// new ADR - used for cXML Punchout configuration
else if (httpError.error?.messages?.length) {
const errors: {
code: string;
causes?: {
code: string;
message: string;
}[];
}[] = httpError.error?.messages;
if (errors.length === 1) {
const error = errors[0];
if (error.causes?.length) {
return {
...responseError,
errors: httpError.error.messages,
message: error.causes.map(c => '<div>'.concat(c.message).concat('</div>')).join(''),
};
}
}
return {
...responseError,
errors: httpError.error?.errors,
};
}

// handle all other error responses with error object
return {
Expand Down
24 changes: 24 additions & 0 deletions src/app/extensions/punchout/facades/punchout.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ import { selectQueryParam } from 'ish-core/store/core/router';
import { decamelizeString } from 'ish-core/utils/functions';
import { whenTruthy } from 'ish-core/utils/operators';

import { CxmlConfiguration } from '../models/cxml-configuration/cxml-configuration.model';
import { OciConfigurationItem } from '../models/oci-configuration-item/oci-configuration-item.model';
import { PunchoutType, PunchoutUser } from '../models/punchout-user/punchout-user.model';
import {
cxmlConfigurationActions,
getCxmlConfiguration,
getCxmlConfigurationError,
getCxmlConfigurationLoading,
} from '../store/cxml-configuration';
import {
getOciConfiguration,
getOciConfigurationError,
Expand Down Expand Up @@ -101,4 +108,21 @@ export class PunchoutFacade {
updateOciConfiguration(configuration: OciConfigurationItem[]) {
this.store.dispatch(ociConfigurationActions.updateOCIConfiguration({ configuration }));
}

cxmlConfiguration$() {
this.store.dispatch(cxmlConfigurationActions.loadCXMLConfiguration());
return this.store.pipe(select(getCxmlConfiguration));
}

cxmlConfigurationLoading$ = this.store.pipe(select(getCxmlConfigurationLoading));

cxmlConfigurationError$ = this.store.pipe(select(getCxmlConfigurationError));

updateCxmlConfiguration(configuration: CxmlConfiguration[]) {
this.store.dispatch(cxmlConfigurationActions.updateCXMLConfiguration({ configuration }));
}

resetCxmlConfiguration() {
this.store.dispatch(cxmlConfigurationActions.resetCXMLConfiguration());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CxmlConfigurationInputType } from './cxml-configuration.model';

export interface CxmlConfigurationData {
data: {
name: string;
value: string;
}[];
info?: {
metaData: {
name: string;
defaultValue?: string;
description?: string;
inputType?: CxmlConfigurationInputType;
}[];
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { CxmlConfigurationData } from './cxml-configuration.interface';
import { CxmlConfigurationMapper } from './cxml-configuration.mapper';

describe('Cxml Configuration Mapper', () => {
describe('fromData', () => {
it('should return Cxml Configuration when getting CxmlConfigurationData', () => {
expect(() => CxmlConfigurationMapper.fromData(undefined)).toThrow();
});

it('should map incoming data to model data', () => {
const data: CxmlConfigurationData = {
data: [
{
name: 'classificationCatalogID',
value: '1',
},
],
info: {
metaData: [
{
name: 'classificationCatalogID',
defaultValue: 'eCl@ass',
description: 'Enter the type of product classification catalog to be used, e.g. UNSPSC or eCl@ass.',
inputType: 'text-short',
},
],
},
};
const mapped = CxmlConfigurationMapper.fromData(data);
expect(mapped).toMatchInlineSnapshot(`
[
{
"defaultValue": "eCl@ass",
"description": "Enter the type of product classification catalog to be used, e.g. UNSPSC or eCl@ass.",
"inputType": "text-short",
"name": "classificationCatalogID",
"value": "1",
},
]
`);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';

import { CxmlConfigurationData } from './cxml-configuration.interface';
import { CxmlConfiguration } from './cxml-configuration.model';

@Injectable({ providedIn: 'root' })
export class CxmlConfigurationMapper {
static fromData(cxmlConfigurationData: CxmlConfigurationData): CxmlConfiguration[] {
if (cxmlConfigurationData) {
const { data, info } = cxmlConfigurationData;

return data?.map(cxmlConfiguration => {
const infoElement = info?.metaData.find(e => e.name === cxmlConfiguration.name);
return {
...cxmlConfiguration,
defaultValue: infoElement?.defaultValue,
description: infoElement?.description,
inputType: infoElement?.inputType,
};
});
} else {
throw new Error(`CxmlConfigurationData is required`);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface CxmlConfiguration {
name: string;
value: string;
defaultValue?: string;
description?: string;
inputType?: CxmlConfigurationInputType;
}

export type CxmlConfigurationInputType = 'text-short' | 'text-long';
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,27 @@
<div class="list-body">
<formly-form [model]="model$ | async" [fields]="fields$ | async" [form]="form" class="pt-1" />
</div>
<div class="row justify-content-end">
<button
type="submit"
class="btn btn-primary"
[disabled]="formDisabled"
data-testing-id="update-oci-configuration"
>
{{ 'account.update.button.label' | translate }}
</button>
<a [routerLink]="['/account/punchout']" [queryParams]="{ format: 'oci' }" class="btn btn-secondary">{{
'account.cancel.link' | translate
}}</a>
<div class="row">
<div class="button-group w-100 clearfix">
<div class="float-md-right">
<button
type="submit"
class="btn btn-primary"
[disabled]="formDisabled"
data-testing-id="update-oci-configuration"
>
{{ 'account.update.button.label' | translate }}
</button>
<a [routerLink]="['/account/punchout']" [queryParams]="{ format: 'oci' }" class="btn btn-secondary">{{
'account.cancel.link' | translate
}}</a>
</div>
<div class="float-md-left">
<a class="btn btn-link pl-md-0" [routerLink]="['/account/punchout']" [queryParams]="{ format: 'oci' }">{{
'account.punchout.configuration.back_to_list' | translate
}}</a>
</div>
</div>
</div>
</form>
</ng-container>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<ng-container *ngIf="selectedUser$ | async as user">
<h1>{{ 'account.punchout.configuration.heading' | translate }} - {{ user.login }}</h1>
<p>{{ 'account.punchout.cxml.configuration.helptext' | translate }}</p>

<ng-container *ngIf="cxmlConfiguration$ | async as cxmlConfiguration; else noConfiguration">
<ng-container *ngIf="cxmlConfiguration.length > 0" ; else noConfiguration>
<ish-cxml-configuration-form [cxmlConfiguration]="cxmlConfiguration" /> </ng-container
></ng-container>
<ng-template #noConfiguration>
<p>{{ 'account.punchout.cxml.configuration.no_configuration' | translate }}</p>
</ng-template>
</ng-container>
<ish-loading *ngIf="loading$ | async" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
import { instance, mock, when } from 'ts-mockito';

import { LoadingComponent } from 'ish-shared/components/common/loading/loading.component';

import { PunchoutFacade } from '../../facades/punchout.facade';
import { CxmlConfiguration } from '../../models/cxml-configuration/cxml-configuration.model';
import { PunchoutUser } from '../../models/punchout-user/punchout-user.model';

import { AccountPunchoutCxmlConfigurationPageComponent } from './account-punchout-cxml-configuration-page.component';
import { CxmlConfigurationFormComponent } from './cxml-configuration-form/cxml-configuration-form.component';

describe('Account Punchout Cxml Configuration Page Component', () => {
let component: AccountPunchoutCxmlConfigurationPageComponent;
let fixture: ComponentFixture<AccountPunchoutCxmlConfigurationPageComponent>;
let element: HTMLElement;
let punchoutFacade: PunchoutFacade;

beforeEach(async () => {
punchoutFacade = mock(PunchoutFacade);
await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [
AccountPunchoutCxmlConfigurationPageComponent,
MockComponent(CxmlConfigurationFormComponent),
MockComponent(LoadingComponent),
],
providers: [{ provide: PunchoutFacade, useFactory: () => instance(punchoutFacade) }],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(AccountPunchoutCxmlConfigurationPageComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;

const user = {
login: '1',
} as PunchoutUser;
const cxmlConfiguration = [{ name: 'test', value: 'test value' }] as CxmlConfiguration[];
when(punchoutFacade.selectedPunchoutUser$).thenReturn(of(user));
when(punchoutFacade.cxmlConfiguration$()).thenReturn(of(cxmlConfiguration));
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should display the configuration form after creation', () => {
fixture.detectChanges();
expect(element.querySelector('ish-cxml-configuration-form')).toBeTruthy();
});

it('should display a loading overlay if the configuration is loading', () => {
when(punchoutFacade.cxmlConfigurationLoading$).thenReturn(of(true));
fixture.detectChanges();
expect(element.querySelector('ish-loading')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

import { PunchoutFacade } from '../../facades/punchout.facade';
import { CxmlConfiguration } from '../../models/cxml-configuration/cxml-configuration.model';
import { PunchoutUser } from '../../models/punchout-user/punchout-user.model';

@Component({
selector: 'ish-account-punchout-cxml-configuration-page',
templateUrl: './account-punchout-cxml-configuration-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccountPunchoutCxmlConfigurationPageComponent implements OnInit {
selectedUser$: Observable<PunchoutUser>;
cxmlConfiguration$: Observable<CxmlConfiguration[]>;
loading$: Observable<boolean>;

constructor(private punchoutFacade: PunchoutFacade) {}

ngOnInit() {
this.selectedUser$ = this.punchoutFacade.selectedPunchoutUser$;
this.cxmlConfiguration$ = this.punchoutFacade.cxmlConfiguration$();
this.loading$ = this.punchoutFacade.cxmlConfigurationLoading$;
}
}
Loading

0 comments on commit 74bb9cf

Please sign in to comment.