408 lines
12 KiB
JavaScript
408 lines
12 KiB
JavaScript
const instana = require('@instana/collector');
|
|
// init tracing
|
|
// MUST be done before loading anything else!
|
|
instana({
|
|
tracing: {
|
|
enabled: true
|
|
}
|
|
});
|
|
|
|
const redis = require('redis');
|
|
const request = require('request');
|
|
const bodyParser = require('body-parser');
|
|
const express = require('express');
|
|
const pino = require('pino');
|
|
const expPino = require('express-pino-logger');
|
|
// Prometheus
|
|
const promClient = require('prom-client');
|
|
const Registry = promClient.Registry;
|
|
const register = new Registry();
|
|
const counter = new promClient.Counter({
|
|
name: 'items_added',
|
|
help: 'running count of items added to cart',
|
|
registers: [register]
|
|
});
|
|
|
|
|
|
var redisConnected = false;
|
|
|
|
var redisHost = process.env.REDIS_HOST || 'redis'
|
|
var catalogueHost = process.env.CATALOGUE_HOST || 'catalogue'
|
|
|
|
const logger = pino({
|
|
level: 'info',
|
|
prettyPrint: false,
|
|
useLevelLabels: true
|
|
});
|
|
const expLogger = expPino({
|
|
logger: logger
|
|
});
|
|
|
|
const app = express();
|
|
|
|
app.use(expLogger);
|
|
|
|
app.use((req, res, next) => {
|
|
res.set('Timing-Allow-Origin', '*');
|
|
res.set('Access-Control-Allow-Origin', '*');
|
|
next();
|
|
});
|
|
|
|
app.use((req, res, next) => {
|
|
let dcs = [
|
|
"asia-northeast2",
|
|
"asia-south1",
|
|
"europe-west3",
|
|
"us-east1",
|
|
"us-west1"
|
|
];
|
|
let span = instana.currentSpan();
|
|
span.annotate('custom.sdk.tags.datacenter', dcs[Math.floor(Math.random() * dcs.length)]);
|
|
|
|
next();
|
|
});
|
|
|
|
app.use(bodyParser.urlencoded({ extended: true }));
|
|
app.use(bodyParser.json());
|
|
|
|
app.get('/health', (req, res) => {
|
|
var stat = {
|
|
app: 'OK',
|
|
redis: redisConnected
|
|
};
|
|
res.json(stat);
|
|
});
|
|
|
|
// Prometheus
|
|
app.get('/metrics', (req, res) => {
|
|
res.header('Content-Type', 'text/plain');
|
|
res.send(register.metrics());
|
|
});
|
|
|
|
|
|
// get cart with id
|
|
app.get('/cart/:id', (req, res) => {
|
|
redisClient.get(req.params.id, (err, data) => {
|
|
if(err) {
|
|
req.log.error('ERROR', err);
|
|
res.status(500).send(err);
|
|
} else {
|
|
if(data == null) {
|
|
res.status(404).send('cart not found');
|
|
} else {
|
|
res.set('Content-Type', 'application/json');
|
|
res.send(data);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// delete cart with id
|
|
app.delete('/cart/:id', (req, res) => {
|
|
redisClient.del(req.params.id, (err, data) => {
|
|
if(err) {
|
|
req.log.error('ERROR', err);
|
|
res.status(500).send(err);
|
|
} else {
|
|
if(data == 1) {
|
|
res.send('OK');
|
|
} else {
|
|
res.status(404).send('cart not found');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// rename cart i.e. at login
|
|
app.get('/rename/:from/:to', (req, res) => {
|
|
redisClient.get(req.params.from, (err, data) => {
|
|
if(err) {
|
|
req.log.error('ERROR', err);
|
|
res.status(500).send(err);
|
|
} else {
|
|
if(data == null) {
|
|
res.status(404).send('cart not found');
|
|
} else {
|
|
var cart = JSON.parse(data);
|
|
saveCart(req.params.to, cart).then((data) => {
|
|
res.json(cart);
|
|
}).catch((err) => {
|
|
req.log.error(err);
|
|
res.status(500).send(err);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// update/create cart
|
|
app.get('/add/:id/:sku/:qty', (req, res) => {
|
|
// check quantity
|
|
var qty = parseInt(req.params.qty);
|
|
if(isNaN(qty)) {
|
|
req.log.warn('quantity not a number');
|
|
res.status(400).send('quantity must be a number');
|
|
return;
|
|
} else if(qty < 1) {
|
|
req.log.warn('quantity less than one');
|
|
res.status(400).send('quantity has to be greater than zero');
|
|
return;
|
|
}
|
|
|
|
// look up product details
|
|
getProduct(req.params.sku).then((product) => {
|
|
req.log.info('got product', product);
|
|
if(!product) {
|
|
res.status(404).send('product not found');
|
|
return;
|
|
}
|
|
// is the product in stock?
|
|
if(product.instock == 0) {
|
|
res.status(404).send('out of stock');
|
|
return;
|
|
}
|
|
// does the cart already exist?
|
|
redisClient.get(req.params.id, (err, data) => {
|
|
if(err) {
|
|
req.log.error('ERROR', err);
|
|
res.status(500).send(err);
|
|
} else {
|
|
var cart;
|
|
if(data == null) {
|
|
// create new cart
|
|
cart = {
|
|
total: 0,
|
|
tax: 0,
|
|
items: []
|
|
};
|
|
} else {
|
|
cart = JSON.parse(data);
|
|
}
|
|
req.log.info('got cart', cart);
|
|
// add sku to cart
|
|
var item = {
|
|
qty: qty,
|
|
sku: req.params.sku,
|
|
name: product.name,
|
|
price: product.price,
|
|
subtotal: qty * product.price
|
|
};
|
|
var list = mergeList(cart.items, item, qty);
|
|
cart.items = list;
|
|
cart.total = calcTotal(cart.items);
|
|
// work out tax
|
|
cart.tax = calcTax(cart.total);
|
|
|
|
// save the new cart
|
|
saveCart(req.params.id, cart).then((data) => {
|
|
counter.inc(qty);
|
|
res.json(cart);
|
|
}).catch((err) => {
|
|
req.log.error(err);
|
|
res.status(500).send(err);
|
|
});
|
|
}
|
|
});
|
|
}).catch((err) => {
|
|
req.log.error(err);
|
|
res.status(500).send(err);
|
|
});
|
|
});
|
|
|
|
// update quantity - remove item when qty == 0
|
|
app.get('/update/:id/:sku/:qty', (req, res) => {
|
|
// check quantity
|
|
var qty = parseInt(req.params.qty);
|
|
if(isNaN(qty)) {
|
|
req.log.warn('quanity not a number');
|
|
res.status(400).send('quantity must be a number');
|
|
return;
|
|
} else if(qty < 0) {
|
|
req.log.warn('quantity less than zero');
|
|
res.status(400).send('negative quantity not allowed');
|
|
return;
|
|
}
|
|
|
|
// get the cart
|
|
redisClient.get(req.params.id, (err, data) => {
|
|
if(err) {
|
|
req.log.error('ERROR', err);
|
|
res.status(500).send(err);
|
|
} else {
|
|
if(data == null) {
|
|
res.status(404).send('cart not found');
|
|
} else {
|
|
var cart = JSON.parse(data);
|
|
var idx;
|
|
var len = cart.items.length;
|
|
for(idx = 0; idx < len; idx++) {
|
|
if(cart.items[idx].sku == req.params.sku) {
|
|
break;
|
|
}
|
|
}
|
|
if(idx == len) {
|
|
// not in list
|
|
res.status(404).send('not in cart');
|
|
} else {
|
|
if(qty == 0) {
|
|
cart.items.splice(idx, 1);
|
|
} else {
|
|
cart.items[idx].qty = qty;
|
|
cart.items[idx].subtotal = cart.items[idx].price * qty;
|
|
}
|
|
cart.total = calcTotal(cart.items);
|
|
// work out tax
|
|
cart.tax = calcTax(cart.total);
|
|
saveCart(req.params.id, cart).then((data) => {
|
|
res.json(cart);
|
|
}).catch((err) => {
|
|
req.log.error(err);
|
|
res.status(500).send(err);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// add shipping
|
|
app.post('/shipping/:id', (req, res) => {
|
|
var shipping = req.body;
|
|
if(shipping.distance === undefined || shipping.cost === undefined || shipping.location == undefined) {
|
|
req.log.warn('shipping data missing', shipping);
|
|
res.status(400).send('shipping data missing');
|
|
} else {
|
|
// get the cart
|
|
redisClient.get(req.params.id, (err, data) => {
|
|
if(err) {
|
|
req.log.error('ERROR', err);
|
|
res.status(500).send(err);
|
|
} else {
|
|
if(data == null) {
|
|
req.log.info('no cart for', req.params.id);
|
|
res.status(404).send('cart not found');
|
|
} else {
|
|
var cart = JSON.parse(data);
|
|
var item = {
|
|
qty: 1,
|
|
sku: 'SHIP',
|
|
name: 'shipping to ' + shipping.location,
|
|
price: shipping.cost,
|
|
subtotal: shipping.cost
|
|
};
|
|
// check shipping already in the cart
|
|
var idx;
|
|
var len = cart.items.length;
|
|
for(idx = 0; idx < len; idx++) {
|
|
if(cart.items[idx].sku == item.sku) {
|
|
break;
|
|
}
|
|
}
|
|
if(idx == len) {
|
|
// not already in cart
|
|
cart.items.push(item);
|
|
} else {
|
|
cart.items[idx] = item;
|
|
}
|
|
cart.total = calcTotal(cart.items);
|
|
// work out tax
|
|
cart.tax = calcTax(cart.total);
|
|
|
|
// save the updated cart
|
|
saveCart(req.params.id, cart).then((data) => {
|
|
res.json(cart);
|
|
}).catch((err) => {
|
|
req.log.error(err);
|
|
res.status(500).send(err);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
function mergeList(list, product, qty) {
|
|
var inlist = false;
|
|
// loop through looking for sku
|
|
var idx;
|
|
var len = list.length;
|
|
for(idx = 0; idx < len; idx++) {
|
|
if(list[idx].sku == product.sku) {
|
|
inlist = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(inlist) {
|
|
list[idx].qty += qty;
|
|
list[idx].subtotal = list[idx].price * list[idx].qty;
|
|
} else {
|
|
list.push(product);
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
function calcTotal(list) {
|
|
var total = 0;
|
|
for(var idx = 0, len = list.length; idx < len; idx++) {
|
|
total += list[idx].subtotal;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
function calcTax(total) {
|
|
// tax @ 20%
|
|
return (total - (total / 1.2));
|
|
}
|
|
|
|
function getProduct(sku) {
|
|
return new Promise((resolve, reject) => {
|
|
request('http://' + catalogueHost + ':8080/product/' + sku, (err, res, body) => {
|
|
if(err) {
|
|
reject(err);
|
|
} else if(res.statusCode != 200) {
|
|
resolve(null);
|
|
} else {
|
|
// return object - body is a string
|
|
// TODO - catch parse error
|
|
resolve(JSON.parse(body));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function saveCart(id, cart) {
|
|
logger.info('saving cart', cart);
|
|
return new Promise((resolve, reject) => {
|
|
redisClient.setex(id, 3600, JSON.stringify(cart), (err, data) => {
|
|
if(err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(data);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// connect to Redis
|
|
var redisClient = redis.createClient({
|
|
host: redisHost
|
|
});
|
|
|
|
redisClient.on('error', (e) => {
|
|
logger.error('Redis ERROR', e);
|
|
});
|
|
redisClient.on('ready', (r) => {
|
|
logger.info('Redis READY', r);
|
|
redisConnected = true;
|
|
});
|
|
|
|
// fire it up!
|
|
const port = process.env.CART_SERVER_PORT || '8080';
|
|
app.listen(port, () => {
|
|
logger.info('Started on port', port);
|
|
});
|
|
|