Skip to content

Commit

Permalink
feat: display checkout and order prices respecting ICM pricing settings
Browse files Browse the repository at this point in the history
  • Loading branch information
dhhyi committed Apr 1, 2020
1 parent 2e0e151 commit b2972f5
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 47 deletions.
180 changes: 142 additions & 38 deletions src/app/core/models/price/price.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,163 @@
import { registerLocaleData } from '@angular/common';
import localeDe from '@angular/common/locales/de';
import { TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import * as using from 'jasmine-data-provider';

import { Customer } from 'ish-core/models/customer/customer.model';
import { PriceItem } from 'ish-core/models/price-item/price-item.model';
import { ApplyConfiguration } from 'ish-core/store/configuration';
import { configurationReducer } from 'ish-core/store/configuration/configuration.reducer';
import { LoginUserSuccess, LogoutUser } from 'ish-core/store/user';
import { userReducer } from 'ish-core/store/user/user.reducer';
import { ngrxTesting } from 'ish-core/utils/dev/ngrx-testing';

import { Price } from './price.model';
import { PricePipe } from './price.pipe';

@Component({ template: '~{{ price | ishPrice }}~' })
class DummyComponent {
price: Price | PriceItem;
}

describe('Price Pipe', () => {
let pipe: PricePipe;
let fixture: ComponentFixture<DummyComponent>;
let component: DummyComponent;
let element: HTMLElement;
let translateService: TranslateService;
let store$: Store<{}>;

beforeEach(() => {
registerLocaleData(localeDe);

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [DummyComponent, PricePipe],
imports: [
TranslateModule.forRoot(),
ngrxTesting({ reducers: { configuration: configurationReducer, user: userReducer } }),
],
providers: [PricePipe],
});
pipe = TestBed.get(PricePipe);
}));

