#829 Initial NgRx Customers Store SetUp

Add Effects (WiP)


Further Implement Customer Store incl. first Unit Tests
This commit is contained in:
Sebastian
2020-07-12 21:04:23 +02:00
parent 7a645d3b94
commit f701c7dd46
24 changed files with 620 additions and 2 deletions

21
.vscode/settings.json vendored
View File

@@ -8,5 +8,24 @@
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} },
"workbench.colorCustomizations": {
"activityBar.background": "#4a1557",
"titleBar.activeBackground": "#270b2e",
"titleBar.activeForeground": "#e7e7e7",
"activityBar.activeBackground": "#4a1557",
"activityBar.activeBorder": "#806d1f",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#806d1f",
"activityBarBadge.foreground": "#e7e7e7",
"statusBar.background": "#270b2e",
"statusBar.border": "#270b2e",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#4a1557",
"titleBar.border": "#270b2e",
"titleBar.inactiveBackground": "#270b2e99",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.color": "#270b2e"
} }

View File

@@ -1,9 +1,11 @@
import { combineReducers, Action } from '@ngrx/store'; import { combineReducers, Action } from '@ngrx/store';
import { CustomerState } from './customer.state'; import { CustomerState } from './customer.state';
import { shelfReducer } from './shelf'; import { shelfReducer } from './shelf';
import { customersReducer } from './customers';
const _customerReducer = combineReducers<CustomerState>({ const _customerReducer = combineReducers<CustomerState>({
shelf: shelfReducer, shelf: shelfReducer,
customers: customersReducer,
}); });
export function customerReducer(state: CustomerState, action: Action) { export function customerReducer(state: CustomerState, action: Action) {

View File

@@ -1,5 +1,7 @@
import { ShelfState } from './shelf'; import { ShelfState } from './shelf';
import { CustomersState } from './customers';
export interface CustomerState { export interface CustomerState {
shelf: ShelfState; shelf: ShelfState;
customers: CustomersState;
} }

View File

@@ -0,0 +1,91 @@
import { createAction, props } from '@ngrx/store';
import {
StrictHttpResponse,
ListResponseArgsOfCustomerInfoDTO,
ResponseArgsOfCustomerDTO,
ListResponseArgsOfShippingAddressDTO,
ListResponseArgsOfAssignedPayerDTO,
} from '@swagger/crm';
import { Customer } from './defs';
const prefix = `[Customer] [Customers]`;
export const fetchCustomer = createAction(
`${prefix} Fetch Customer`,
props<{ id: number }>()
);
export const fetchCustomerDone = createAction(
`${prefix} Fetch Customer Done`,
props<{
id: number;
response: StrictHttpResponse<ResponseArgsOfCustomerDTO>;
}>()
);
export const fetchCustomers = createAction(
`${prefix} Fetch Customers`,
props<{ query: any }>()
);
export const fetchCustomersDone = createAction(
`${prefix} Fetch Customers Done`,
props<{
id: number;
response: StrictHttpResponse<ListResponseArgsOfCustomerInfoDTO>;
}>()
);
export const addCustomer = createAction(
`${prefix} Add Customer`,
props<{
id: number;
customer: Customer;
}>()
);
export const addCustomers = createAction(
`${prefix} Add Customers`,
props<{
ids: number[];
customers: Customer[];
}>()
);
export const updateCustomer = createAction(
`${prefix} Add Customers`,
props<{
id: number;
customers: Partial<Customer>;
}>()
);
export const fetchPayerAddresses = createAction(
`${prefix} Fetch Payer Address`,
props<{
id: number;
}>()
);
export const fetchPayerAddressesDone = createAction(
`${prefix} Fetch Payer Address Done`,
props<{
id: number;
response: StrictHttpResponse<ListResponseArgsOfAssignedPayerDTO>;
}>()
);
export const fetchShippingyAddresses = createAction(
`${prefix} Fetch Shipping Address`,
props<{
id: number;
}>()
);
export const fetchShippingAddressesDone = createAction(
`${prefix} Fetch Shipping Address Done`,
props<{
id: number;
response: StrictHttpResponse<ListResponseArgsOfShippingAddressDTO>;
}>()
);

View File

@@ -0,0 +1,73 @@
import { Injectable } from '@angular/core';
import { CustomerService } from '@swagger/crm';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { flatMap, map } from 'rxjs/operators';
import * as actions from './customers.actions';
import { customerDtoToCustomer } from './mappers';
import { NEVER } from 'rxjs';
import { Store } from '@ngrx/store';
@Injectable({ providedIn: 'root' })
export class CustomersEffects {
constructor(
private actions$: Actions,
private crmService: CustomerService,
private store: Store<any>
) {}
fetchCustomer$ = createEffect(() =>
this.actions$.pipe(
ofType(actions.fetchCustomer),
flatMap((action) =>
this.crmService
.CustomerGetCustomerResponse({ customerId: action.id })
.pipe(
map((response) =>
actions.fetchCustomerDone({ id: action.id, response })
)
)
)
)
);
fetchCustomerDone$ = createEffect(() =>
this.actions$.pipe(
ofType(actions.fetchCustomerDone),
flatMap((action) => {
if (action.response.ok) {
const result = action.response.body;
if (!result || !result.result) {
return NEVER;
}
const customer = customerDtoToCustomer(result.result);
return [actions.addCustomer({ id: action.id, customer })];
}
return NEVER;
})
)
);
fetchPayerAddress$ = createEffect(() =>
this.actions$.pipe(
ofType(actions.fetchPayerAddresses),
flatMap((action) =>
this.crmService
.CustomerGetAssignedPayersByCustomerIdResponse(action.id)
.pipe(
map((response) =>
actions.fetchPayerAddressesDone({ id: action.id, response })
)
)
)
)
);
fetchPayerAddressDone$ = createEffect(() =>
this.actions$.pipe(ofType(actions.fetchPayerAddressesDone))
);
fetchShippingAddress$ = createEffect(() => this.actions$.pipe());
fetchShippingAddressDone$ = createEffect(() => this.actions$.pipe());
}

View File

@@ -0,0 +1,47 @@
import { TestBed } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { CustomersState } from './customers.state';
import { CustomersStateFacade } from './customers.facade';
import { of } from 'rxjs';
import * as CustomersSelectors from './customers.selectors';
fdescribe('#CustomersStateFacade', () => {
let facade: CustomersStateFacade;
let store: jasmine.SpyObj<Store<CustomersState>>;
const id = 123;
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [
CustomersStateFacade,
{
provide: Store,
useValue: jasmine.createSpyObj<Store<CustomersState>>('store', {
select: of({}),
}),
},
],
});
});
beforeEach(() => {
facade = TestBed.get(CustomersStateFacade);
store = TestBed.get(Store);
});
it('should be created', () => {
expect(facade instanceof CustomersStateFacade).toBeTruthy();
});
describe('#getGeneralAddress$', () => {
it('should select the general address from store', () => {
facade.getGeneralAddress$(id);
expect(store.select).toHaveBeenCalledWith(
CustomersSelectors.selectGeneralAddress,
id
);
});
});
});

