Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#163354061 Customer should be able to cancel orders #18

Merged
merged 1 commit into from
Jan 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ export const getUserOrderHistory = () => async (dispatch) => {
}
};

export const cancelOrder = orderId => async (dispatch) => {
dispatch(startFetching());
try {
await axios.delete(`/orders/${orderId}`);
dispatch({ type: types.CANCEL_ORDER, payload: { orderId } });
dispatch(stopFetching());
return toast.success('Order cancelled successfully 🎉');
} catch (error) {
dispatch({ type: types.CANCEL_ORDER_FAIL });
dispatch(stopFetching(false, errorResponse(error)));
return toast.error(errorResponse(error));
}
};

export const logout = () => (dispatch) => {
dispatch(emptyCart());
removeToken();
Expand Down
2 changes: 2 additions & 0 deletions src/actions/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export default {
EMPTY_CART: 'EMPTY_CART',
GET_ORDER_HISTORY: 'GET_ORDER_HISTORY',
GET_ORDER_HISTORY_FAIL: 'GET_ORDER_HISTORY_FAIL',
CANCEL_ORDER: 'CANCEL_ORDER',
CANCEL_ORDER_FAIL: 'CANCEL_ORDER_FAIL',
};
11 changes: 9 additions & 2 deletions src/components/Nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import LogoutLink from './Logout';

export const Nav = ({ isLoggedIn, role }) => {
export const Nav = ({ isLoggedIn, role, cartItemsCount }) => {
const navContent = () => {
if (isLoggedIn && role === 'customer') {
return (
Expand All @@ -13,7 +13,11 @@ export const Nav = ({ isLoggedIn, role }) => {
<Link to="/menu">Menu</Link>
</li>
<li>
<Link to="/cart">Cart</Link>
<Link to="/cart">
Cart [
{cartItemsCount}
]
</Link>
</li>
<li>
<Link to="/order-history">Order History</Link>
Expand Down Expand Up @@ -69,15 +73,18 @@ export const Nav = ({ isLoggedIn, role }) => {
Nav.propTypes = {
isLoggedIn: PropTypes.bool,
role: PropTypes.string.isRequired,
cartItemsCount: PropTypes.number,
};

Nav.defaultProps = {
isLoggedIn: false,
cartItemsCount: 0,
};

const mapStateToProps = state => ({
isLoggedIn: state.user.isLoggedIn,
role: state.user.role,
cartItemsCount: Object.keys(state.cart).length,
});

export default connect(mapStateToProps)(Nav);
12 changes: 10 additions & 2 deletions src/components/OrderCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';

const OrderCard = ({
foodItems, orderPrice, date, orderStatus,
foodItems, orderPrice, date, orderStatus, cancelOrderCallback,
}) => {
const formattedFoodNames = foodItems.map(foodName => (
<p key={Math.random() * foodItems.length * 12039}>{foodName}</p>
Expand Down Expand Up @@ -48,7 +48,14 @@ const OrderCard = ({
.join('/')}
</p>
</div>
<div className={`order-status-${status}`} />
<div className="order-status">
<div className={`order-status-${status}`} />
<div className="cancel-order">
<button className="small" type="button" onClick={cancelOrderCallback}>
cancel
</button>
</div>
</div>
</div>
);
};
Expand All @@ -58,6 +65,7 @@ OrderCard.propTypes = {
orderPrice: PropTypes.number.isRequired,
date: PropTypes.string.isRequired,
orderStatus: PropTypes.string.isRequired,
cancelOrderCallback: PropTypes.func.isRequired,
};

export default OrderCard;
33 changes: 22 additions & 11 deletions src/components/OrderHistory.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,39 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import OrderCard from './OrderCard';
import { getUserOrderHistory } from '../actions';
import { getUserOrderHistory, cancelOrder } from '../actions';

export class OrderHistory extends Component {
async componentDidMount() {
const { getUserOrderHistory: getOrderHistory } = this.props;
await getOrderHistory();
}

onCancelOrder = async (orderId) => {
const { cancelOrder: deleteOrder } = this.props;
await deleteOrder(Number(orderId));
};

render() {
const { orders } = this.props;

return (
<div className="wrapper">
<section className="container order-history">
{orders.map(order => (
<OrderCard
key={order.id}
foodItems={order.items}
orderPrice={order.price}
orderStatus={order.status}
date={order.date}
/>
))}
{orders.length ? (
orders.map(order => (
<OrderCard
key={order.id}
foodItems={order.items}
orderPrice={order.price}
orderStatus={order.status}
date={order.date}
cancelOrderCallback={() => this.onCancelOrder(order.id)}
/>
))
) : (
<h2>No Orders Yet!</h2>
)}
</section>
</div>
);
Expand All @@ -34,6 +44,7 @@ export class OrderHistory extends Component {
OrderHistory.propTypes = {
getUserOrderHistory: PropTypes.func.isRequired,
orders: PropTypes.instanceOf(Array).isRequired,
cancelOrder: PropTypes.func.isRequired,
};

const mapStateToProps = state => ({
Expand All @@ -42,5 +53,5 @@ const mapStateToProps = state => ({

export default connect(
mapStateToProps,
{ getUserOrderHistory },
{ getUserOrderHistory, cancelOrder },
)(OrderHistory);
2 changes: 1 addition & 1 deletion src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk)));
const jsx = (
<Provider store={store}>
<AppIndex />
<ToastContainer transition={Flip} position="top-center" autoClose={3500} />
<ToastContainer transition={Flip} position="bottom-right" autoClose={3500} />
</Provider>
);

Expand Down
6 changes: 6 additions & 0 deletions src/reducers/ordersReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ export default (state = intialState, action) => {
switch (action.type) {
case types.GET_ORDER_HISTORY:
return { ...intialState, history: action.payload.orders };
case types.CANCEL_ORDER:
return {
...state,
history: state.history.filter(order => order.id !== action.payload.orderId),
};
case types.GET_ORDER_HISTORY_FAIL:
case types.CANCEL_ORDER_FAIL:
default:
return state;
}
Expand Down
40 changes: 39 additions & 1 deletion src/tests/actions/orders.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { getUserOrderHistory } from '../../actions';
import { getUserOrderHistory, cancelOrder } from '../../actions';
import jwt from '../../utils/jwt';
import axios from '../../services/axios';

afterAll(() => jest.restoreAllMocks());

describe('getUserOrderHistory', () => {
jest.spyOn(jwt, 'decode').mockImplementation(() => ({ userId: 1 }));
const dispatch = jest.fn();
Expand Down Expand Up @@ -35,3 +37,39 @@ describe('getUserOrderHistory', () => {
});
});
});

describe('cancelOrder()', () => {
const dispatch = jest.fn();
const errResponse = { response: { data: { message: 'failed to cancel order' } } };
afterEach(() => jest.resetAllMocks());

it('should dispatch correct action to cancel an order', async () => {
jest.spyOn(axios, 'delete').mockImplementation(() => Promise.resolve());
await cancelOrder(7)(dispatch);

expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch.mock.calls[1][0]).toEqual({
type: 'CANCEL_ORDER',
payload: { orderId: 7 },
});
expect(dispatch).toHaveBeenLastCalledWith({
type: 'STOP_FETCHING',
payload: { error: false, message: '' },
});
});

it('should dispatch correct action for order cancellation failure', async () => {
jest.spyOn(axios, 'delete').mockImplementation(() => Promise.reject(errResponse));
await cancelOrder(2)(dispatch);

expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch.mock.calls[1][0]).toEqual({ type: 'CANCEL_ORDER_FAIL' });
expect(dispatch).toHaveBeenLastCalledWith({
type: 'STOP_FETCHING',
payload: {
error: true,
message: 'failed to cancel order',
},
});
});
});
10 changes: 10 additions & 0 deletions src/tests/components/OrderHistory.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('<OrderHistory />', () => {
const props = {
orders,
getUserOrderHistory: jest.fn(),
cancelOrder: jest.fn(),
};

it('should render correctly', () => {
Expand All @@ -47,4 +48,13 @@ describe('<OrderHistory />', () => {
expect(orderCards.length).toEqual(4);
wrapper.unmount();
});

it('should fire cancel order action creator upon order cancellation request', () => {
const wrapper = mount(<OrderHistory {...props} />);
const fourthOrderCard = wrapper.find('.order-card').last();
fourthOrderCard.find('.cancel-order > button').simulate('click');

expect(props.cancelOrder).toHaveBeenCalled();
expect(props.cancelOrder).toHaveBeenCalledWith(4);
});
});
2 changes: 2 additions & 0 deletions src/tests/components/__snapshots__/App.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ exports[`App component should render app correctly 1`] = `
>
<Connect(Nav)>
<Nav
cartItemsCount={0}
dispatch={[Function]}
isLoggedIn={null}
role=""
Expand Down Expand Up @@ -262,6 +263,7 @@ exports[`App component should render app correctly for authenticated customers 1
>
<Connect(Nav)>
<Nav
cartItemsCount={0}
dispatch={[Function]}
isLoggedIn={null}
role=""
Expand Down
44 changes: 41 additions & 3 deletions src/tests/reducers/orders.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,52 @@ const initialState = Object.freeze({
history: [],
});

const stateWithOrders = Object.freeze({
history: [
{
id: 2,
items: ['Tasty Prawns', 'Chicken Wings'],
price: 2100,
date: '2018-10-19T00:00:00.000Z',
status: 'cancelled',
},
{
id: 5,
items: ['Tasty Prawns'],
price: 1250,
date: '2018-11-25T00:00:00.000Z',
status: 'new',
},
{
id: 9,
items: ['Tasty Prawns', 'Turkey Wings'],
price: 2200,
date: '2018-12-02T00:00:00.000Z',
status: 'new',
},
],
});

const getOrderHistoryAction = {
type: 'GET_ORDER_HISTORY',
payload: {
orders: [
{
foodName: 'das',
foodPrice: 20,
date: 'yesterday?',
id: 55,
items: ['Tasty Prawns'],
price: 1250,
date: '2018-11-25T00:00:00.000Z',
status: 'new',
},
],
},
};

const cancelOrderAction = {
type: 'CANCEL_ORDER',
payload: { orderId: 5 },
};

const getOrderHistoryFailAction = { type: 'GET_ORDER_HISTORY_FAIL' };

describe('orders reducer', () => {
Expand All @@ -30,4 +63,9 @@ describe('orders reducer', () => {
const state = ordersReducer(undefined, getOrderHistoryFailAction);
expect({ ...state.history }).toEqual({ ...initialState.history });
});

it('should return correct state for cancelling user order', () => {
const state = ordersReducer(stateWithOrders, cancelOrderAction);
expect(state.history).toHaveLength(2);
});
});