Skip to content

Commit

Permalink
feat: remove promotion from basket
Browse files Browse the repository at this point in the history
  • Loading branch information
abeyerIntershop authored and dhhyi committed Dec 18, 2019
1 parent 11d729d commit b9edaa9
Show file tree
Hide file tree
Showing 18 changed files with 287 additions and 8 deletions.
5 changes: 5 additions & 0 deletions src/app/core/facades/checkout.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
DeleteBasketShippingAddress,
LoadBasketEligiblePaymentMethods,
LoadBasketEligibleShippingMethods,
RemovePromotionCodeFromBasket,
SetBasketPayment,
UpdateBasketAddress,
UpdateBasketItems,
Expand Down Expand Up @@ -163,4 +164,8 @@ export class CheckoutFacade {
addPromotionCodeToBasket(code: string) {
this.store.dispatch(new AddPromotionCodeToBasket({ code }));
}

removePromotionCodeFromBasket(code: string) {
this.store.dispatch(new RemovePromotionCodeFromBasket({ code }));
}
}
9 changes: 9 additions & 0 deletions src/app/core/services/basket/basket.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,15 @@ describe('Basket Service', () => {
});
});

it("should remove a promotion code from a specific basket when 'removePromotionCodeFromBasket' is called", done => {
when(apiService.delete(anyString(), anything())).thenReturn(of({}));

basketService.removePromotionCodeFromBasket(basketMockData.data.id, 'promoCode').subscribe(() => {
verify(apiService.delete(`baskets/${basketMockData.data.id}/promotioncodes/promoCode`, anything())).once();
done();
});
});