View File

@@ -0,0 +1,57 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { CustomersState } from './customers.state';
import {
QueryTokenDTO,
CustomerInfoDTO,
AddressDTO,
ShippingAddressDTO,
AssignedPayerDTO,
} from '@swagger/crm';
import * as CustomersSelectors from './customers.selectors';
@Injectable({ providedIn: 'root' })
export class CustomersStateFacade {
constructor(private store: Store<any>) {}
public getGeneralAddress$(customerId: number): Observable<AddressDTO> {
return this.store.select(
CustomersSelectors.selectGeneralAddress,
customerId
);
}
public getPayerAddresses$(
customerId: number
): Observable<ShippingAddressDTO[]> {
return;
}
public getShippingAddresses$(
customerId: number
): Observable<AssignedPayerDTO[]> {
return;
}
public setBaseCustomer(customer: CustomerInfoDTO) {
// import mapper
// dispatch action to set customer
}
public setInvoiceAddresses(customerId: number, address: AddressDTO[]) {}
public setDeliveryAddresses(customerId: number, address: AddressDTO[]) {}
private fetchCustomer(id?: number) {
// fetch single customer
}
private fetchCustomers() {
// fetch multiple customers
}
private getQueryToken(): QueryTokenDTO {
return;
}
}

View File

@@ -0,0 +1,16 @@
import {
INITIAL_CUSTOMERS_STATE,
customersStateAdapter,
CustomersState,
} from './customers.state';
import * as actions from './customers.actions';
import { createReducer, on, Action } from '@ngrx/store';
export const _customersReducer = createReducer(
INITIAL_CUSTOMERS_STATE,
on(actions.addCustomer, (s, a) => customersStateAdapter.addOne(a.customer, s))
);
export function customersReducer(state: CustomersState, action: Action) {
return _customersReducer(state, action);
}

View File

