Skip to content

Commit

Permalink
feat(checkout,order): move placed order facade and selector to checko…
Browse files Browse the repository at this point in the history
…ut (#2750)
  • Loading branch information
griest024 committed Feb 14, 2024
1 parent 65aa9bc commit de45407
Show file tree
Hide file tree
Showing 26 changed files with 438 additions and 1 deletion.
2 changes: 2 additions & 0 deletions libs/checkout/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@angular/core": "0.0.0-PLACEHOLDER",
"@daffodil/cart": "0.0.0-PLACEHOLDER",
"@daffodil/core": "0.0.0-PLACEHOLDER",
"@daffodil/order": "0.0.0-PLACEHOLDER",
"@daffodil/product": "0.0.0-PLACEHOLDER",
"@ngrx/effects": "0.0.0-PLACEHOLDER",
"@ngrx/entity": "0.0.0-PLACEHOLDER",
Expand All @@ -44,6 +45,7 @@
"devDependencies": {
"@daffodil/cart": "0.0.0-PLACEHOLDER",
"@daffodil/core": "0.0.0-PLACEHOLDER",
"@daffodil/order": "0.0.0-PLACEHOLDER",
"@daffodil/product": "0.0.0-PLACEHOLDER"
}
}
6 changes: 6 additions & 0 deletions libs/checkout/state/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
"lib": {
"entryFile": "src/index.ts"
}
}
3 changes: 3 additions & 0 deletions libs/checkout/state/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "@daffodil/checkout/state"
}
15 changes: 15 additions & 0 deletions libs/checkout/state/src/checkout-state.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';

import { DaffCartStateModule } from '@daffodil/cart/state';
import { DaffOrderStateModule } from '@daffodil/order/state';

/**
* The module for `@daffodil/checkout/state`.
*/
@NgModule({
imports: [
DaffCartStateModule,
DaffOrderStateModule,
],
})
export class DaffCheckoutStateModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Action } from '@ngrx/store';
import { Observable } from 'rxjs';

import { DaffStoreFacade } from '@daffodil/core/state';
import { DaffOrder } from '@daffodil/order';

/**
* Represents the surface of placed order state.
*/
export interface DaffCheckoutPlacedOrderFacadeInterface<T extends DaffOrder = DaffOrder> extends DaffStoreFacade<Action> {
/**
* The most recently placed order (if any).
*/
placedOrder$: Observable<T>;
/**
* Whether there is a placed order.
*/
hasPlacedOrder$: Observable<boolean>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { TestBed } from '@angular/core/testing';
import {
StoreModule,
combineReducers,
Store,
} from '@ngrx/store';
import { cold } from 'jasmine-marbles';

import {
DaffCartPlaceOrderSuccess,
daffCartReducers,
DAFF_CART_STORE_FEATURE_KEY,
} from '@daffodil/cart/state';
import { DaffCheckoutStateRootSlice } from '@daffodil/checkout/state';
import { DaffOrder } from '@daffodil/order';
import {
DaffOrderLoadSuccess,
daffOrderReducers,
DAFF_ORDER_STORE_FEATURE_KEY,
} from '@daffodil/order/state';
import { DaffOrderFactory } from '@daffodil/order/testing';

import { DaffCheckoutPlacedOrderFacade } from './placed-order.facade';

describe('@daffodil/checkout/state | DaffCheckoutPlacedOrderFacade', () => {
let store: Store<DaffCheckoutStateRootSlice>;
let facade: DaffCheckoutPlacedOrderFacade;
let orderFactory: DaffOrderFactory;

let mockOrder: DaffOrder;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({
[DAFF_ORDER_STORE_FEATURE_KEY]: combineReducers(daffOrderReducers),
[DAFF_CART_STORE_FEATURE_KEY]: combineReducers(daffCartReducers),
}),
],
providers: [
DaffCheckoutPlacedOrderFacade,
],
});

store = TestBed.inject(Store);
facade = TestBed.inject(DaffCheckoutPlacedOrderFacade);
orderFactory = TestBed.inject(DaffOrderFactory);

mockOrder = orderFactory.create();
});