it("should create a basket address when 'createBasketAddress' is called", done => {
when(apiService.post(anyString(), anything(), anything())).thenReturn(of({ data: {} as Address }));

Expand Down
11 changes: 11 additions & 0 deletions src/app/core/services/basket/basket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,17 @@ export class BasketService {
.pipe(map(({ infos }) => infos && infos[0] && infos[0].message));
}

/**
* Remove a promotion code from basket.
* @param basketId The id of the basket where the promotion code should be removed.
* @param codeStr The code string of the promotion code that should be removed from basket.
*/
removePromotionCodeFromBasket(basketId: string = 'current', codeStr: string): Observable<string> {
return this.apiService.delete(`baskets/${basketId}/promotioncodes/${codeStr}`, {
headers: this.basketHeaders,
});
}

/**
* Updates specific line items (quantity/shipping method) for the given basket.
* @param basketId The id of the basket in which the item should be updated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ describe('Basket Promotion Code Effects', () => {
store$ = TestBed.get(Store);
});

describe('loadBasketAfterAddPromotionCodeToBasket$', () => {
it('should map to action of type LoadBasket if AddPromotionCodeToBasketSuccess action triggered', () => {
const action = new basketActions.AddPromotionCodeToBasketSuccess();
const completion = new basketActions.LoadBasket();
actions$ = hot('-a-a-a', { a: action });
const expected$ = cold('-c-c-c', { c: completion });

expect(effects.loadBasketAfterAddPromotionCodeToBasketChangeSuccess$).toBeObservable(expected$);
});
});

describe('addPromotionCodeToBasket$', () => {
beforeEach(() => {
when(basketServiceMock.addPromotionCodeToBasket(anyString(), anyString())).thenReturn(of(undefined));
Expand Down Expand Up @@ -95,14 +106,66 @@ describe('Basket Promotion Code Effects', () => {
});
});

describe('loadBasketAfterAddPromotionCodeToBasket$', () => {
it('should map to action of type LoadBasket if AddPromotionCodeToBasketSuccess action triggered', () => {
const action = new basketActions.AddPromotionCodeToBasketSuccess();
describe('removePromotionCodeFromBasket$', () => {
beforeEach(() => {
when(basketServiceMock.removePromotionCodeFromBasket(anyString(), anyString())).thenReturn(of(undefined));

store$.dispatch(
new basketActions.LoadBasketSuccess({
basket: {
id: 'BID',
lineItems: [],
} as Basket,
})
);
});

it('should call the basketService for RemovePromotionCodeFromBasket action', done => {
const code = 'CODE';
const action = new basketActions.RemovePromotionCodeFromBasket({ code });
actions$ = of(action);

effects.removePromotionCodeFromBasket$.subscribe(() => {
verify(basketServiceMock.removePromotionCodeFromBasket('BID', 'CODE')).once();
done();
});
});

it('should map to action of type RemovePromotionCodeFromBasketSuccess', () => {
const code = 'CODE';
const action = new basketActions.RemovePromotionCodeFromBasket({ code });
const completion = new basketActions.RemovePromotionCodeFromBasketSuccess();
actions$ = hot('-a-a-a', { a: action });
const expected$ = cold('-c-c-c', { c: completion });

expect(effects.removePromotionCodeFromBasket$).toBeObservable(expected$);
});

it('should map invalid request to action of type RemovePromotionCodeFromBasketFail', () => {
when(basketServiceMock.removePromotionCodeFromBasket(anyString(), anyString())).thenReturn(
throwError({ message: 'invalid' })
);

const code = 'CODE';
const action = new basketActions.RemovePromotionCodeFromBasket({ code });
const completion = new basketActions.RemovePromotionCodeFromBasketFail({
error: { message: 'invalid' } as HttpError,
});
actions$ = hot('-a-a-a', { a: action });
const expected$ = cold('-c-c-c', { c: completion });

expect(effects.removePromotionCodeFromBasket$).toBeObservable(expected$);
});
});

describe('loadBasketAfterRemovePromotionCodeFromBasket$', () => {
it('should map to action of type LoadBasket if RemovePromotionCodeFromBasketSuccess action triggered', () => {
const action = new basketActions.RemovePromotionCodeFromBasketSuccess();
const completion = new basketActions.LoadBasket();
actions$ = hot('-a-a-a', { a: action });
const expected$ = cold('-c-c-c', { c: completion });

expect(effects.loadBasketAfterAddPromotionCodeToBasketChangeSuccess$).toBeObservable(expected$);
expect(effects.loadBasketAfterRemovePromotionCodeFromBasketChangeSuccess$).toBeObservable(expected$);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,29 @@ export class BasketPromotionCodeEffects {
ofType(basketActions.BasketActionTypes.AddPromotionCodeToBasketSuccess),
mapTo(new basketActions.LoadBasket())
);

/**
* Remove promotion code from the current basket.
*/
@Effect()
removePromotionCodeFromBasket$ = this.actions$.pipe(
ofType<basketActions.RemovePromotionCodeFromBasket>(basketActions.BasketActionTypes.RemovePromotionCodeFromBasket),
mapToPayloadProperty('code'),
withLatestFrom(this.store.pipe(select(getCurrentBasketId))),
concatMap(([code, basketId]) =>
this.basketService.removePromotionCodeFromBasket(basketId, code).pipe(
mapTo(new basketActions.RemovePromotionCodeFromBasketSuccess()),
mapErrorToAction(basketActions.RemovePromotionCodeFromBasketFail)
)
)
);

/**
* Reload basket after successfully removing a promo code
*/
@Effect()
loadBasketAfterRemovePromotionCodeFromBasketChangeSuccess$ = this.actions$.pipe(
ofType(basketActions.BasketActionTypes.RemovePromotionCodeFromBasketSuccess),
mapTo(new basketActions.LoadBasket())
);
}
20 changes: 20 additions & 0 deletions src/app/core/store/checkout/basket/basket.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export enum BasketActionTypes {
AddPromotionCodeToBasket = '[Basket Internal] Add Promotion Code To Basket',
AddPromotionCodeToBasketFail = '[Basket API] Add Promotion Code To Basket Fail',
AddPromotionCodeToBasketSuccess = '[Basket API] Add Promotion Code To Basket Success',
RemovePromotionCodeFromBasket = '[Basket Internal] Remove Promotion Code From Basket',
RemovePromotionCodeFromBasketFail = '[Basket API] Remove Promotion Code From Basket Fail',
RemovePromotionCodeFromBasketSuccess = '[Basket API] Remove Promotion Code From Basket Success',
UpdateBasketItems = '[Basket] Update Basket Items',
UpdateBasketItemsFail = '[Basket API] Update Basket Items Fail',
UpdateBasketItemsSuccess = '[Basket API] Update Basket Items Success',
Expand Down Expand Up @@ -218,6 +221,20 @@ export class DeleteBasketItemSuccess implements Action {
constructor(public payload: { info: BasketInfo[] }) {}
}

export class RemovePromotionCodeFromBasket implements Action {
readonly type = BasketActionTypes.RemovePromotionCodeFromBasket;
constructor(public payload: { code: string }) {}
}

export class RemovePromotionCodeFromBasketFail implements Action {
readonly type = BasketActionTypes.RemovePromotionCodeFromBasketFail;
constructor(public payload: { error: HttpError }) {}
}

export class RemovePromotionCodeFromBasketSuccess implements Action {
readonly type = BasketActionTypes.RemovePromotionCodeFromBasketSuccess;
}

export class AddPromotionCodeToBasket implements Action {
readonly type = BasketActionTypes.AddPromotionCodeToBasket;
constructor(public payload: { code: string }) {}
Expand Down Expand Up @@ -351,6 +368,9 @@ export type BasketAction =
| AddPromotionCodeToBasket
| AddPromotionCodeToBasketFail
| AddPromotionCodeToBasketSuccess
| RemovePromotionCodeFromBasket
| RemovePromotionCodeFromBasketFail
| RemovePromotionCodeFromBasketSuccess
| UpdateBasketItems
| UpdateBasketItemsFail
| UpdateBasketItemsSuccess
Expand Down
32 changes: 32 additions & 0 deletions src/app/core/store/checkout/basket/basket.reducer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,38 @@ describe('Basket Reducer', () => {
});
});

describe('RemovePromotionCodeFromBasket actions', () => {
describe('RemovePromotionCodeFromBasket action', () => {
it('should set loading to true', () => {
const action = new fromActions.RemovePromotionCodeFromBasket({ code: 'test' });
const state = basketReducer(initialState, action);

expect(state.loading).toBeTrue();
});
});

describe('RemovePromotionCodeFromBasketFail action', () => {
it('should set loading to false', () => {
const error = undefined as HttpError;
const action = new fromActions.RemovePromotionCodeFromBasketFail({ error });
const state = basketReducer(initialState, action);

expect(state.loading).toBeFalse();
expect(state.promotionError).toEqual(error);
});
});

describe('RemovePromotionCodeFromBasketSuccess action', () => {
it('should set loading to false', () => {
const action = new fromActions.RemovePromotionCodeFromBasketSuccess();
const state = basketReducer(initialState, action);

expect(state.loading).toBeFalse();
expect(state.error).toBeUndefined();
});
});
});

describe('ResetBasket action', () => {
it('should reset to initial state', () => {
const oldState = {
Expand Down
3 changes: 3 additions & 0 deletions src/app/core/store/checkout/basket/basket.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function basketReducer(state = initialState, action: BasketAction | Order
case BasketActionTypes.UpdateBasket:
case BasketActionTypes.AddProductToBasket:
case BasketActionTypes.AddPromotionCodeToBasket:
case BasketActionTypes.RemovePromotionCodeFromBasket:
case BasketActionTypes.AddItemsToBasket:
case BasketActionTypes.MergeBasket:
case BasketActionTypes.ContinueCheckout:
Expand All @@ -68,6 +69,7 @@ export function basketReducer(state = initialState, action: BasketAction | Order
case BasketActionTypes.UpdateBasketFail:
case BasketActionTypes.ContinueCheckoutFail:
case BasketActionTypes.AddItemsToBasketFail:
case BasketActionTypes.RemovePromotionCodeFromBasketFail:
case BasketActionTypes.UpdateBasketItemsFail:
case BasketActionTypes.DeleteBasketItemFail:
case BasketActionTypes.LoadBasketEligibleShippingMethodsFail:
Expand Down Expand Up @@ -114,6 +116,7 @@ export function basketReducer(state = initialState, action: BasketAction | Order
};
}

case BasketActionTypes.RemovePromotionCodeFromBasketSuccess:
case BasketActionTypes.SetBasketPaymentSuccess:
case BasketActionTypes.CreateBasketPaymentSuccess:
case BasketActionTypes.UpdateBasketPaymentSuccess:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<ng-container *ngIf="promotion$ | async as promotion"
><div *ngIf="promotion && !promotion.disableMessages">
<div class="promotion-title" [ishServerHtml]="promotion.title"></div>
<ish-promotion-details [promotion]="promotion"></ish-promotion-details>
<div class="promotion-details-and-remove-links">
<ish-promotion-details [promotion]="promotion"></ish-promotion-details>
<ish-promotion-remove [code]="rebate.code"></ish-promotion-remove>
</div>
</div>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ShoppingFacade } from 'ish-core/facades/shopping.facade';
import { BasketRebate } from 'ish-core/models/basket-rebate/basket-rebate.model';
import { getICMBaseURL } from 'ish-core/store/configuration';
import { PromotionDetailsComponent } from 'ish-shared/components/promotion/promotion-details/promotion-details.component';
import { PromotionRemoveComponent } from 'ish-shared/components/promotion/promotion-remove/promotion-remove.component';

import { BasketPromotionComponent } from './basket-promotion.component';

Expand All @@ -23,7 +24,12 @@ describe('Basket Promotion Component', () => {
shoppingFacade = mock(ShoppingFacade);

TestBed.configureTestingModule({
declarations: [BasketPromotionComponent, MockComponent(PromotionDetailsComponent), ServerHtmlDirective],
declarations: [
BasketPromotionComponent,
MockComponent(PromotionDetailsComponent),
MockComponent(PromotionRemoveComponent),
ServerHtmlDirective,
],
imports: [RouterTestingModule],
providers: [
{ provide: ShoppingFacade, useFactory: () => instance(shoppingFacade) },
Expand Down Expand Up @@ -63,7 +69,9 @@ describe('Basket Promotion Component', () => {
expect(element).toMatchInlineSnapshot(`
<div>
<div class="promotion-title">MyPromotionTitle</div>
<ish-promotion-details></ish-promotion-details>
<div class="promotion-details-and-remove-links">
<ish-promotion-details></ish-promotion-details><ish-promotion-remove></ish-promotion-remove>
</div>
</div>
`);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<ng-container *ngIf="code && (basket$ | async)">
<span class="promotion-links-seperator">|</span>
<a data-testing-id="promo-remove-link" class="details-link promotion-remove-link" (click)="removePromotion()">{{
'promotion.removelink.text' | translate
}}</a>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';
import { instance, mock, when } from 'ts-mockito';

import { CheckoutFacade } from 'ish-core/facades/checkout.facade';
import { BasketView } from 'ish-core/models/basket/basket.model';

import { PromotionRemoveComponent } from './promotion-remove.component';

describe('Promotion Remove Component', () => {
let component: PromotionRemoveComponent;
let fixture: ComponentFixture<PromotionRemoveComponent>;
let element: HTMLElement;
let checkoutFacade: CheckoutFacade;

beforeEach(async(() => {
checkoutFacade = mock(CheckoutFacade);
when(checkoutFacade.basket$).thenReturn(of({} as BasketView));

TestBed.configureTestingModule({
declarations: [PromotionRemoveComponent],
imports: [TranslateModule.forRoot()],
providers: [{ provide: CheckoutFacade, useFactory: () => instance(checkoutFacade) }],
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(PromotionRemoveComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
component.code = 'test';
});

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

it('should display the link and input field on component', () => {
expect(() => fixture.detectChanges()).not.toThrow();

expect(element.querySelector('[data-testing-id=promo-remove-link]')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

import { CheckoutFacade } from 'ish-core/facades/checkout.facade';
import { BasketView } from 'ish-core/models/basket/basket.model';

@Component({
selector: 'ish-promotion-remove',
templateUrl: './promotion-remove.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
// tslint:disable-next-line: ccp-no-intelligence-in-components
export class PromotionRemoveComponent implements OnInit {
basket$: Observable<BasketView>;

@Input() code: string;

constructor(private checkoutFacade: CheckoutFacade) {}

ngOnInit() {
this.basket$ = this.checkoutFacade.basket$;
}

removePromotion() {
this.checkoutFacade.removePromotionCodeFromBasket(this.code);
}
}
Loading

0 comments on commit b9edaa9

Please sign in to comment.