Skip to content

Commit

Permalink
feat: display multiple basket errors (#1230 #1108)
Browse files Browse the repository at this point in the history
Co-authored-by: Silke Grueber <s.grueber@intershop.de>
  • Loading branch information
Ivo Pereira and SGrueber committed Aug 2, 2022
1 parent 3957b14 commit c0d73c7
Show file tree
Hide file tree
Showing 16 changed files with 188 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ describe('Basket Handling', () => {
page.addProductToCart().its('response.statusCode').should('equal', 422);
waitLoadingEnd(1000);
page.header.miniCart.error.should('contain', 'could not be added');
page.header.miniCart.error.should('contain', 'has exceeded our Maximum Purchasing Policy');
});
});

Expand Down
56 changes: 44 additions & 12 deletions src/app/core/interceptors/icm-error-mapper.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,35 @@ describe('Icm Error Mapper Interceptor', () => {
next: fail,
error: error => {
expect(error).toMatchInlineSnapshot(`
Object {
"message": "The promotion code could not be added. The promotion code could not be found. Some other error.",
"name": "HttpErrorResponse",
"status": 422,
}
`);
Object {
"errors": Array [
Object {
"causes": Array [
Object {
"code": "basket.promotion_code.add_code_promotion_code_not_found.error",
"message": "The promotion code could not be found.",
"paths": Array [
"$.code",
],
},
Object {
"code": "some.other.error",
"message": "Some other error.",
"paths": Array [
"$.code",
],
},
],
"code": "basket.promotion_code.add_not_successful.error",
"message": "The promotion code could not be added.",
"status": "422",
},
],
"message": "The promotion code could not be added. The promotion code could not be found. Some other error.",
"name": "HttpErrorResponse",
"status": 422,
}
`);
done();
},
});
Expand Down Expand Up @@ -114,12 +137,21 @@ describe('Icm Error Mapper Interceptor', () => {
next: fail,
error: error => {
expect(error).toMatchInlineSnapshot(`
Object {
"message": "The product could not be added to your cart.",
"name": "HttpErrorResponse",
"status": 422,
}
`);
Object {
"errors": Array [
Object {
"code": "basket.add_line_item_not_successful.error",
"message": "The product could not be added to your cart.",
"paths": Array [
"$[0]",
],
"status": "422",
},
],
"name": "HttpErrorResponse",
"status": 422,
}
`);
done();
},
});
Expand Down
20 changes: 10 additions & 10 deletions src/app/core/interceptors/icm-error-mapper.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,19 @@ export class ICMErrorMapperInterceptor implements HttpInterceptor {
}[];
}[] = httpError.error?.errors;
if (errors?.length) {
if (errors.length > 1) {
console.warn(`ignoring errors${JSON.stringify(errors.slice(1))}`);
}
const error = errors[0];
if (error.causes?.length) {
return {
...responseError,
message: [error.message].concat(...error.causes.map(c => c.message)).join(' '),
};
if (errors.length === 1) {
const error = errors[0];
if (error.causes?.length) {
return {
...responseError,
errors: httpError.error?.errors,
message: [error.message].concat(...error.causes.map(c => c.message)).join(' '),
};
}
}
return {
...responseError,
message: error.message,
errors: httpError.error?.errors,
};
} else {
return {
Expand Down
16 changes: 16 additions & 0 deletions src/app/core/models/http-error/http-error.model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
interface ErrorCause {
message: string;
parameters?: {
[id: string]: string;
};
}

export interface ErrorFeedback {
causes?: ErrorCause[];
code: string;
message: string;
}

export interface HttpError {
/** name for distinguishing with other errors */
name: 'HttpErrorResponse';
Expand All @@ -10,4 +23,7 @@ export interface HttpError {

/** human readable (and localized) error message */
message?: string;

/* if the response contains a data section with errors and causes, e.g. in the basket and order REST response */
errors?: ErrorFeedback[];
}
9 changes: 7 additions & 2 deletions src/app/core/services/basket/basket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { BasketValidation, BasketValidationScopeType } from 'ish-core/models/bas
import { BasketBaseData, BasketData } from 'ish-core/models/basket/basket.interface';
import { BasketMapper } from 'ish-core/models/basket/basket.mapper';
import { Basket } from 'ish-core/models/basket/basket.model';
import { ErrorFeedback, HttpError } from 'ish-core/models/http-error/http-error.model';
import { LineItemData } from 'ish-core/models/line-item/line-item.interface';
import { LineItemMapper } from 'ish-core/models/line-item/line-item.mapper';
import { LineItem } from 'ish-core/models/line-item/line-item.model';
Expand Down Expand Up @@ -329,8 +330,11 @@ export class BasketService {
* @param items The list of product SKU and quantity pairs to be added to the basket.
* @returns lineItems The list of line items, that have been added to the basket.
* info Info responded by the server.
* error Errors responded by the server.
*/
addItemsToBasket(items: SkuQuantityType[]): Observable<{ lineItems: LineItem[]; info: BasketInfo[] }> {
addItemsToBasket(
items: SkuQuantityType[]
): Observable<{ lineItems: LineItem[]; info: BasketInfo[]; error: HttpError }> {
if (!items) {
return throwError(() => new Error('addItemsToBasket() called without items'));
}
Expand All @@ -344,13 +348,14 @@ export class BasketService {
}));

return this.currentBasketEndpoint()
.post<{ data: LineItemData[]; infos: BasketInfo[] }>('items', body, {
.post<{ data: LineItemData[]; infos: BasketInfo[]; errors?: ErrorFeedback[] }>('items', body, {
headers: this.basketHeaders,
})
.pipe(
map(payload => ({
lineItems: payload.data.map(item => LineItemMapper.fromData(item)),
info: BasketInfoMapper.fromInfo({ infos: payload.infos }),
error: payload.errors ? { name: 'HttpErrorResponse', errors: payload.errors } : undefined,
}))
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ describe('Basket Items Effects', () => {

describe('addItemsToBasket$', () => {
beforeEach(() => {
when(basketServiceMock.addItemsToBasket(anything())).thenReturn(of({ lineItems: [], info: undefined }));
when(basketServiceMock.addItemsToBasket(anything())).thenReturn(
of({ lineItems: [], info: undefined, error: undefined })
);
});

it('should call the basketService for addItemsToBasket', done => {
Expand Down
3 changes: 2 additions & 1 deletion src/app/core/store/customer/basket/basket.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Attribute } from 'ish-core/models/attribute/attribute.model';
import { BasketInfo } from 'ish-core/models/basket-info/basket-info.model';
import { BasketValidation, BasketValidationScopeType } from 'ish-core/models/basket-validation/basket-validation.model';
import { Basket } from 'ish-core/models/basket/basket.model';
import { HttpError } from 'ish-core/models/http-error/http-error.model';
import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-update.model';
import { LineItem } from 'ish-core/models/line-item/line-item.model';
import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model';
Expand Down Expand Up @@ -84,7 +85,7 @@ export const addItemsToBasketFail = createAction('[Basket API] Add Items To Bask

export const addItemsToBasketSuccess = createAction(
'[Basket API] Add Items To Basket Success',
payload<{ info: BasketInfo[]; lineItems: LineItem[] }>()
payload<{ lineItems: LineItem[]; info: BasketInfo[]; error?: HttpError }>()
);

export const mergeBasketInProgress = createAction('[Basket API] Merge two baskets in progress');
Expand Down
4 changes: 2 additions & 2 deletions src/app/core/store/customer/basket/basket.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,8 @@ export const basketReducer = createReducer(
startCheckout,
mergeBasketInProgress
),
unsetLoadingOn(addPromotionCodeToBasketSuccess, addPromotionCodeToBasketFail),
unsetLoadingOn(addPromotionCodeToBasketSuccess, addPromotionCodeToBasketFail, loadBasketSuccess),
unsetLoadingAndErrorOn(
loadBasketSuccess,
mergeBasketSuccess,
updateBasketItemSuccess,
updateBasketItemsSuccess,
Expand Down Expand Up @@ -226,6 +225,7 @@ export const basketReducer = createReducer(
...state,
basket: { ...state.basket, lineItems: unionBy(action.payload.lineItems, state.basket.lineItems ?? [], 'id') },
info: action.payload.info,
error: action.payload.error,
lastTimeProductAdded: new Date().getTime(),
submittedBasket: undefined,
})),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ <h1 class="d-flex flex-wrap align-items-baseline">

<div>
<!-- Error message -->
<ish-error-message [error]="error"></ish-error-message>
<ish-error-message *ngIf="error?.message" [error]="error"></ish-error-message>

<ish-basket-error-message [error]="error"></ish-basket-error-message>

<!-- Basket Info messages -->
<ish-basket-info></ish-basket-info>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { BasketMockData } from 'ish-core/utils/dev/basket-mock-data';
import { ContentIncludeComponent } from 'ish-shared/cms/components/content-include/content-include.component';
import { BasketCostCenterSelectionComponent } from 'ish-shared/components/basket/basket-cost-center-selection/basket-cost-center-selection.component';
import { BasketCostSummaryComponent } from 'ish-shared/components/basket/basket-cost-summary/basket-cost-summary.component';
import { BasketErrorMessageComponent } from 'ish-shared/components/basket/basket-error-message/basket-error-message.component';
import { BasketInfoComponent } from 'ish-shared/components/basket/basket-info/basket-info.component';
import { BasketPromotionCodeComponent } from 'ish-shared/components/basket/basket-promotion-code/basket-promotion-code.component';
import { BasketValidationResultsComponent } from 'ish-shared/components/basket/basket-validation-results/basket-validation-results.component';
Expand All @@ -33,6 +34,7 @@ describe('Shopping Basket Component', () => {
declarations: [
MockComponent(BasketCostCenterSelectionComponent),
MockComponent(BasketCostSummaryComponent),
MockComponent(BasketErrorMessageComponent),
MockComponent(BasketInfoComponent),
MockComponent(BasketPromotionCodeComponent),
MockComponent(BasketValidationResultsComponent),
Expand Down Expand Up @@ -73,7 +75,7 @@ describe('Shopping Basket Component', () => {
});

it('should render an error if an error occurs', () => {
component.error = makeHttpError({ status: 404 });
component.error = makeHttpError({ status: 404, message: 'error message' });
fixture.detectChanges();
expect(element.querySelector('ish-error-message')).toBeTruthy();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<ng-container *ngIf="error as basketError">
<ng-container *ngIf="basketError.errors as errors">
<div *ngFor="let error of errors" [class]="cssClass" data-testing-id="basket-errors">
<strong>{{ error.message }}</strong>
<ng-container *ngIf="error.causes as causes">
<p *ngFor="let cause of causes" role="alert">
<span> {{ cause.message }}</span>
<span *ngIf="cause.parameters?.sku" class="product-id"
><br />
<label>{{ 'product.itemNumber.label' | translate }}</label>
<span itemprop="sku">{{ cause.parameters.sku }}</span>
</span>
</p>
</ng-container>
</div>
</ng-container>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { HttpError } from 'ish-core/models/http-error/http-error.model';

import { BasketErrorMessageComponent } from './basket-error-message.component';

describe('Basket Error Message Component', () => {
let component: BasketErrorMessageComponent;
let fixture: ComponentFixture<BasketErrorMessageComponent>;
let element: HTMLElement;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BasketErrorMessageComponent],
}).compileComponents();
});

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

component.error = {
errors: [{ message: 'main_message', causes: [{ message: 'cause_message' }] }],
} as HttpError;
});

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

it('should display the error message and cause message', () => {
fixture.detectChanges();

expect(element.querySelector('div[data-testing-id=basket-errors]').textContent).toMatchInlineSnapshot(
`"main_message cause_message"`
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

import { HttpError } from 'ish-core/models/http-error/http-error.model';

@Component({
selector: 'ish-basket-error-message',
templateUrl: './basket-error-message.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BasketErrorMessageComponent {
@Input() error: HttpError;
@Input() cssClass = 'alert alert-danger';
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
<ng-container *ngIf="basketError$ | async as error">
<p *ngIf="error" role="alert" class="text-danger">
<span>{{ error.message }}</span>
</p>
</ng-container>

<ng-container *ngIf="lineItems$ | async as lineItems; else emptyBlock">
<div class="product-rows-block">
<ish-basket-error-message
*ngIf="basketError$ | async as basketError"
[error]="basketError"
cssClass="text-danger"
></ish-basket-error-message>
<div *ngFor="let lineItem of lineItems | slice: 0:maxItemNumber" class="product-row">
<ng-container ishProductContext [sku]="lineItem.productSKU">
<div class="mini-product-img">
Expand Down Expand Up @@ -34,4 +33,11 @@
</a>
</ng-container>

<ng-template #emptyBlock> {{ 'shopping_cart.ministatus.empty_cart.text' | translate }} </ng-template>
<ng-template #emptyBlock>
<ish-basket-error-message
*ngIf="basketError$ | async as basketError"
[error]="basketError"
cssClass="text-danger"
></ish-basket-error-message>
{{ 'shopping_cart.ministatus.empty_cart.text' | translate }}
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,21 @@ export class ErrorMessageComponent implements OnChanges {

ngOnChanges() {
if (this.toast) {
this.displayToast();
this.displayToast(this.error);
}
}

private displayToast() {
if (this.error) {
private displayToast(err: HttpError) {
if (err && !err.errors) {
this.messageFacade.error({
message: this.error.message || this.error.code,
message: err.message || err.code,
});
}
if (err?.errors) {
err?.errors.map(cause => {
this.messageFacade.error({
message: cause.message,
});
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { BasketBuyerComponent } from './components/basket/basket-buyer/basket-bu
import { BasketCostCenterSelectionComponent } from './components/basket/basket-cost-center-selection/basket-cost-center-selection.component';
import { BasketCostSummaryComponent } from './components/basket/basket-cost-summary/basket-cost-summary.component';
import { BasketDesiredDeliveryDateComponent } from './components/basket/basket-desired-delivery-date/basket-desired-delivery-date.component';
import { BasketErrorMessageComponent } from './components/basket/basket-error-message/basket-error-message.component';
import { BasketInfoComponent } from './components/basket/basket-info/basket-info.component';
import { BasketItemsSummaryComponent } from './components/basket/basket-items-summary/basket-items-summary.component';
import { BasketOrderReferenceComponent } from './components/basket/basket-order-reference/basket-order-reference.component';
Expand Down Expand Up @@ -227,6 +228,7 @@ const exportedComponents = [
BasketCostCenterSelectionComponent,
BasketCostSummaryComponent,
BasketDesiredDeliveryDateComponent,
BasketErrorMessageComponent,
BasketInfoComponent,
BasketInvoiceAddressWidgetComponent,
BasketItemsSummaryComponent,
Expand Down

0 comments on commit c0d73c7

Please sign in to comment.