it('should be created', () => {
expect(facade).toBeTruthy();
});

it('should be able to dispatch an action to the store', () => {
spyOn(store, 'dispatch');
const action = { type: 'SOME_TYPE' };

facade.dispatch(action);
expect(store.dispatch).toHaveBeenCalledWith(action);
expect(store.dispatch).toHaveBeenCalledTimes(1);
});

describe('placedOrder$', () => {
it('should initially be null', () => {
const expected = cold('a', { a: null });
expect(facade.placedOrder$).toBeObservable(expected);
});

it('should contain the order upon a successful place order and order load', () => {
const expected = cold('a', { a: mockOrder });
store.dispatch(new DaffCartPlaceOrderSuccess({ orderId: mockOrder.id, cartId: 'cartId' }));
store.dispatch(new DaffOrderLoadSuccess(mockOrder));
expect(facade.placedOrder$).toBeObservable(expected);
});
});

describe('hasPlacedOrder$', () => {
it('should initially be false', () => {
const expected = cold('a', { a: false });
expect(facade.hasPlacedOrder$).toBeObservable(expected);
});

it('should be true upon a successful place order and order load', () => {
const expected = cold('a', { a: true });
store.dispatch(new DaffCartPlaceOrderSuccess({ orderId: mockOrder.id, cartId: 'cartId' }));
store.dispatch(new DaffOrderLoadSuccess(mockOrder));
expect(facade.hasPlacedOrder$).toBeObservable(expected);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import {
Action,
Store,
select,
} from '@ngrx/store';
import { Observable } from 'rxjs';

import { DaffOrder } from '@daffodil/order';

import { DaffCheckoutPlacedOrderFacadeInterface } from './placed-order-facade.interface';
import { DaffCheckoutStateRootSlice } from '../../reducers/public_api';
import { getDaffCheckoutSelectors } from '../../selectors/public_api';

/**
* @inheritdoc
*/
@Injectable({
providedIn: 'root',
})
export class DaffCheckoutPlacedOrderFacade<T extends DaffOrder = DaffOrder> implements DaffCheckoutPlacedOrderFacadeInterface<T> {
placedOrder$: Observable<T>;
hasPlacedOrder$: Observable<boolean>;

constructor(private store: Store<DaffCheckoutStateRootSlice<T>>) {
const {
selectPlacedOrder,
selectHasPlacedOrder,
} = getDaffCheckoutSelectors<T>();

this.placedOrder$ = this.store.pipe(select(selectPlacedOrder));
this.hasPlacedOrder$ = this.store.pipe(select(selectHasPlacedOrder));
}

dispatch(action: Action) {
this.store.dispatch(action);
}
}
2 changes: 2 additions & 0 deletions libs/checkout/state/src/facades/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DaffCheckoutPlacedOrderFacadeInterface } from './placed-order/placed-order-facade.interface';
export { DaffCheckoutPlacedOrderFacade } from './placed-order/placed-order.facade';
1 change: 1 addition & 0 deletions libs/checkout/state/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './public_api';
5 changes: 5 additions & 0 deletions libs/checkout/state/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './selectors/public_api';
export * from './facades/public_api';
export * from './reducers/public_api';

export { DaffCheckoutStateModule } from './checkout-state.module';
8 changes: 8 additions & 0 deletions libs/checkout/state/src/reducers/order-reducers.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { DaffCartStateRootSlice } from '@daffodil/cart/state';
import { DaffOrder } from '@daffodil/order';
import { DaffOrderStateRootSlice } from '@daffodil/order/state';

/**
* The footprint of `@daffodil/checkout/state` in root.
*/
export interface DaffCheckoutStateRootSlice<T extends DaffOrder = DaffOrder> extends DaffCartStateRootSlice, DaffOrderStateRootSlice<T> {}
1 change: 1 addition & 0 deletions libs/checkout/state/src/reducers/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DaffCheckoutStateRootSlice } from './order-reducers.interface';
19 changes: 19 additions & 0 deletions libs/checkout/state/src/selectors/checkout-all.selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DaffOrder } from '@daffodil/order';

import {
DaffCheckoutPlacedOrderSelectors,
getCheckoutPlacedOrderSelectors,
} from './placed-order.selector';

export type DaffCheckoutSelectors<T extends DaffOrder = DaffOrder> = DaffCheckoutPlacedOrderSelectors<T>;

/**
* Gets all of `@daffodil/checkout/state` selectors.
*/
export const getDaffCheckoutSelectors = (() => {
let cache;
return <T extends DaffOrder = DaffOrder>(): DaffCheckoutSelectors<T> =>
cache = cache || {
...getCheckoutPlacedOrderSelectors<T>(),
};
})();
111 changes: 111 additions & 0 deletions libs/checkout/state/src/selectors/placed-order.selector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { TestBed } from '@angular/core/testing';
import {
Store,
select,
combineReducers,
StoreModule,
} from '@ngrx/store';
import { cold } from 'jasmine-marbles';

import {
DAFF_CART_STORE_FEATURE_KEY,
daffCartReducers,
DaffCartPlaceOrderSuccess,
} from '@daffodil/cart/state';
import { DaffCheckoutStateRootSlice } from '@daffodil/checkout/state';
import {
DaffOrder,
DaffOrderCollection,
} from '@daffodil/order';
import {
DaffOrderListSuccess,
daffOrderReducers,
DAFF_ORDER_STORE_FEATURE_KEY,
} from '@daffodil/order/state';
import {
DaffOrderCollectionFactory,
DaffOrderFactory,
} from '@daffodil/order/testing';

import { getCheckoutPlacedOrderSelectors } from './placed-order.selector';

describe('@daffodil/checkout/state | getCheckoutPlacedOrderSelectors', () => {
let store: Store<DaffCheckoutStateRootSlice>;

let orderFactory: DaffOrderFactory;
let orderCollectionFactory: DaffOrderCollectionFactory;

let mockOrder: DaffOrder;
let mockOrderCollection: DaffOrderCollection;

const {
selectPlacedOrder,
selectHasPlacedOrder,
} = getCheckoutPlacedOrderSelectors();

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({
[DAFF_CART_STORE_FEATURE_KEY]: combineReducers(daffCartReducers),
[DAFF_ORDER_STORE_FEATURE_KEY]: combineReducers(daffOrderReducers),
}),
],
});