@@ -0,0 +1,22 @@
import { CustomersState } from './customers.state';
import { customersStateMock } from './mocks';
import * as CustomersSelectors from './customers.selectors';
import { Dictionary } from '@cmf/core';
import { Customer } from './defs';
fdescribe('#CustomersSelectors', () => {
let entities: Dictionary<Customer>;
describe('selectGeneralAddress', () => {
beforeEach(() => {
entities = customersStateMock.entities;
});
it('should select the general address for the provided customerId', () => {
const id = 123;
expect(
CustomersSelectors.selectGeneralAddress.projector(entities, 123)
).toEqual(entities[id].addresses.generalAddress);
});
});
});

View File

@@ -0,0 +1,70 @@
import { createSelector } from '@ngrx/store';
import { Dictionary } from '@cmf/core';
import { customersStateAdapter } from './customers.state';
import { selectCustomerState } from '../customer.selector';
import { Customer } from './defs';
export const selectCustomersState = createSelector(
selectCustomerState,
(s) => s.customers
);
export const { selectAll, selectEntities } = customersStateAdapter.getSelectors(
selectCustomersState
);
export const selectCustomers = createSelector(
selectEntities,
(entities: Dictionary<Customer>) => entities
);
export const selectCustomer = createSelector(
selectEntities,
(entities: Dictionary<Customer>, id: number) => entities[id]
);
export const selectAddresses = createSelector(
selectEntities,
(entities: Dictionary<Customer>, id: number) =>
entities[id] && entities[id].addresses
);
export const selectGeneralAddress = createSelector(
selectEntities,
(entities: Dictionary<Customer>, id: number) =>
entities[id] &&
entities[id].addresses &&
entities[id].addresses.generalAddress
);
export const selectShippingAddresses = createSelector(
selectEntities,
(entities: Dictionary<Customer>, id: number) =>
entities[id] && entities[id].addresses.shippingAddresses
);
export const selectDefaultShippingAddress = createSelector(
selectEntities,
(entities: Dictionary<Customer>, id: number) =>
entities[id] &&
entities[id].addresses.shippingAddresses &&
entities[id].addresses.shippingAddresses.find(
(address) => address.isDefault
)
);
export const selectPayerAddresses = createSelector(
selectEntities,
(entities: Dictionary<Customer>, id: number) =>
entities[id] &&
entities[id].addresses &&
entities[id].addresses.payerAddresses
);
export const selectDefaultPayerAddresses = createSelector(
selectEntities,
(entities: Dictionary<Customer>, id: number) =>
entities[id] &&
entities[id].addresses &&
entities[id].addresses.payerAddresses.find((address) => address.isDefault)
);

View File

@@ -0,0 +1,26 @@
import { Customer } from './defs';
import { EntityState, createEntityAdapter } from '@ngrx/entity';
export interface CustomersState extends EntityState<Customer> {}
export const customersStateAdapter = createEntityAdapter<Customer>();
export const INITIAL_CUSTOMERS_STATE: CustomersState = {
...customersStateAdapter.getInitialState(),
};
export const INITIAL_CUSTOMER: Customer = {
id: undefined,
baseData: {},
addresses: {
generalAddress: {},
shippingAddresses: [],
payerAddresses: [],
shippingAddressIds: [],
payerAddressIds: [],
},
detailsLoaded: false,
};

View File

@@ -0,0 +1,10 @@
import { AddressDTO, ShippingAddressDTO, AssignedPayerDTO } from '@swagger/crm';
export interface CustomerAddresses {
generalAddress: AddressDTO;
shippingAddresses: ShippingAddressDTO[];
payerAddresses: AssignedPayerDTO[];
shippingAddressIds: number[];
payerAddressIds: number[];
}

View File

@@ -0,0 +1,29 @@
import {
EntityDTOContainerOfBranchDTO,
CustomerType,
CustomerStatus,
Gender,
NotificationChannel,
CommunicationDetailsDTO,
} from '@swagger/crm';
export interface CustomerBaseData {
/** Basisdaten */
gender?: Gender;
title?: string;
firstName?: string;
lastName?: string;
dateOfBirth?: string;
/** Kundentyp */
customerGroup?: string;
createdInBranch?: EntityDTOContainerOfBranchDTO;
customerNumber?: string;
customerType?: CustomerType;
customerStatus?: CustomerStatus;
isGuestAccount?: boolean;
hasOnlineAccount?: boolean;
notificationChannels?: NotificationChannel;
communicationDetails?: CommunicationDetailsDTO;
}

View File