beforeEach(() => {
registerLocaleData(localeDe);
translateService = TestBed.get(TranslateService);
translateService.setDefaultLang('en');
store$ = TestBed.get(Store);

fixture = TestBed.createComponent(DummyComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
});

function dataProvider() {
return [
{
price: {
type: 'Money',
value: 391.98,
currency: 'USD',
},
en_US: '$391.98',
de_DE: '391,98\xA0$',
},
{
price: {
type: 'ProductPrice',
value: 391.99,
currency: 'EUR',
},
en_US: '€391.99',
de_DE: '391,99\xA0€',
},
];
}

using(dataProvider, slice => {
it(`should translate price of type ${slice.price.type} correcly when locale en_US is set`, () => {
translateService.use('en_US');
expect(pipe.transform(slice.price)).toEqual(slice.en_US);
it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should display N/A for default', () => {
translateService.use('en');
fixture.detectChanges();
expect(element).toMatchInlineSnapshot(`~product.price.na.text~`);
});

describe('Price', () => {
const euroPrice: Price = {
type: 'Money',
value: 12391.98,
currency: 'EUR',
};

const dollarPrice: Price = {
type: 'Money',
value: 12391.98,
currency: 'USD',
};

it('should display dollar price for english', () => {
component.price = dollarPrice;
translateService.use('en');

fixture.detectChanges();
expect(element).toMatchInlineSnapshot(`~$12,391.98~`);
});

it('should display dollar price for german', () => {
component.price = dollarPrice;
translateService.use('de');

fixture.detectChanges();
expect(element).toMatchInlineSnapshot(`~12.391,98&nbsp;$~`);
});

it(`should translate price of type ${slice.price.type} correcly when locale de_DE is set`, () => {
translateService.use('de_DE');
expect(pipe.transform(slice.price)).toEqual(slice.de_DE);
it('should display euro price for english', () => {
component.price = euroPrice;
translateService.use('en');

fixture.detectChanges();
expect(element).toMatchInlineSnapshot(`~€12,391.98~`);
});
it('should display euro price for german', () => {
component.price = euroPrice;
translateService.use('de');

fixture.detectChanges();
expect(element).toMatchInlineSnapshot(`~12.391,98&nbsp;€~`);
});
});

describe('PriceItem', () => {
const priceItem: PriceItem = {
type: 'PriceItem',
gross: 12391.98,
net: 987.6,
currency: 'USD',
};

beforeEach(() => {
component.price = priceItem;
translateService.use('en');
});

it('should display price depending on state', fakeAsync(() => {
fixture.detectChanges();
tick(500);

expect(element).toMatchInlineSnapshot(`~$12,391.98~`);

store$.dispatch(new LoginUserSuccess({ customer: { isBusinessCustomer: true } as Customer }));
fixture.detectChanges();
tick(500);

expect(element).toMatchInlineSnapshot(`~$987.60~`);

store$.dispatch(new LogoutUser());
fixture.detectChanges();
tick(500);

expect(element).toMatchInlineSnapshot(`~$12,391.98~`);

store$.dispatch(
new ApplyConfiguration({ serverConfig: { pricing: { defaultCustomerTypeForPriceDisplay: 'SMB' } } })
);
fixture.detectChanges();
tick(500);

expect(element).toMatchInlineSnapshot(`~$987.60~`);
}));

it('should display price depending on input', fakeAsync(() => {
fixture.detectChanges();
tick(500);

expect(element).toMatchInlineSnapshot(`~$12,391.98~`);

component.price = { ...priceItem, gross: 123 };
fixture.detectChanges();
tick(500);

expect(element).toMatchInlineSnapshot(`~$123.00~`);
}));
});
});
28 changes: 24 additions & 4 deletions src/app/core/models/price/price.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { formatCurrency, getCurrencySymbol } from '@angular/common';
import { Pipe, PipeTransform } from '@angular/core';
import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { PriceItemHelper } from 'ish-core/models/price-item/price-item.helper';
import { PriceItem } from 'ish-core/models/price-item/price-item.model';
import { getPriceDisplayType } from 'ish-core/store/user';

import { Price } from './price.model';

Expand All @@ -13,9 +17,16 @@ export function formatPrice(price: Price, lang: string): string {
}

@Pipe({ name: 'ishPrice', pure: false })
export class PricePipe implements PipeTransform {
constructor(private translateService: TranslateService) {}
export class PricePipe implements PipeTransform, OnDestroy {
displayText: string;

private destroy$ = new Subject();

constructor(private translateService: TranslateService, private store: Store<{}>, private cdRef: ChangeDetectorRef) {}

ngOnDestroy() {
this.destroy$.next();
}
transform(data: Price | PriceItem): string {
if (!data) {
return this.translateService.instant('product.price.na.text');
Expand All @@ -27,7 +38,16 @@ export class PricePipe implements PipeTransform {

switch (data.type) {
case 'PriceItem':
return formatPrice(PriceItemHelper.selectType(data, 'gross'), this.translateService.currentLang);
this.store
.pipe(
select(getPriceDisplayType),
takeUntil(this.destroy$)
)
.subscribe(type => {
this.displayText = formatPrice(PriceItemHelper.selectType(data, type), this.translateService.currentLang);
this.cdRef.markForCheck();
});
return this.displayText;
default:
return formatPrice(data as Price, this.translateService.currentLang);
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/core/store/configuration/configuration.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface ConfigurationState {
error?: HttpError;
}

const initialState: ConfigurationState = {
export const initialState: ConfigurationState = {
baseURL: undefined,
server: undefined,
serverStatic: undefined,
Expand Down
8 changes: 5 additions & 3 deletions src/app/core/store/configuration/configuration.selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@ export const isServerConfigurationLoaded = createSelector(
serverConfig => !!serverConfig
);

export const getServerConfigParameter = (path: string) =>
export const getServerConfigParameter = <T>(path: string) =>
createSelector(
getServerConfig,
serverConfig =>
path.split('.').reduce((obj, key) => (obj && obj[key] !== undefined ? obj[key] : undefined), serverConfig)
(serverConfig): T =>
path
.split('.')
.reduce((obj, key) => (obj && obj[key] !== undefined ? obj[key] : undefined), serverConfig as unknown) as T
);
73 changes: 72 additions & 1 deletion src/app/core/store/user/user.selectors.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { TestBed } from '@angular/core/testing';
import { combineReducers } from '@ngrx/store';

import { CustomerUserType } from 'ish-core/models/customer/customer.model';
import { Customer, CustomerUserType } from 'ish-core/models/customer/customer.model';
import { HttpError, HttpHeader } from 'ish-core/models/http-error/http-error.model';
import { PasswordReminder } from 'ish-core/models/password-reminder/password-reminder.model';
import { PaymentMethod } from 'ish-core/models/payment-method/payment-method.model';
import { Product } from 'ish-core/models/product/product.model';
import { User } from 'ish-core/models/user/user.model';
import { checkoutReducers } from 'ish-core/store/checkout/checkout-store.module';
import { ApplyConfiguration } from 'ish-core/store/configuration';
import { coreReducers } from 'ish-core/store/core-store.module';
import { LoadProductSuccess } from 'ish-core/store/shopping/products';
import { shoppingReducers } from 'ish-core/store/shopping/shopping-store.module';
Expand All @@ -29,6 +30,7 @@ import {
getLoggedInUser,
getPasswordReminderError,
getPasswordReminderSuccess,
getPriceDisplayType,
getUserAuthorized,
getUserError,
getUserLoading,
Expand Down Expand Up @@ -204,4 +206,73 @@ describe('User Selectors', () => {
expect(getPasswordReminderSuccess(store$.state)).toBeFalse();
expect(getUserLoading(store$.state)).toBeFalse();
});

describe('getPriceDisplayType', () => {
describe('without config', () => {
it.each([
['gross', undefined],
['gross', { isBusinessCustomer: false } as Customer],
['net', { isBusinessCustomer: true } as Customer],
])('should be "%s" for user %o', (expected, customer: Customer) => {
if (customer) {
store$.dispatch(new LoginUserSuccess({ customer }));
}
expect(getPriceDisplayType(store$.state)).toEqual(expected);
});
});

describe('B2C', () => {
beforeEach(() => {
store$.dispatch(
new ApplyConfiguration({
serverConfig: {
pricing: {
defaultCustomerTypeForPriceDisplay: 'PRIVATE',
privateCustomerPriceDisplayType: 'gross',
smbCustomerPriceDisplayType: 'net',
},
},
})
);
});

it.each([
['gross', undefined],
['gross', { isBusinessCustomer: false } as Customer],
['net', { isBusinessCustomer: true } as Customer],
])('should be "%s" for user %o', (expected, customer: Customer) => {
if (customer) {
store$.dispatch(new LoginUserSuccess({ customer }));
}
expect(getPriceDisplayType(store$.state)).toEqual(expected);
});
});

describe('B2B', () => {
beforeEach(() => {
store$.dispatch(
new ApplyConfiguration({
serverConfig: {
pricing: {
defaultCustomerTypeForPriceDisplay: 'SMB',
privateCustomerPriceDisplayType: 'gross',
smbCustomerPriceDisplayType: 'net',
},
},
})
);
});

it.each([
['net', undefined],
['gross', { isBusinessCustomer: false } as Customer],
['net', { isBusinessCustomer: true } as Customer],
])('should be "%s" for user %o', (expected, customer: Customer) => {
if (customer) {
store$.dispatch(new LoginUserSuccess({ customer }));
}
expect(getPriceDisplayType(store$.state)).toEqual(expected);
});
});
});
});
13 changes: 13 additions & 0 deletions src/app/core/store/user/user.selectors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createSelector } from '@ngrx/store';

import { getServerConfigParameter } from 'ish-core/store/configuration';
import { getCoreState } from 'ish-core/store/core-store';

const getUserState = createSelector(
Expand Down Expand Up @@ -58,3 +59,15 @@ export const getPasswordReminderError = createSelector(
getUserState,
state => state.passwordReminderError
);

export const getPriceDisplayType = createSelector(
getUserAuthorized,
isBusinessCustomer,
getServerConfigParameter<'PRIVATE' | 'SMB'>('pricing.defaultCustomerTypeForPriceDisplay'),
getServerConfigParameter<'gross' | 'net'>('pricing.privateCustomerPriceDisplayType'),
getServerConfigParameter<'gross' | 'net'>('pricing.smbCustomerPriceDisplayType'),
(loggedIn, businessCustomer, defaultCustomer, b2c, b2b): 'gross' | 'net' => {
const isB2B = (!loggedIn && defaultCustomer === 'SMB') || businessCustomer;
return isB2B ? b2b || 'net' : b2c || 'gross';
}
);
Loading

0 comments on commit b2972f5

Please sign in to comment.