Last active
May 8, 2017 16:36
-
-
Save sluongng/e16f3bdb5ce52444a29da56de5e8043c to your computer and use it in GitHub Desktop.
lzd-cart-project
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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)); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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 | |
} | |
] | |
} | |
] | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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), | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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