store = TestBed.inject(Store);
orderFactory = TestBed.inject(DaffOrderFactory);
orderCollectionFactory = TestBed.inject(DaffOrderCollectionFactory);

mockOrderCollection = orderCollectionFactory.create();
mockOrder = mockOrderCollection.data[mockOrderCollection.metadata.ids[0]];

store.dispatch(new DaffOrderListSuccess(mockOrderCollection));
});

describe('selectPlacedOrder', () => {
it('should initially be null', () => {
const selector = store.pipe(select(selectPlacedOrder));
const expected = cold('a', { a: null });

expect(selector).toBeObservable(expected);
});

describe('when an order has been placed and loaded', () => {
beforeEach(() => {
store.dispatch(new DaffCartPlaceOrderSuccess({ orderId: mockOrder.id, cartId: 'cartId' }));
store.dispatch(new DaffOrderListSuccess(mockOrderCollection));
});

it('should select the most recently placed order', () => {
const selector = store.pipe(select(selectPlacedOrder));
const expected = cold('a', { a: mockOrder });

expect(selector).toBeObservable(expected);
});
});
});

describe('selectHasPlacedOrder', () => {
it('should initially be false', () => {
const selector = store.pipe(select(selectHasPlacedOrder));
const expected = cold('a', { a: false });

expect(selector).toBeObservable(expected);
});

describe('when an order has been placed and loaded', () => {
beforeEach(() => {
store.dispatch(new DaffCartPlaceOrderSuccess({ orderId: mockOrder.id, cartId: 'cartId' }));
store.dispatch(new DaffOrderListSuccess(mockOrderCollection));
});

it('should select if the most recently placed order exists', () => {
const selector = store.pipe(select(selectHasPlacedOrder));
const expected = cold('a', { a: true });

expect(selector).toBeObservable(expected);
});
});
});
});
Loading

0 comments on commit de45407

Please sign in to comment.