Bungysheep
Bungysheep

Reputation: 40

Refactor promise javascript code

Basically, I have a code to approve an order code by applying Coupon entered by user into the Order transaction:

var Order = require('../models/order');
var Product = require('../models/product');
var Coupon = require('../models/coupon');

var _ = require('lodash');

exports.approveOrder = function(req, res) {
    var tempProducts;    
    var tempCoupon;
    var orderNbr = req.params.OrderNbr;
    if (orderNbr != undefined && orderNbr != '')
    {
        Order.findOne({ OrderNbr: orderNbr }).exec()
        .then(function(order) {
            if (order == undefined) {
                throw "Order " + orderNbr + " does not exist.";
            }
            else {
                if (order.OrderLines.length <= 0) throw "Ensure your Order has at least one order line.";

                if (req.body.CouponCode != undefined && req.body.CouponCode != '') {
                    // ***********************************************
                    // Want to refactor this following codes into some functions
                    // ***********************************************                  
                    Coupon.findOne({ CouponCode: req.body.CouponCode }).exec()
                    .then(function(coupon) {                        
                        if (coupon == undefined) {
                            throw "Coupon " + req.body.CouponCode + " does not exist.";
                        }
                        else {
                            if (coupon.Qty > 0 && (coupon.ValidFrom <= new Date() && coupon.ValidTo >= new Date())) {
                                coupon.Qty -= 1;
                                tempCoupon = coupon;

                                var prodNbrs = _.map(order.OrderLines, 'ProdNbr');
                                return Product.find({ ProdNbr: { $in : prodNbrs } }).exec();
                            }
                            else {
                                throw "Coupon " + coupon.CouponCode + " is not valid.";
                            }
                        }
                    })
                    .then(function(products) {
                        var prodNbrs = _.map(order.OrderLines, 'ProdNbr');                                                
                        var totalQtyPerProd = 0;

                        _.forEach(products, function(product) {
                            totalQtyPerProd = _.sumBy(order.OrderLines, function(line) {
                                if (product.ProdNbr == line.ProdNbr) return line.Qty;
                            })
                            if (product.QtyOnHand - totalQtyPerProd < 0) throw "Product " + product.ProdNbr + " has insufficient quantity on hand.";

                            _.remove(prodNbrs, function(nbr) { return nbr == product.ProdNbr });
                            product.QtyOnHand -= totalQtyPerProd;

                            var totalDiscount = tempCoupon.Value / order.OrderLines.length;
                            if (tempCoupon.IsPercentage) {
                                totalDiscount = 1 - (tempCoupon.Value / 100);
                            }

                            _.forEach(order.OrderLines, function(line) {
                                if (line.ProdNbr == product.ProdNbr) {
                                    line.UnitPrice = product.UnitPrice;

                                    line.Amount = (line.Qty * line.UnitPrice) - totalDiscount;
                                    if (line.Amount < 0) line.Amount = 0;
                                    if (tempCoupon.IsPercentage) {
                                        line.Amount = (line.Qty * line.UnitPrice) * totalDiscount;
                                    }
                                }
                            })
                        })

                        if (prodNbrs.length > 0) throw "Product " + prodNbrs[0] + " does not exist.";

                        tempProducts = products;

                        order.CouponCode = tempCoupon.CouponCode;
                        order.Status = 'S';
                        return order.save();
                    })
                    .then(function() {
                        return tempCoupon.save();
                    })
                    .then(function() {
                        _.forEach(tempProducts, function(product) {
                            product.save()
                            .then(function() {

                            })
                            .catch(function(err) {
                                if (err) res.status('500').jsonp({ error: err });
                            });
                        })
                        res.status('200').jsonp({ information: "Order "+ order.OrderNbr +" has been submitted successfully." });
                    })
                    .catch(function(err) {
                        if (err) res.status('500').jsonp({ error: err });
                    });
                }
            }            
        }) 
        .catch(function(err) {
            if (err) res.status('500').jsonp({ error: err });
        });
    }
    else {
        res.status('500').jsonp({ error: "Order Number must be specified." });
    }
};

I want to split code each 'then' clause into some functions:

  1. find Coupon and then return a Coupon.
  2. update the order lines amount
  3. update the coupon
  4. return confirmation message

I tried using local variable to keep found Coupon, unfortunately the variable will be undefined if it is out of findOne() method, so in this code I used so many and long '.then' in findOne()

Any idea?

Upvotes: 0

Views: 502

Answers (2)

gzp___
gzp___

Reputation: 388

Your approach to use the "then chain" is fine. I would refactor the code creating separated functions to use in the chain like this:

var saveProducts = function() {
                        _.forEach(tempProducts, function(product) {
                            product.save()
                            .then(function() {

                            })
                            .catch(function(err) {
                                if (err) res.status('500').jsonp({ error: err });
                            });
                        })
                        res.status('200').jsonp({ information: "Order "+ order.OrderNbr +" has been submitted successfully." });
                    }

var saveCoupon = function() {
                        return tempCoupon.save();
                    }

var products = function(products) {
                        var prodNbrs = _.map(order.OrderLines, 'ProdNbr');                                                
                        var totalQtyPerProd = 0;

                        _.forEach(products, function(product) {
                            totalQtyPerProd = _.sumBy(order.OrderLines, function(line) {
                                if (product.ProdNbr == line.ProdNbr) return line.Qty;
                            })
                            if (product.QtyOnHand - totalQtyPerProd < 0) throw "Product " + product.ProdNbr + " has insufficient quantity on hand.";

                            _.remove(prodNbrs, function(nbr) { return nbr == product.ProdNbr });
                            product.QtyOnHand -= totalQtyPerProd;

                            var totalDiscount = tempCoupon.Value / order.OrderLines.length;
                            if (tempCoupon.IsPercentage) {
                                totalDiscount = 1 - (tempCoupon.Value / 100);
                            }

                            _.forEach(order.OrderLines, function(line) {
                                if (line.ProdNbr == product.ProdNbr) {
                                    line.UnitPrice = product.UnitPrice;

                                    line.Amount = (line.Qty * line.UnitPrice) - totalDiscount;
                                    if (line.Amount < 0) line.Amount = 0;
                                    if (tempCoupon.IsPercentage) {
                                        line.Amount = (line.Qty * line.UnitPrice) * totalDiscount;
                                    }
                                }
                            })
                        })

                        if (prodNbrs.length > 0) throw "Product " + prodNbrs[0] + " does not exist.";

                        tempProducts = products;

                        order.CouponCode = tempCoupon.CouponCode;
                        order.Status = 'S';
                        return order.save();
                    }

var getProducts = function(coupon) {                        
                        if (coupon == undefined) {
                            throw "Coupon " + req.body.CouponCode + " does not exist.";
                        }
                        else {
                            if (coupon.Qty > 0 && (coupon.ValidFrom <= new Date() && coupon.ValidTo >= new Date())) {
                                coupon.Qty -= 1;
                                tempCoupon = coupon;

                                var prodNbrs = _.map(order.OrderLines, 'ProdNbr');
                                return Product.find({ ProdNbr: { $in : prodNbrs } }).exec();
                            }
                            else {
                                throw "Coupon " + coupon.CouponCode + " is not valid.";
                            }
                        }
                    }

exports.approveOrder = function(req, res) {
    var tempProducts;    
    var tempCoupon;
    var orderNbr = req.params.OrderNbr;
    if (orderNbr != undefined && orderNbr != '')
    {
        Order.findOne({ OrderNbr: orderNbr }).exec()
        .then(function(order) {
            if (order == undefined) {
                throw "Order " + orderNbr + " does not exist.";
            }
            else {
                if (order.OrderLines.length <= 0) throw "Ensure your Order has at least one order line.";

                if (req.body.CouponCode != undefined && req.body.CouponCode != '') {
                    // ***********************************************
                    // Want to refactor this following codes into some functions
                    // ***********************************************                  
                    Coupon.findOne({ CouponCode: req.body.CouponCode }).exec()
                    .then(getProducts)
                    .then(products)
                    .then(saveCoupon)
                    .then(saveProducts)
                    .catch(function(err) {
                        if (err) res.status('500').jsonp({ error: err });
                    });
                }
            }            
        }) 
        .catch(function(err) {
            if (err) res.status('500').jsonp({ error: err });
        });
    }
    else {
        res.status('500').jsonp({ error: "Order Number must be specified." });
    }
};

the result of each functions will be passed to the next in the chain

I suggest this thread on the best practices for promises

https://github.com/airbnb/javascript/issues/216

Upvotes: 1

Reza Karami
Reza Karami

Reputation: 515

I recommend you make use of the async library with the waterfall method. Essentially it will allow you to have a more flat structure without the nesting by using callbacks.

Something like this:

var async = require('async');

var findCoupons = function(cb) {
    //do something
    cb(null, 'success');
}

var updateOrders = function(data, cb) {
    //do something
    console.log(data); //success
    cb(null, 'etc.');
}

...

async.waterfall([
    findCoupons,
    updateOrders,
    updateCoupon,
    confirmMessage
], function(err, result) {
    //all functions have finished running, do something.
});

Upvotes: 1

Related Questions