Skip to content

Commit

Permalink
CXSPA-4783 Add dialog for checkout action and adapt guard (#17856)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristophHi authored Sep 21, 2023
1 parent 7ebaaab commit 3005ac4
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 12 deletions.
7 changes: 7 additions & 0 deletions feature-libs/quote/assets/translations/en/quote.i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ export const quote = {
confirmNote: 'Are you sure you want to cancel this quote?',
successMessage: 'Quote cancelled',
},
checkout: {
title: 'Checkout Quote {{ code }}?',
confirmNote:
'Are you sure you want to accept and checkout this quote?',
},
},
expired: {
edit: {
Expand Down Expand Up @@ -173,6 +178,8 @@ export const quote = {
httpHandlers: {
cartValidationIssue:
'Quote request not possible because we found problems with your entries. Please review your cart.',
quoteCartIssue:
'Not possible to do changes to cart entries. Proceed to checkout',
absoluteDiscountIssue:
'Choose a discount that does not exceed the total value',
expirationDateIssue: 'Choose an expiration date in the future',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ const defaultDialogMappings: ConfirmActionDialogMappingConfig = {
i18nKey: 'quote.confirmActionDialog.buyer_offer.cancel',
...defaultConfirmActionDialogConfig,
},
CHECKOUT: {
i18nKey: 'quote.confirmActionDialog.buyer_offer.checkout',
...defaultConfirmActionDialogConfig,
showSuccessMessage: false,
},
},
EXPIRED: {
EDIT: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
Priority,
} from '@spartacus/core';
import { QuoteBadRequestHandler } from './quote-bad-request.handler';
import { of } from 'rxjs';
import { QuoteCartService } from '@spartacus/quote/root';

const mockRequest = {} as HttpRequest<any>;

Expand All @@ -32,6 +34,16 @@ const mockCartValidationResponse = {
},
} as HttpErrorResponse;

const mockDomainErrorResponse = {
error: {
errors: [
{
type: 'DomainError',
},
],
},
} as HttpErrorResponse;

const mockQuoteDiscountResponse = {
error: {
errors: [
Expand Down Expand Up @@ -74,6 +86,13 @@ class MockGlobalMessageService {
add() {}
remove() {}
}
let isQuoteCartActive: any;

class MockQuoteCartService {
isQuoteCartActive() {
return of(isQuoteCartActive);
}
}

describe('QuoteBadRequestHandler', () => {
let service: QuoteBadRequestHandler;
Expand All @@ -87,10 +106,12 @@ describe('QuoteBadRequestHandler', () => {
provide: GlobalMessageService,
useClass: MockGlobalMessageService,
},
{ provide: QuoteCartService, useClass: MockQuoteCartService },
],
});
service = TestBed.inject(QuoteBadRequestHandler);
globalMessageService = TestBed.inject(GlobalMessageService);
isQuoteCartActive = false;
});

it('should be created', () => {
Expand Down Expand Up @@ -125,6 +146,26 @@ describe('QuoteBadRequestHandler', () => {
);
});

it('should do nothing on domain error issues in case cart is not linked to quote', () => {
spyOn(globalMessageService, 'add');
service.handleError(mockRequest, mockDomainErrorResponse);

expect(globalMessageService.add).not.toHaveBeenCalled();
});

it('should handle domain error issues in case cart is linked to quote', () => {
isQuoteCartActive = true;
spyOn(globalMessageService, 'add');
service.handleError(mockRequest, mockDomainErrorResponse);

expect(globalMessageService.add).toHaveBeenCalledWith(
{
key: 'quote.httpHandlers.quoteCartIssue',
},
GlobalMessageType.MSG_TYPE_ERROR
);
});

it('should handle quote discount error', () => {
spyOn(globalMessageService, 'add');
service.handleError(mockRequest, mockQuoteDiscountResponse);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@ import {
HttpResponseStatus,
Priority,
} from '@spartacus/core';
import { QuoteCartService } from '@spartacus/quote/root';
import { take } from 'rxjs/operators';

@Injectable({
providedIn: 'root',
})
export class QuoteBadRequestHandler extends HttpErrorHandler {
constructor(protected globalMessageService: GlobalMessageService) {
constructor(
protected globalMessageService: GlobalMessageService,
protected quoteCartService: QuoteCartService
) {
super(globalMessageService);
}
responseStatus = HttpResponseStatus.BAD_REQUEST;
Expand All @@ -40,6 +45,10 @@ export class QuoteBadRequestHandler extends HttpErrorHandler {
if (this.getCartValidationErrors(response).length > 0) {
this.handleCartValidationIssues();
}

if (this.getDomainErrors(response).length > 0) {
this.handleDomainErrors(this.quoteCartService);
}
}

protected getQuoteThresholdErrors(response: HttpErrorResponse): ErrorModel[] {
Expand All @@ -54,6 +63,12 @@ export class QuoteBadRequestHandler extends HttpErrorHandler {
);
}

protected getDomainErrors(response: HttpErrorResponse): ErrorModel[] {
return (response.error?.errors ?? []).filter(
(error: ErrorModel) => error.type === 'DomainError'
);
}

protected getIllegalArgumentErrors(
response: HttpErrorResponse
): ErrorModel[] {
Expand Down Expand Up @@ -85,6 +100,22 @@ export class QuoteBadRequestHandler extends HttpErrorHandler {
);
}

protected handleDomainErrors(quoteCartService: QuoteCartService) {
quoteCartService
.isQuoteCartActive()
.pipe(take(1))
.subscribe((isActive) => {
if (isActive) {
this.globalMessageService.add(
{
key: 'quote.httpHandlers.quoteCartIssue',
},
GlobalMessageType.MSG_TYPE_ERROR
);
}
});
}

protected handleIllegalArgumentIssues(message: string) {
const discountMask = /Discount type is absolute/;
const discountRelated = message.match(discountMask);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ const SUBMIT_EDIT_CANCEL_UNORDERED = [
QuoteActionType.CANCEL,
];

const CHECKOUT_EDIT_CANCEL_UNORDERED = [
QuoteActionType.CHECKOUT,
QuoteActionType.EDIT,
QuoteActionType.CANCEL,
];

let isQuoteCartActive: any;
let quoteId: any;
class MockQuoteCartService {
Expand Down Expand Up @@ -80,6 +86,11 @@ describe('OccQuoteActionNormalizer', () => {
QuoteActionType.EDIT,
QuoteActionType.SUBMIT,
],
BUYER_OFFER: [
QuoteActionType.CANCEL,
QuoteActionType.EDIT,
QuoteActionType.CHECKOUT,
],
},
primaryActions: [QuoteActionType.SUBMIT],
},
Expand Down Expand Up @@ -194,6 +205,21 @@ describe('OccQuoteActionNormalizer', () => {
QuoteActionType.SUBMIT,
]);
});

it('should not remove edit action in case quote cart is linked to current quote for state BUYER_OFFER', () => {
isQuoteCartActive = true;
quoteId = QUOTE_CODE;
const orderedActions = service['getOrderedActions'](
QuoteState.BUYER_OFFER,
CHECKOUT_EDIT_CANCEL_UNORDERED,
QUOTE_CODE
);
expect(orderedActions).toEqual([
QuoteActionType.CANCEL,
QuoteActionType.EDIT,
QuoteActionType.CHECKOUT,
]);
});
});

describe('getActionCategory', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ export class OccQuoteActionNormalizer implements Converter<OccQuote, Quote> {
])
.pipe(take(1))
.subscribe(([isQuoteCartActive, cartQuoteId]) => {
if (isQuoteCartActive && cartQuoteId === quoteId) {
if (
state !== QuoteState.BUYER_OFFER &&
isQuoteCartActive &&
cartQuoteId === quoteId
) {
const editIndex = clonedActionList.indexOf(QuoteActionType.EDIT);
if (editIndex > -1) {
clonedActionList.splice(editIndex, 1);
Expand Down
39 changes: 35 additions & 4 deletions feature-libs/quote/root/guards/quote-cart.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { QuoteCartGuard } from './quote-cart.guard';
import { RouterState, RoutingService } from '@spartacus/core';
import {
ActivatedRouterStateSnapshot,
RouterState,
RoutingService,
} from '@spartacus/core';
import { of } from 'rxjs';
import { QuoteCartService } from './quote-cart.service';
import { QUOTE_CODE } from '../../core/testing/quote-test-utils';
Expand All @@ -13,10 +17,20 @@ let quoteId: any;
let checkoutAllowed: boolean;
let routerState: any;

const checkoutState: ActivatedRouterStateSnapshot = {
semanticRoute: 'checkout',
url: '',
queryParams: [],
params: [],
context: { id: '' },
cmsRequired: false,
};

const routerStateCheckout: RouterState = {
navigationId: 0,
nextState: checkoutState,
state: {
semanticRoute: 'checkout',
semanticRoute: 'quote',
url: '',
queryParams: [],
params: [],
Expand All @@ -25,10 +39,16 @@ const routerStateCheckout: RouterState = {
},
};

const routerStateCheckoutWoNextState: RouterState = {
...routerStateCheckout,
nextState: undefined,
state: checkoutState,
};

const routerStateCart: RouterState = {
...routerStateCheckout,
state: {
...routerStateCheckout.state,
nextState: {
...checkoutState,
semanticRoute: 'cart',
},
};
Expand Down Expand Up @@ -116,6 +136,17 @@ describe('QuoteCartGuard', () => {
});
});

it('should allow a navigation to checkout if service allows it, current state is checkout and nextState is undefined', (done) => {
isQuoteCartActive = true;
checkoutAllowed = true;
routerState = routerStateCheckoutWoNextState;
quoteId = QUOTE_CODE;
guard.canActivate().subscribe((result) => {
expect(result).toBe(true);
done();
});
});

it('should not allow a navigation to cart if service allows checkout', (done) => {
isQuoteCartActive = true;
checkoutAllowed = true;
Expand Down
34 changes: 28 additions & 6 deletions feature-libs/quote/root/guards/quote-cart.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { CanActivate, UrlTree } from '@angular/router';
import { Observable, combineLatest } from 'rxjs';
import { QuoteCartService } from './quote-cart.service';
import { map } from 'rxjs/operators';
import { RoutingService } from '@spartacus/core';
import { RouterState, RoutingService } from '@spartacus/core';

@Injectable({
providedIn: 'root',
Expand All @@ -28,11 +28,12 @@ export class QuoteCartGuard implements CanActivate {
this.routingService.getRouterState(),
]).pipe(
map(([isQuoteCartActive, quoteId, isCheckoutAllowed, routerState]) => {
const noCheckoutBlocking =
routerState.nextState?.semanticRoute?.startsWith('checkout') ||
(routerState.state.semanticRoute?.startsWith('checkout') &&
isCheckoutAllowed);
if (isQuoteCartActive && !noCheckoutBlocking) {
const isAllowedCheckoutNavigation = this.checkAllowedCheckoutNavigation(
routerState,
isCheckoutAllowed
);

if (isQuoteCartActive && !isAllowedCheckoutNavigation) {
this.routingService.go({
cxRoute: 'quoteDetails',
params: { quoteId: quoteId },
Expand All @@ -43,4 +44,25 @@ export class QuoteCartGuard implements CanActivate {
})
);
}
/**
* Check if the current navigation is an allowed checkout navigation.
* @param routerState Used to assess if a checkout navigation is attempted
* @param isCheckoutAllowed Is checkout allowed although this guard is active? Only happens for quotes in state BUYER_OFFER
* that can be ordered
* @returns true if a checkout navigation is attempted and allowed because of the current quote state
*/
protected checkAllowedCheckoutNavigation(
routerState: RouterState,
isCheckoutAllowed: boolean
) {
const nextStateIsCheckout =
routerState.nextState?.semanticRoute?.startsWith('checkout');
const currentStateIsCheckout =
routerState.state.semanticRoute?.startsWith('checkout');
const noNextState = routerState.nextState === undefined;
return (
(nextStateIsCheckout || (currentStateIsCheckout && noNextState)) &&
isCheckoutAllowed
);
}
}

0 comments on commit 3005ac4

Please sign in to comment.