@@ -0,0 +1,15 @@
import { CustomerBaseData } from './customer-base-data';
import { CustomerAddresses } from './customer-address';
export interface Customer {
id: number;
/** Stammdaten */
baseData: CustomerBaseData;
/** Addresdaten */
addresses: CustomerAddresses;
/** Wurden die Kundendetails geladen? */
detailsLoaded?: boolean;
}

View File

@@ -0,0 +1,6 @@
// start:ng42.barrel
export * from './customer-address';
export * from './customer-base-data';
export * from './customer.model';
// end:ng42.barrel

View File

@@ -0,0 +1,9 @@
// start:ng42.barrel
export * from './customers.actions';
export * from './customers.effects';
export * from './customers.facade';
export * from './customers.reducer';
export * from './customers.selectors';
export * from './customers.state';
// end:ng42.barrel

View File

@@ -0,0 +1,19 @@
import { CustomerDTO } from '@swagger/crm';
import { CustomerAddresses } from '../defs';
export function customerDtoToCustomerAddresses(
customerDto: CustomerDTO
): CustomerAddresses {
return {
generalAddress: customerDto.address,
shippingAddresses: [],
payerAddresses: [],
shippingAddressIds: customerDto.shippingAddresses.map(
(addressData) => addressData.id
),
payerAddressIds: customerDto.payers
.filter((addressData) => addressData.payer)
.map((addressData) => addressData.payer.id),
};
}

View File

@@ -0,0 +1,39 @@
import { CustomerDTO } from '@swagger/crm';
import { CustomerBaseData } from '../defs';
export function customerDtoToCustomerBaseData(
customerDto: CustomerDTO
): CustomerBaseData {
const {
gender,
title,
firstName,
lastName,
dateOfBirth,
customerGroup,
createdInBranch,
customerNumber,
customerType,
customerStatus,
isGuestAccount,
notificationChannels,
hasOnlineAccount,
communicationDetails,
} = customerDto;
return {
gender,
title,
firstName,
lastName,
dateOfBirth,
customerGroup,
createdInBranch,
customerNumber,
customerType,
customerStatus,
isGuestAccount,
notificationChannels,
hasOnlineAccount,
communicationDetails,
};
}

View File

@@ -0,0 +1,12 @@
import { CustomerDTO } from '@swagger/crm/lib';
import { Customer } from '../defs';
import { customerDtoToCustomerBaseData } from './customer-dto-to-customer-base-data.mapper';
import { customerDtoToCustomerAddresses } from './customer-dto-to-customer-addresses.mapper';
export function customerDtoToCustomer(customerDto: CustomerDTO): Customer {
return {
id: customerDto.id,
baseData: customerDtoToCustomerBaseData(customerDto),
addresses: customerDtoToCustomerAddresses(customerDto),
};
}

View File

@@ -0,0 +1,5 @@
// start:ng42.barrel
export * from './customer-dto-to-customer-base-data.mapper';
export * from './customer-dto-to-customer-addresses.mapper';
export * from './customer-dto-to-customer.mapper';
// end:ng42.barrel

View File

@@ -0,0 +1,46 @@
import { CustomersState } from '../customers.state';
export const customersStateMock: CustomersState = {
ids: [123, 456, 789],
entities: {
123: {
id: 123,
baseData: {},
addresses: {
generalAddress: {},
shippingAddresses: [],
payerAddresses: [],
shippingAddressIds: [],
payerAddressIds: [],
},
detailsLoaded: false,
},
456: {
id: 456,
baseData: {},
addresses: {
generalAddress: {},
shippingAddresses: [],
payerAddresses: [],
shippingAddressIds: [],
payerAddressIds: [],
},
detailsLoaded: false,
},
789: {
id: 789,
baseData: {},
addresses: {
generalAddress: {},
shippingAddresses: [],
payerAddresses: [],
shippingAddressIds: [],
payerAddressIds: [],
},
detailsLoaded: false,
},
},
};

View File

@@ -0,0 +1,4 @@
// start:ng42.barrel
export * from './customers-state.mock';
// end:ng42.barrel

View File

@@ -20,7 +20,6 @@ import {
ListResponseArgsOfOrderItemListItemDTO, ListResponseArgsOfOrderItemListItemDTO,
ResponseArgsOfIEnumerableOfInputDTO, ResponseArgsOfIEnumerableOfInputDTO,
} from '@swagger/oms'; } from '@swagger/oms';
import { BranchService } from '@sales/core-services';
import { SearchStateFacade } from './search.facade'; import { SearchStateFacade } from './search.facade';
import { of, NEVER } from 'rxjs'; import { of, NEVER } from 'rxjs';
import { import {

0
prod
View File