Skip to content

Commit

Permalink
feat: punchout functionality - user management, basket transmission, …
Browse files Browse the repository at this point in the history
…functions routing (OCI Punchout) (#490)

- a B2B user with administration rights can manage OCI Punchot users in the My Account section
- a punchout user will see reduced functionality when browsing the storefront
- transfer a basket as OCI punchout order to the given punchout system (HOOK_URL)
- handle punchout functions (LOGIN, DETAILS, VALIDATE, SEARCH) through a dedicated punchout route
- 'punchout' needs to be enabled via feature toggle

Co-authored-by: Stefan Hauke <s.hauke@intershop.de>
  • Loading branch information
SGrueber and shauke authored Jan 29, 2021
1 parent 24fb46b commit 2529e89
Show file tree
Hide file tree
Showing 79 changed files with 2,628 additions and 104 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/demo-server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,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 TACTON='${{ secrets.TACTON }}'
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,punchout,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
4 changes: 2 additions & 2 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
run: |
echo "THEME=blue" >> $GITHUB_ENV
echo "ICM_CHANNEL=inSPIRED-inTRONICS_Business-Site" >> $GITHUB_ENV
echo "FEATURES=quoting,quickorder,orderTemplates,compare,recently,businessCustomerRegistration,advancedVariationHandling" >> $GITHUB_ENV
echo "FEATURES=quoting,quickorder,orderTemplates,compare,recently,businessCustomerRegistration,punchout,advancedVariationHandling" >> $GITHUB_ENV
- name: Start SSR
run: npm run serve &
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:
run: |
echo "THEME=blue" >> $GITHUB_ENV
echo "ICM_CHANNEL=inSPIRED-inTRONICS_Business-Site" >> $GITHUB_ENV
echo "FEATURES=quoting,quickorder,orderTemplates,compare,recently,businessCustomerRegistration,advancedVariationHandling" >> $GITHUB_ENV
echo "FEATURES=quoting,quickorder,orderTemplates,compare,recently,businessCustomerRegistration,punchout,advancedVariationHandling" >> $GITHUB_ENV
- name: Start SSR
run: npm run serve &
Expand Down
8 changes: 4 additions & 4 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ deploy_demo_b2b:
--env LOGGING=true
--env PROXY_ICM=true
--env THEME="blue|688dc3"
--env FEATURES=quoting,quickorder,orderTemplates,compare,recently,businessCustomerRegistration,advancedVariationHandling,sentry,tacton
--env FEATURES=quoting,quickorder,orderTemplates,compare,recently,businessCustomerRegistration,advancedVariationHandling,sentry,tacton, punchout
--env ICM_CHANNEL=inSPIRED-inTRONICS_Business-Site
--env SENTRY_DSN=${SENTRY_DSN}
--env TACTON="${TACTON}"
Expand Down Expand Up @@ -228,7 +228,7 @@ deploy_demo_nginx:
.+\.com:
channel: inSPIRED-inTRONICS_Business-Site
lang: de_DE
features: quoting,quickorder,orderTemplates,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry,tacton
features: quoting,quickorder,orderTemplates,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry,tacton, punchout
theme: blue|688dc3
.+\.fr:
channel: inSPIRED-inTRONICS-Site
Expand Down Expand Up @@ -334,7 +334,7 @@ deploy_review_b2c:
.+\.com:
channel: inSPIRED-inTRONICS_Business-Site
lang: de_DE
features: quoting,quickorder,orderTemplates,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry,tacton
features: quoting,quickorder,orderTemplates,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry,tacton, punchout
theme: blue|688dc3
.+\.fr:
channel: inSPIRED-inTRONICS-Site
Expand Down Expand Up @@ -447,7 +447,7 @@ deploy_review_b2b:
-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,tacton
-e FEATURES=quoting,quickorder,orderTemplates,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry,tacton, punchout
--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
27 changes: 27 additions & 0 deletions e2e/cypress/integration/pages/account/punchout-create.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { fillFormField } from '../../framework';

declare interface PunchoutUserCreateForm {
login: string;
password: string;
passwordConfirmation: string;
}

export class PunchoutCreatePage {
readonly tag = 'ish-account-punchout-create-page';

private submitButton = () => cy.get('[data-testing-id="create-punchout-user-submit"]');

fillForm(content: PunchoutUserCreateForm) {
Object.keys(content)
.filter(key => content[key] !== undefined)
.forEach((key: keyof PunchoutUserCreateForm) => {
fillFormField(this.tag, key, content[key]);
});

return this;
}

submit() {
this.submitButton().click();
}
}
19 changes: 19 additions & 0 deletions e2e/cypress/integration/pages/account/punchout-edit.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { waitLoadingEnd } from '../../framework';

export class PunchoutEditPage {
readonly tag = 'ish-account-punchout-details-page';

private submitButton = () => cy.get('[data-testing-id="update-punchout-user-submit"]');

editActiveFlag(active: boolean) {
cy.get('[data-testing-id="active"]').uncheck();
if (active) {
cy.get('[data-testing-id="active"]').check();
}
}

submit() {
this.submitButton().click();
waitLoadingEnd(1000);
}
}
38 changes: 38 additions & 0 deletions e2e/cypress/integration/pages/account/punchout-overview.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { waitLoadingEnd } from '../../framework';
import { BreadcrumbModule } from '../breadcrumb.module';
import { HeaderModule } from '../header.module';

export class PunchoutOverviewPage {
readonly tag = 'ish-account-punchout-page';

readonly header = new HeaderModule();
readonly breadcrumb = new BreadcrumbModule();

get userList() {
return cy.get('div[data-testing-id="user-list"]');
}

get emptyList() {
return cy.get(this.tag).find('[data-testing-id="empty-user-list"]');
}

get successMessage() {
return {
message: cy.get('#toast-container').find('.toast-message'),
};
}

addUser() {
cy.get('button[data-testing-id="add-user-button"]').click();
}

editUser(login: string) {
cy.get(this.tag).contains('div.list-item-row', login).find('[data-testing-id="edit-user"]').click();
}

deleteUser(login: string) {
cy.get(this.tag).contains('div.list-item-row', login).find('[data-testing-id="delete-user"]').click();
cy.get('[data-testing-id="confirm"]', { timeout: 2000 }).click();
waitLoadingEnd(2000);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { at } from '../../framework';
import { createB2BUserViaREST } from '../../framework/b2b-user';
import { LoginPage } from '../../pages/account/login.page';
import { PunchoutCreatePage } from '../../pages/account/punchout-create.page';
import { PunchoutEditPage } from '../../pages/account/punchout-edit.page';
import { PunchoutOverviewPage } from '../../pages/account/punchout-overview.page';
import { sensibleDefaults } from '../../pages/account/registration.page';

const _ = {
user: {
login: `test${new Date().getTime()}@testcity.de`,
...sensibleDefaults,
},
ociUser: {
login: `punchoutuser${new Date().getTime()}@test.intershop.de`,
password: 'Intershop00!',
},
};

describe('Punchout MyAccount Functionality', () => {
before(() => {
createB2BUserViaREST(_.user);
});

it('should start punchout management by logging in', () => {
LoginPage.navigateTo('/account/punchout');
at(LoginPage, page => {
page.fillForm(_.user.login, _.user.password);
page.submit().its('response.statusCode').should('equal', 200);
});
at(PunchoutOverviewPage, page => {
page.emptyList.should('be.visible');
});
});

it('admin user creates a punchout user', () => {
at(PunchoutOverviewPage, page => page.addUser());
at(PunchoutCreatePage, page => {
page.fillForm({ ..._.ociUser, passwordConfirmation: _.ociUser.password });
page.submit();
});
at(PunchoutOverviewPage, page => {
page.userList.should('contain', `${_.ociUser.login}`);
page.userList.should('not.contain', `Inactive`);
page.successMessage.message.should('contain', 'created');
});
});

it('admin user sets a punchout user inactive', () => {
at(PunchoutOverviewPage, page => page.editUser(_.ociUser.login));
at(PunchoutEditPage, page => {
page.editActiveFlag(false);
page.submit();
});
at(PunchoutOverviewPage, page => {
page.userList.should('contain', `Inactive`);
});
});

it('admin user deletes a punchout user', () => {
at(PunchoutOverviewPage, page => {
page.deleteUser(_.ociUser.login);
page.userList.should('not.exist');
page.emptyList.should('be.visible');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('Users Selectors', () => {
});

describe('LoadUsersSuccess', () => {
const users = [{ login: '1' }, { login: '2' }] as User[];
const users = [{ login: '1' }, { login: '2' }, { login: '3', roleIDs: ['APP_B2B_OCI_USER'] }] as User[];
const successAction = loadUsersSuccess({ users });

beforeEach(() => {
Expand All @@ -85,7 +85,7 @@ describe('Users Selectors', () => {
});

it('should have entities when successfully loading', () => {
expect(getUsers(store$.state)).not.toBeEmpty();
expect(getUsers(store$.state)).toHaveLength(2);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export const getUsersError = createSelector(getUsersState, state => state.error)

const { selectAll, selectEntities, selectTotal } = usersAdapter.getSelectors(getUsersState);

export const getUsers = selectAll;
export const getUsers = createSelector(selectAll, users =>
users.filter(user => !user.roleIDs?.find(roleID => roleID === 'APP_B2B_OCI_USER'))
);

export const getUserCount = selectTotal;

Expand Down
2 changes: 1 addition & 1 deletion scripts/init-local-environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const b2b = 0;
const extraFeatures: typeof environment.features =
// default
b2b ? ['advancedVariationHandling', 'businessCustomerRegistration', 'quoting', 'quickorder', 'orderTemplates'] : ['wishlists'];
b2b ? ['advancedVariationHandling', 'businessCustomerRegistration', 'quoting', 'quickorder', 'orderTemplates', 'punchout'] : ['wishlists'];
// none
// [];
Expand Down
4 changes: 3 additions & 1 deletion src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
</div>
</div>

<footer data-testing-id="section-footer"><ish-footer [deviceType]="deviceType$ | async"></ish-footer></footer>
<footer data-testing-id="section-footer">
<ish-footer *ishHasNotRole="'APP_B2B_OCI_USER'" [deviceType]="deviceType$ | async"></ish-footer>
</footer>
</div>

<ish-cookies-banner></ish-cookies-banner>
3 changes: 2 additions & 1 deletion src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MockComponent } from 'ng-mocks';
import { instance, mock } from 'ts-mockito';

import { AppFacade } from 'ish-core/facades/app.facade';
import { RoleToggleModule } from 'ish-core/role-toggle.module';
import { findAllCustomElements } from 'ish-core/utils/dev/html-query-utils';
import { CookiesBannerComponent } from 'ish-shell/application/cookies-banner/cookies-banner.component';

Expand All @@ -27,7 +28,7 @@ describe('App Component', () => {
MockComponent(FooterComponent),
MockComponent(HeaderComponent),
],
imports: [RouterTestingModule, TranslateModule.forRoot()],
imports: [RoleToggleModule.forTesting(), RouterTestingModule, TranslateModule.forRoot()],
providers: [{ provide: AppFacade, useFactory: () => instance(mock(AppFacade)) }],
}).compileComponents();
});
Expand Down
4 changes: 3 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CoreModule } from 'ish-core/core.module';
import { environment } from '../environments/environment';

import { AppComponent } from './app.component';
import { PunchoutRoutingModule } from './extensions/punchout/pages/punchout-routing.module';
import { QuickorderRoutingModule } from './extensions/quickorder/pages/quickorder-routing.module';
import { QuotingRoutingModule } from './extensions/quoting/pages/quoting-routing.module';
import { TactonRoutingModule } from './extensions/tacton/pages/tacton-routing.module';
Expand All @@ -24,8 +25,9 @@ import { ShellModule } from './shell/shell.module';
ShellModule,
AppRoutingModule,
QuickorderRoutingModule,
TactonRoutingModule,
QuotingRoutingModule,
PunchoutRoutingModule,
TactonRoutingModule,
AppLastRoutingModule,
],
bootstrap: [AppComponent],
Expand Down
1 change: 1 addition & 0 deletions src/app/extensions/punchout/exports/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*/**/*.*
20 changes: 20 additions & 0 deletions src/app/extensions/punchout/exports/punchout-exports.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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 { LazyPunchoutTransferBasketComponent } from './lazy-punchout-transfer-basket/lazy-punchout-transfer-basket.component';

@NgModule({
imports: [FeatureToggleModule],
providers: [
{
provide: LAZY_FEATURE_MODULE,
useValue: { feature: 'punchout', location: import('../store/punchout-store.module') },
multi: true,
},
],
declarations: [LazyPunchoutTransferBasketComponent],
exports: [LazyPunchoutTransferBasketComponent],
})
export class PunchoutExportsModule {}
46 changes: 46 additions & 0 deletions src/app/extensions/punchout/facades/punchout.facade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { Store, select } from '@ngrx/store';

import { PunchoutUser } from '../models/punchout-user/punchout-user.model';
import { transferPunchoutBasket } from '../store/punchout-functions';
import {
addPunchoutUser,
deletePunchoutUser,
getPunchoutError,
getPunchoutLoading,
getPunchoutUsers,
getSelectedPunchoutUser,
loadPunchoutUsers,
updatePunchoutUser,
} from '../store/punchout-users';

// tslint:disable:member-ordering
@Injectable({ providedIn: 'root' })
export class PunchoutFacade {
constructor(private store: Store) {}

punchoutLoading$ = this.store.pipe(select(getPunchoutLoading));
punchoutError$ = this.store.pipe(select(getPunchoutError));

punchoutUsers$() {
this.store.dispatch(loadPunchoutUsers());
return this.store.pipe(select(getPunchoutUsers));
}
selectedPunchoutUser$ = this.store.pipe(select(getSelectedPunchoutUser));

addPunchoutUser(user: PunchoutUser) {
this.store.dispatch(addPunchoutUser({ user }));
}

updatePunchoutUser(user: PunchoutUser) {
this.store.dispatch(updatePunchoutUser({ user }));
}

deletePunchoutUser(login: string) {
this.store.dispatch(deletePunchoutUser({ login }));
}

transferBasket() {
this.store.dispatch(transferPunchoutBasket());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface PunchoutUser {
id: string;
email: string;
login?: string;
password?: string;
active: boolean;
}
Loading

0 comments on commit 2529e89

Please sign in to comment.