Skip to content

Instantly share code, notes, and snippets.

@sluongng
Last active May 8, 2017 16:36
Show Gist options
  • Save sluongng/e16f3bdb5ce52444a29da56de5e8043c to your computer and use it in GitHub Desktop.
Save sluongng/e16f3bdb5ce52444a29da56de5e8043c to your computer and use it in GitHub Desktop.
lzd-cart-project
//Lib file for frontend
import config from '../config';
import AWS from 'aws-sdk';
export async function invokeLzdApiGateway(
{
path,
method = 'GET',
body
}, userToken) {
const url = `${config.lzdCartApiGateway.URL}${path}`;
const headers = {
"Content-Type": 'application/responsejson'
};
body = (body) ? JSON.stringify(body) : body;
const results = await fetch(url, {
method,
body,
headers
});
return results.json();
}
/**
* Created by NB on 5/6/2017.
*/
import { success, notFound } from "./libs/response-lib";
import _ from "lodash";
import { priceTable } from "./priceTable2.json";
//shipping price is simply a relationship between location points
//this could either be done by a generic distance lookup via online map webservice
//OR we could store locations in a graphDb
//
//realistically 2 different items sharing the same source-destination could still have different shipping price due
//to different vendor shipping method
//
//here I used a temp hardcoded JSON to focus on demonstrating solution for price calculation
//this is not ideal and should not be applied in production
//
//General lookup sequence: Item -> Source -> Destination -> Price
const CONST_PRICE_TABLE = priceTable;
//this should be in a separate config file
//but that would require an endpoint to
//fetch config file from AWS S3
//thus increase the complexity of demo solution
const CONST_FLAT_RATE = 5.00;
//async was added as a provision of future usages of Database queries
//
export async function main(event, context, callback) {
const data = JSON.parse(event.body);
const destination = data.destination;
let res = _.cloneDeep(data);
if (res.items.size < 1) {
callback(notFound("Cart is empty"));
return;
}
res.items.map((item) => {
const temp = shippingCost(destination, item, callback);
if (temp === -1) callback(null, notFound("Item not found in price table"));
item.shippingCost = temp.price + temp.overweightFee;
item.bestSource = temp.sourceId;
});
let totalShippingCost = 0;
let totalValue = 0;
res.items.forEach(function(item) {
totalValue = totalValue + (item.itemDetails.value * item.quantity);
});
if (totalValue <= 100) totalShippingCost = totalShippingCost + CONST_FLAT_RATE;
res.items.forEach(function(item) {
totalShippingCost = totalShippingCost + item.shippingCost;
});
res.cartPrice = totalValue + totalShippingCost;
callback(null, success(res));
}
//Input:
// destination
// item
//Output:
// lowest 'cost' of respective 'item' shipping to respective 'destination'
//Error:
// [400] item not found
// [400] destination not found
function shippingCost(destination, item, callback) {
// DEBUG
console.log("STARTING shippingCost for destination " + destination + " and item " + item.itemId);
const itemId = item.itemId;
//find index of item in priceTable
//TODO: replace _.findIndex with _.find
let index = _.findIndex( CONST_PRICE_TABLE , function(o) {
// DEBUG
//console.log("itemId inside priceTable: " + o.itemId);
return o.itemId === itemId;
});
if (index === -1) {
callback(null, notFound("Item not found in price table"));
return;
}
// DEBUG
// console.log("The index id is: " + index);
// console.log("-----------------");
const sources = CONST_PRICE_TABLE[index].sources;
// DEBUG
// console.log("The first sourceId of item " + itemId + " is: " + sources[0].sourceId);
//create an array of {sourceId, price}
const sourceAndPrice = sources.map(function(source) {
// DEBUG
console.log("Start mapping source with ID: " + source.sourceId);
const destinationList = source.destinations;
// var i = _.findIndex(destinations, function(o) { return o.destination; })
//
// if (i === -1) return -2;
let temp = _.find(destinationList, function(dest) {
// DEBUG
console.log("The dest node " + dest.destination + " vs " + destination);
console.log("Return value is " + (dest.destination === destination));
return dest.destination == destination;
});
console.log("Type of var_temp: " + (typeof temp));
if (typeof temp === "undefined") {
callback(null, notFound("Destination " + destination + " was not found in Price Table"));
return;
}
return {
sourceId: source.sourceId,
price: temp.price
};
});
let prices = _.map(sourceAndPrice, 'price');
let minCost = _.min(prices);
let bestSourceAndPrice = _.find(sourceAndPrice, function(o) { return o.price === minCost; });
// sources.forEach( (source) => {
// const location = source.location;
// } );
let overweightFee = 0;
const totalWeight = item.itemDetails.weight * item.quantity;
if (totalWeight > 1) overweightFee = (CONST_FLAT_RATE * 10 / 100) * (totalWeight - 1);
const res = {
sourceId: bestSourceAndPrice.sourceId,
price: bestSourceAndPrice.price,
overweightFee: overweightFee
};
// DEBUG
console.log("bestSource is: " + res.sourceId);
console.log("minCost is: " + res.price);
console.log("overweightFee is: " + res.overweightFee);
console.log("-----END------");
return res;
}
/**
* Created by NB on 5/7/2017.
*/
import React, {Component} from "react";
import {withRouter} from "react-router-dom";
import {
ControlLabel,
FormControl,
FormGroup,
InputGroup,
Form,
PageHeader,
ListGroup,
Col,
Panel,
Grid,
Button
} from "react-bootstrap";
import "./Cart.css";
import {invokeLzdApiGateway} from "../libs/awsLib";
import _ from "lodash";
class Cart extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
cartContent: null
};
}
async componentWillMount() {
this.setState({isLoading: true});
try {
const initCart = await this.getCart();
const results = await this.calCart(initCart);
this.setState({cartContent: results});
}
catch (e) {
alert(e);
}
this.setState({isLoading: false});
}
async changeQuantity( iter, event ) {
this.setState({isLoading: true});
const newQuantity = event.target.value;
let newCart = _.cloneDeep(this.state.cartContent);
newCart.items[iter].quantity = newQuantity;
try {
const results = await this.calCart(newCart);
this.setState({cartContent: results});
}
catch (e) {
alert(e);
}
this.setState({isLoading: false});
}
async changeDestination(event) {
this.setState({isLoading: true});
const newDestination = event.target.value;
let newCart = _.cloneDeep(this.state.cartContent);
newCart.destination = newDestination;
try {
const results = await this.calCart(newCart);
this.setState({cartContent: results});
}
catch (e) {
alert(e);
}
this.setState({isLoading: false});
}
calCart(cart) {
const result = invokeLzdApiGateway({path: '/lzd/cart/calculate', method: 'POST', body: cart}, this.props.userToken);
return result;
}
getCart() {
const results = invokeLzdApiGateway({path: '/lzd/cart/cart123'}, this.props.userToken);
return results;
}
renderItemsList(cart) {
return cart.items.map((item, i) => (
<Panel header={item.itemDetails.name.trim()} bsStyle="info">
<Form horizontal>
<FormGroup controlId="ItemPrice">
<Col componentClass={ControlLabel} sm={4}>
Price Each
</Col>
<Col sm={8}>
<InputGroup>
<FormControl readOnly type="number" value={item.itemDetails.value} />
<InputGroup.Addon>$</InputGroup.Addon>
</InputGroup>
</Col>
</FormGroup>
<FormGroup controlId="ItemWeight">
<Col componentClass={ControlLabel} sm={4}>
Weight Each
</Col>
<Col sm={8}>
<InputGroup>
<FormControl readOnly type="number" value={item.itemDetails.weight} />
<InputGroup.Addon>kg</InputGroup.Addon>
</InputGroup>
</Col>
</FormGroup>
<FormGroup controlId="ItemQuantity">
<Col componentClass={ControlLabel} sm={4}>
Quantity
</Col>
<Col sm={8}>
<FormControl type="number" defaultValue={item.quantity} onBlur={this.changeQuantity.bind(this, i)}/>
{/*<FormControl type="number" defaultValue={item.quantity} />*/}
</Col>
</FormGroup>
<FormGroup controlId="ItemSource">
<Col componentClass={ControlLabel} sm={4}>
Shipping From
</Col>
<Col sm={8}>
<FormControl readOnly type="text" defaultValue={item.bestSource} />
</Col>
</FormGroup>
</Form>
</Panel>
));
}
renderCartInfo(cart) {
return (
<Panel header="Cart Total" bsStyle="success">
<Form horizontal>
<FormGroup controlId="CartId">
<br/>
<Col componentClass={ControlLabel} sm={4}>
Cart ID
</Col>
<Col sm={8}>
<FormControl.Static>{cart.cartId}</FormControl.Static>
</Col>
</FormGroup>
<FormGroup controlId="CartDestination">
<Col componentClass={ControlLabel} sm={4}>
Destination
</Col>
<Col sm={8}>
<FormControl componentClass="select" placeholder="Postal Code" value={this.state.cartContent.destination} onChange={this.changeDestination.bind(this)}>
<option value="100001">100001</option>
<option value="100002">100002</option>
<option value="100003">100003</option>
</FormControl>
</Col>
</FormGroup>
<FormGroup controlId="Total Price">
<Col componentClass={ControlLabel} sm={4}>
Total Price
</Col>
<Col sm={8}>
<FormControl readOnly type="number" defaultValue={cart.cartPrice} />
</Col>
</FormGroup>
<FormGroup controlId="PurchaseBttn" style={{maxWidth: 250, margin: '0 auto 10px'}}>
<Button bsStyle="danger" bsSize="large" block>Purchase</Button>
</FormGroup>
</Form>
</Panel>
);
}
render() {
return (
<div className="Cart">
<PageHeader>Your Cart</PageHeader>
<Grid>
<Col xs={12} md={8}>
<ListGroup>
{
!this.state.isLoading
&& this.renderItemsList(this.state.cartContent)
}
</ListGroup>
</Col>
<Col xs={6} md={4}>
{
!this.state.isLoading
&& this.renderCartInfo(this.state.cartContent)
}
</Col>
</Grid>
</div>
);
}
}
export default withRouter(Cart);
/**
* Created by NB on 5/6/2017.
*/
import { success, failure } from './libs/response-lib';
export async function main(event, context, callback) {
const defaultCart = {
cartId: 'Cart12334',
items: [
{
itemId: 'item1',
itemDetails: {
name: 'ePhone 9',
pictures: [
{
pictureId: 100001,
resourceUrl: ''
}
],
value: 700,
weight: 0.14,
sources: [
{source: 100003},
{source: 100001}
]
},
bestSource: '',
quantity: 1,
shippingCost: 0.00,
totalPrice: 0.00
},
{
itemId: 'item2',
itemDetails: {
name: 'TV SamSong 40 inches',
pictures: [
{
pictureId: 100001,
resourceUrl: ''
}
],
value: 1000,
weight: 8.00,
sources: [
{source: 100001},
{source: 100002}
]
},
bestSource: '',
quantity: 1,
shippingCost: 0.00,
totalPrice: 0.00
},
{
itemId: 'item3',
itemDetails: {
name: 'grounded coffee arabica',
pictures: [
{
pictureId: 100001,
resourceUrl: ''
}
],
value: 35,
weight: 0.1,
sources: [
{source: 100001},
{source: 100002}
]
},
bestSource: '',
quantity: 1,
shippingCost: 0.00,
totalPrice: 0.00
},
],
destination: 100003,
cartPrice: 0.00
};
callback(null, success(defaultCart));
}
{
"priceTable": [
{
"itemId": "item1",
"sources": [
{
"sourceId": 100001,
"destinations": [
{
"destination": 100001,
"price": 0.00
},
{
"destination": 100002,
"price": 3.00
},
{
"destination": 100003,
"price": 10.00
}
]
},
{
"sourceId": 100003,
"destinations": [
{
"destination": 100001,
"price": 10.00
},
{
"destination": 100002,
"price": 5.00
},
{
"destination": 100003,
"price": 0.00
}
]
}
]
},
{
"itemId": "item2",
"sources": [
{
"sourceId": 100001,
"destinations": [
{
"destination": 100001,
"price": 0.00
},
{
"destination": 100002,
"price": 2.00
},
{
"destination": 100003,
"price": 4.00
}
]
},
{
"sourceId": 100002,
"destinations": [
{
"destination": 100001,
"price": 2.00
},
{
"destination": 100002,
"price": 0.00
},
{
"destination": 100003,
"price": 8.00
}
]
}
]
},
{
"itemId": "item3",
"sources": [
{
"sourceId": 100001,
"destinations": [
{
"destination": 100001,
"price": 0.00
},
{
"destination": 100002,
"price": 7.00
},
{
"destination": 100003,
"price": 4.00
}
]
},
{
"sourceId": 100002,
"destinations": [
{
"destination": 100001,
"price": 7.00
},
{
"destination": 100002,
"price": 0.00
},
{
"destination": 100003,
"price": 3.00
}
]
}
]
}
]
}
/**
* Created by NB on 5/5/2017.
*/
//backend lib
export function success(body) {
return buildResponse(200, body);
}
export function notFound(body) {
return buildResponse(400, body);
}
export function failure(body) {
return buildResponse(500, body);
}
function buildResponse(statusCode, body) {
return {
statusCode: statusCode,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
},
body: JSON.stringify(body),
};
}
service: lzd-cart-backend
plugins:
- serverless-webpack
custom:
webpackIncludeModules: true
provider:
name: aws
runtime: nodejs6.10
stage: prod
region: ap-northeast-2
functions:
getCart:
handler: getCart.main
events:
- http:
path: /lzd/cart/{id}
method: get
cors: true
calculateCart:
handler: calculateCart.main
events:
- http:
path: /lzd/cart/calculate
method: post
cors: true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment