trailpack-proxy-cart
Version:
eCommerce - Trailpack for Proxy Engine
1,404 lines (1,308 loc) • 65.9 kB
JavaScript
/* eslint new-cap: [0] */
/* eslint no-console: [0] */
'use strict'
const Model = require('trails/model')
const helpers = require('proxy-engine-helpers')
const Errors = require('proxy-engine-errors')
const _ = require('lodash')
const CART_STATUS = require('../../lib').Enums.CART_STATUS
const DISCOUNT_STATUS = require('../../lib').Enums.DISCOUNT_STATUS
const PAYMENT_PROCESSING_METHOD = require('../../lib').Enums.PAYMENT_PROCESSING_METHOD
const queryDefaults = require('../utils/queryDefaults')
/**
* @module Cart
* @description Cart Model
*/
module.exports = class Cart extends Model {
static config (app, Sequelize) {
return {
options: {
enums: {
CART_STATUS: CART_STATUS
},
underscored: true,
// defaultScope: {
// where: {
// live_mode: app.config.proxyEngine.live_mode
// }
// },
scopes: {
live: {
where: {
live_mode: true
}
}
},
indexes: [
// Creates a gin index on data with the jsonb_path_ops operator
{
fields: ['line_items'],
using: 'gin',
operator: 'jsonb_path_ops'
},
{
fields: ['client_details'],
using: 'gin',
operator: 'jsonb_path_ops'
}
],
hooks: {
beforeCreate: (values, options) => {
return app.services.CartService.beforeCreate(values, options)
.catch(err => {
return Promise.reject(err)
})
},
beforeUpdate: (values, options) => {
return app.services.CartService.beforeUpdate(values, options)
.catch(err => {
return Promise.reject(err)
})
},
beforeSave: (values, options) => {
return app.services.CartService.beforeSave(values, options)
.catch(err => {
return Promise.reject(err)
})
}
},
classMethods: {
/**
* Associate the Model
* @param models
*/
associate: (models) => {
models.Cart.belongsTo(models.Customer, {
foreignKey: 'customer_id'
// as: 'customer_id',
})
models.Cart.belongsTo(models.Shop, {
foreignKey: 'shop_id'
// as: 'shop_id',
})
models.Cart.belongsTo(models.Order, {
foreignKey: 'order_id'
// as: 'shop_id',
})
models.Cart.belongsToMany(models.User, {
as: 'owners',
through: {
model: models.UserItem,
scope: {
item: 'cart'
}
},
foreign_id: 'item_id',
constraints: false
})
models.Cart.belongsTo(models.Address, {
as: 'shipping_address',
foreignKey: 'shipping_address_id'
})
models.Cart.belongsTo(models.Address, {
as: 'billing_address',
foreignKey: 'billing_address_id'
})
models.Cart.belongsToMany(models.Address, {
as: 'addresses',
// otherKey: 'address_id',
foreignKey: 'model_id',
through: {
model: models.ItemAddress,
scope: {
model: 'cart'
},
constraints: false
},
constraints: false
})
models.Cart.belongsToMany(models.Discount, {
as: 'discounts',
through: {
model: models.ItemDiscount,
unique: false,
scope: {
model: 'cart'
}
},
foreignKey: 'model_id',
constraints: false
})
// models.Cart.belongsTo(models.Customer, {
// foreignKey: 'default_cart'
// // as: 'customer_id'
// })
// models.Cart.hasMany(models.Product, {
// as: 'products'
// // constraints: false
// })
// models.Cart.hasMany(models.ProductVariant, {
// as: 'variants'
// // constraints: false
// })
// models.Cart.hasMany(models.Coupon, {
// as: 'coupons'
// // constraints: false
// })
// models.Cart.hasMany(models.GiftCard, {
// as: 'gift_cards'
// // constraints: false
// })
},
/**
*
* @param criteria
* @param options
* @returns {*|Promise.<Instance>}
*/
findByIdDefault: function(criteria, options) {
options = app.services.ProxyEngineService.mergeOptionDefaults(
queryDefaults.Cart.default(app),
options || {}
)
return this.findById(criteria, options)
},
/**
*
* @param options
* @returns {*|Promise.<Instance>}
*/
findOneDefault: function(options) {
options = app.services.ProxyEngineService.mergeOptionDefaults(
queryDefaults.Cart.default(app),
options || {}
)
return this.findOne(options)
},
/**
*
* @param token
* @param options
* @returns {*|Promise.<Instance>}
*/
findByTokenDefault: function(token, options) {
options = app.services.ProxyEngineService.mergeOptionDefaults(
queryDefaults.Cart.default(app),
options || {},
{
where: {
token: token
}
}
)
return this.findOne(options)
},
/**
*
* @param cart
* @param options
* @returns {*}
*/
resolve: function(cart, options){
options = options || {}
const Cart = this
if (cart instanceof Cart){
return Promise.resolve(cart)
}
else if (cart && _.isObject(cart) && cart.id) {
return Cart.findByIdDefault(cart.id, options)
.then(resCart => {
if (!resCart) {
throw new Errors.FoundError(Error(`Cart ${cart.id} not found`))
}
return resCart
})
}
else if (cart && _.isObject(cart) && cart.token) {
return Cart.findByTokenDefault(cart.token, options)
.then(resCart => {
if (!resCart) {
throw new Errors.FoundError(Error(`Cart ${cart.token} not found`))
}
return resCart
})
}
else if (cart && _.isObject(cart)) {
return this.create(cart, options)
}
else if (cart && (_.isNumber(cart))) {
return Cart.findByIdDefault(cart, options)
.then(resCart => {
if (!resCart) {
throw new Errors.FoundError(Error(`Cart ${cart} not found`))
}
return resCart
})
}
else if (cart && (_.isString(cart))) {
return Cart.findByTokenDefault(cart, options)
.then(resCart => {
if (!resCart) {
throw new Errors.FoundError(Error(`Cart ${cart} not found`))
}
return resCart
})
}
else {
// TODO create proper error
const err = new Error(`Unable to resolve Cart ${cart}`)
return Promise.reject(err)
}
}
},
instanceMethods: {
/**
* Resets the defaults so they can be recalculated
* @returns {*}
*/
resetDefaults: function() {
this.total_items = 0
this.total_shipping = 0
this.subtotal_price = 0
this.total_discounts = 0
this.total_coupons = 0
this.total_tax = 0
this.total_weight = 0
this.total_line_items_price = 0
this.total_overrides = 0
this.total_price = 0
this.total_due = 0
this.has_subscription = false
this.has_shipping = false
this.has_taxes = false
this.discounted_lines = []
this.coupon_lines = []
// this.shipping_lines = []
// this.tax_lines = []
// Filter any non manual tax lines
this.tax_lines = this.tax_lines.filter(line =>
Object.keys(line).indexOf('line') === -1
)
// Filter any non manual shipping lines
this.shipping_lines = this.shipping_lines.filter(line =>
Object.keys(line).indexOf('line') === -1
)
// Reset line items
this.line_items.map(item => {
item.shipping_lines = []
item.discounted_lines = []
item.coupon_lines = []
item.tax_lines = []
item.total_discounts = 0
item.calculated_price = item.price
return item
})
return this
},
/**
*
* @param lines
*/
setLineItems: function(lines) {
this.line_items = lines || []
this.total_items = 0
this.subtotal_price = 0
this.total_line_items_price = 0
this.has_shipping = this.line_items.some(item => item.requires_shipping)
this.has_taxes = this.line_items.some(item => item.requires_taxes)
this.line_items.forEach(item => {
// Check if at least one time requires shipping
if (item.requires_shipping) {
this.total_weight = this.total_weight + item.grams
}
// Check if at least one item requires subscription
if (item.requires_subscription) {
this.has_subscription = true
}
this.total_items = this.total_items + item.quantity
this.subtotal_price = this.subtotal_price + item.price
this.total_line_items_price = this.total_line_items_price + item.price
})
return this.setTotals()
},
/**
*
* @param item
* @param discount
* @param criteria
* @returns {*}
*/
setItemDiscountedLines: function(item, discount, criteria) {
if (!(discount instanceof app.orm['Discount'])) {
throw new Error('setItemDiscountedLines expects discount parameter to be a Discount Instance')
}
item = discount.discountItem(item, criteria)
return item
},
/**
*
* @param discounts
* @param criteria
* @returns {*}
*/
setItemsDiscountedLines: function (discounts, criteria) {
// Make this an array if null
discounts = discounts || []
// Make this an array if null
criteria = criteria || []
// Make this an array if null
this.line_items = this.line_items || []
// Set this to the default
this.discounted_lines = []
// Holds the final factored results
const factoredDiscountedLines = []
// Holds list of all discount objects being tried
let discountsArr = []
// Holds list lines and their discounts
let discountedLines = []
// For each item run the normal discounts
this.line_items = this.line_items.map((item, index) => {
discounts.forEach(discount => {
item = this.setItemDiscountedLines(item, discount, criteria)
})
if (item.discounted_lines.length > 0) {
const i = discountedLines.findIndex(line => line.line === index)
if (i > -1) {
discountedLines[i].discounts = [...discountedLines[i].discounts, ...item.discounted_lines]
}
else {
discountedLines.push({
line: index,
discounts: item.discounted_lines
})
}
}
return item
})
// Gather all discounts into a single array
discountedLines.forEach(line => {
discountsArr = [...discountsArr, ...line.discounts.map(d => d.id)]
})
// Check rules
discountedLines = discountedLines.map(line => {
line.discounts = line.discounts.map(discount => {
// Applies once Rule
if (discount.rules.applies_once && discountsArr.filter(d => d === discount.id).length > 1) {
const arrRemove = discountsArr.findIndex(d => d === discount.id)
// Removes duplicated from discountArr
discountsArr = discountsArr.splice(arrRemove, 1)
// This means the next occurrence of the discount will receive the one time discount
discount.applies = false
}
// Minimum Order Rule
else if (
discount.rules.minimum_order_amount > 0
&& this.total_line_items_price < discount.minimum_order_amount
) {
discount.applies = false
}
// Compounding Discounts Rule
else if (
discount.rules.applies_compound === false && discountsArr.length > 1
) {
discount.applies = false
}
else {
discount.applies = true
}
return discount
})
return line
})
// console.log('Lines results', discountedLines)
// Apply rules to line item discounts
discountedLines.forEach(line => {
line.discounts.forEach(discount => {
const index = this.line_items[line.line].discounted_lines.findIndex(d => d.id === discount.id)
this.line_items[line.line].discounted_lines[index].applies = discount.applies
})
})
// Loop through items and apply discounts and factor cart discounted_lines
this.line_items = this.line_items.map((item, index) => {
item.discounted_lines.forEach(discountedLine => {
if (discountedLine.applies === true) {
// New Calculated Price
const calculatedPrice = Math.max(0, item.calculated_price - discountedLine.price)
// Total Deducted
const totalDeducted = Math.min(item.calculated_price, (item.calculated_price - (item.calculated_price - discountedLine.price)))
// Set item calculated price
item.calculated_price = calculatedPrice
// Set item total_discounts
item.total_discounts = Math.min(item.price, item.total_discounts + totalDeducted)
const fI = factoredDiscountedLines.findIndex(d => d.id === discountedLine.id)
if (fI > -1) {
factoredDiscountedLines[fI].lines = [...factoredDiscountedLines[fI].lines, index]
factoredDiscountedLines[fI].price = factoredDiscountedLines[fI].price + totalDeducted
}
else {
discountedLine.lines = [index]
discountedLine.price = totalDeducted
factoredDiscountedLines.push(discountedLine)
}
}
})
return item
})
return this.setDiscountedLines(factoredDiscountedLines)
},
/**
*
* @param lines
*/
setDiscountedLines: function(lines) {
this.total_discounts = 0
this.discounted_lines = lines || []
this.discounted_lines.forEach(line => {
this.total_discounts = this.total_discounts + line.price
})
return this.setTotals()
},
/**
*
* @param lines
*/
setPricingOverrides: function(lines) {
this.total_overrides = 0
this.pricing_overrides = lines || []
this.pricing_overrides.forEach(line => {
this.total_overrides = this.total_overrides + line.price
})
return this.setTotals()
},
/**
*
* @param lines
*/
setCouponLines: function(lines) {
this.total_coupons = 0
this.coupon_lines = lines || []
this.coupon_lines.forEach(line => {
this.total_coupons = this.total_coupons + line.price
})
return this.setTotals()
},
setItemsShippingLines: function (items) {
let shippingLines = []
let totalShipping = 0
// Make this an array if null
this.line_items = this.line_items || []
this.line_items = this.line_items.map((item, i) => {
const shippedLine = items.find(i => i.sku === item.sku)
if (shippedLine) {
shippedLine.shipping_lines = shippedLine.shipping_lines || []
shippedLine.shipping_lines.map(line => {
line.line = i
return line
})
totalShipping = shippedLine.shipping_lines.forEach(line => {
totalShipping = totalShipping + line.price
})
// console.log('SHIPPED LINE', shippedLine)
shippingLines = [...shippingLines, ...shippedLine.shipping_lines]
item.shipping_lines = shippedLine.shipping_lines
item.total_shipping = totalShipping
}
return item
})
return this.setShippingLines(shippingLines)
},
/**
*
* @param lines
*/
setShippingLines: function(lines) {
lines = lines || []
this.total_shipping = 0
this.shipping_lines = [...this.shipping_lines, ...lines],
this.shipping_lines.forEach(line => {
this.total_shipping = this.total_shipping + line.price
})
return this.setTotals()
},
setItemsTaxLines: function (items) {
let taxesLines = []
let totalTaxes = 0
// Make this an array if null
this.line_items = this.line_items || []
this.line_items = this.line_items.map((item, i) => {
const taxedLine = items.find(i => i.sku === item.sku)
if (taxedLine) {
taxedLine.tax_lines = taxedLine.tax_lines || []
taxedLine.tax_lines.map(line => {
line.line = i
return line
})
totalTaxes = taxedLine.tax_lines.forEach(line => {
totalTaxes = totalTaxes + line.price
})
// console.log('TAXED LINE', taxedLine)
taxesLines = [...taxesLines, ...taxedLine.tax_lines]
item.tax_lines = taxedLine.tax_lines
item.total_taxes = totalTaxes
}
return item
})
return this.setTaxLines(taxesLines)
},
/**
*
* @param lines
*/
setTaxLines: function(lines) {
lines = lines || []
this.total_tax = 0
this.tax_lines = [...this.tax_lines, ...lines]
this.tax_lines.forEach(line => {
this.total_tax = this.total_tax + line.price
})
return this.setTotals()
},
/**
*
*/
setTotals: function() {
// Set Cart values
this.total_price = Math.max(0,
this.total_tax
+ this.total_shipping
+ this.subtotal_price
)
this.total_due = Math.max(0,
this.total_price
- this.total_discounts
- this.total_coupons
- this.total_overrides
)
return this
},
setLineProperties: (line) => {
if (line.properties) {
for (const l in line.properties){
if (line.properties.hasOwnProperty(l)) {
line.price = line.price + line.properties[l].price
line.price_per_unit = line.price_per_unit + line.properties[l].price
}
}
}
return line
},
/**
*
* @param data
*/
// TODO Select Vendor
line: function(data){
// handle empty product
data.Product = data.Product || {}
data.property_pricing = data.property_pricing || data.Product.property_pricing
data.properties = data.properties || []
const properties = {}
if (
data.properties.length > 0
&& data.property_pricing
) {
data.properties.forEach(prop => {
if (!prop.name) {
return
}
if (data.property_pricing[prop.name]) {
properties[prop.name] = data.property_pricing[prop.name]
if (prop.value) {
properties[prop.name]['value'] = prop.value
}
}
})
}
const line = {
product_id: data.product_id,
product_handle: data.Product.handle,
variant_id: data.id || data.variant_id,
type: data.type,
sku: data.sku,
title: data.Product.title,
variant_title: data.title,
name: data.title === data.Product.title ? data.title : `${data.Product.title} - ${data.title}`,
properties: properties,
property_pricing: data.property_pricing,
option: data.option,
barcode: data.barcode,
price: data.price * data.quantity,
calculated_price: data.price * data.quantity,
compare_at_price: data.compare_at_price,
price_per_unit: data.price,
currency: data.currency,
fulfillment_service: data.fulfillment_service,
gift_card: data.gift_card,
requires_shipping: data.requires_shipping,
requires_taxes: data.requires_taxes,
tax_code: data.tax_code,
tax_lines: [],
total_taxes: 0,
shipping_lines: [],
total_shipping: 0,
discounted_lines: [],
total_discounts: 0,
requires_subscription: data.requires_subscription,
subscription_interval: data.subscription_interval,
subscription_unit: data.subscription_unit,
weight: data.weight * data.quantity,
weight_unit: data.weight_unit,
images: data.images.length > 0 ? data.images : data.Product.images,
quantity: data.quantity,
fulfillable_quantity: data.fulfillable_quantity,
max_quantity: data.max_quantity,
grams: app.services.ProxyCartService.resolveConversion(data.weight, data.weight_unit) * data.quantity,
vendors: data.Product.vendors,
vendor_id: data.vendor_id || null,
average_shipping: data.Product.average_shipping,
exclude_payment_types: data.Product.exclude_payment_types,
fulfillment_extras: data.fufillment_extras,
live_mode: data.live_mode
}
return line
},
/**
*
* @param item
* @param qty
* @param properties
* @param options
* @returns {Promise.<TResult>}
*/
addLine: function(item, qty, properties, options) {
options = options || {}
// The quantity available of this variant
let lineQtyAvailable = -1
let line
// Check if Product is Available
return item.checkAvailability(qty, {transaction: options.transaction || null})
.then(availability => {
if (!availability.allowed) {
throw new Error(`${availability.title} is not available in this quantity, please try a lower quantity`)
}
lineQtyAvailable = availability.quantity
// Check if Product is Restricted
return item.checkRestrictions(
this.Customer || this.customer_id,
{transaction: options.transaction || null}
)
})
.then(restricted => {
if (restricted) {
throw new Error(`${restricted.title} can not be delivered to ${restricted.city} ${restricted.province} ${restricted.country}`)
}
// Rename line items so they are no longer immutable
const lineItems = this.line_items
// Make quantity an integer
if (!qty || !_.isNumber(qty)) {
qty = 1
}
const itemIndex = _.findIndex(lineItems, {variant_id: item.id})
// If already in cart
if (itemIndex > -1) {
app.log.silly('Cart.addLine NEW QTY', lineItems[itemIndex])
const maxQuantity = lineItems[itemIndex].max_quantity || -1
let calculatedQty = lineItems[itemIndex].quantity + qty
if (maxQuantity > -1 && calculatedQty > maxQuantity) {
calculatedQty = maxQuantity
}
if (lineQtyAvailable > -1 && calculatedQty > lineQtyAvailable) {
calculatedQty = Math.max(0, lineQtyAvailable - calculatedQty)
}
lineItems[itemIndex].quantity = calculatedQty
lineItems[itemIndex].fulfillable_quantity = calculatedQty
lineItems[itemIndex] = this.setLineProperties(lineItems[itemIndex])
this.line_items = lineItems
}
// If new item
else {
const maxQuantity = item.max_quantity || -1
let calculatedQty = qty
if (maxQuantity > -1 && calculatedQty > maxQuantity) {
calculatedQty = maxQuantity
}
if (lineQtyAvailable > -1 && calculatedQty > lineQtyAvailable) {
calculatedQty = Math.max(0, lineQtyAvailable - calculatedQty)
}
// Item Quantity in cart
item.quantity = calculatedQty
// The max that will be fulfilled
item.fulfillable_quantity = calculatedQty
// The max allowed to be purchased
item.max_quantity = maxQuantity
// The properties of the item
item.properties = properties
// Set line
line = this.line(item)
line = this.setLineProperties(line)
app.log.silly('Cart.addLine NEW LINE', line)
// Add line to line items
lineItems.push(line)
// Assign line items
this.line_items = lineItems
}
return this
})
},
/**
*
* @param item
* @param qty
* @returns {Promise.<this>}
*/
removeLine: function(item, qty, options) {
options = options || {}
const lineItems = this.line_items
if (!qty || !_.isNumber(qty)) {
qty = 1
}
const itemIndex = _.findIndex(lineItems, {variant_id: item.id})
if (itemIndex > -1) {
lineItems[itemIndex].quantity = lineItems[itemIndex].quantity - qty
lineItems[itemIndex].fulfillable_quantity = Math.max(0, lineItems[itemIndex].fulfillable_quantity - qty)
// Resolve Grams
if ( lineItems[itemIndex].quantity < 1) {
app.log.silly(`Cart.removeLine removing '${lineItems[itemIndex].variant_id}' line completely`)
lineItems.splice(itemIndex, 1)
}
this.line_items = lineItems
return Promise.resolve(this)
}
},
/**
*
* @param shipping
* @param options
* @returns {Promise.<T>}
*/
addShipping: function(shipping, options) {
shipping = shipping || []
options = options || {}
const shippingLines = this.shipping_lines
if (_.isArray(shipping)) {
shipping.forEach(ship => {
const i = _.findIndex(shippingLines, (s) => {
return s.name === ship.name
})
// Make sure shipping price is a number
ship.price = app.services.ProxyCartService.normalizeCurrency(parseInt(ship.price))
if (i > -1) {
shippingLines[i] = ship
}
else {
shippingLines.push(ship)
}
})
}
else if (_.isObject(shipping)){
const i = _.findIndex(shippingLines, (s) => {
return s.name === shipping.name
})
// Make sure shipping price is a number
shipping.price = app.services.ProxyCartService.normalizeCurrency(parseInt(shipping.price))
if (i > -1) {
shippingLines[i] = shipping
}
else {
shippingLines.push(shipping)
}
}
this.shipping_lines = shippingLines
// this.setShippingLines(shippingLines)
return this.save({transaction: options.transaction || null})
},
/**
*
* @param shipping
* @param options
* @returns {Promise.<T>}
*/
removeShipping: function(shipping, options){
shipping = shipping || []
options = options || {}
const shippingLines = this.shipping_lines
if (_.isArray(shipping)) {
shipping.forEach(ship => {
const i = _.findIndex(shippingLines, (s) => {
return s.name === ship.name
})
if (i > -1) {
shippingLines.splice(i, 1)
}
})
}
else if (_.isObject(shipping)) {
const i = _.findIndex(shippingLines, (s) => {
return s.name === shipping.name
})
if (i > -1) {
shippingLines.splice(i, 1)
}
}
this.shipping_lines = shippingLines
// this.setShippingLines(shippingLines)
return this.save({transaction: options.transaction || null})
},
/**
*
* @param taxes
* @param options
* @returns {Promise.<T>}
*/
addTaxes: function(taxes, options) {
taxes = taxes || []
options = options || {}
const taxLines = this.tax_lines
if (_.isArray(taxes)) {
taxes.forEach(tax => {
const i = _.findIndex(taxLines, (s) => {
return s.name === tax.name
})
// Make sure taxes price is a number
tax.price = app.services.ProxyCartService.normalizeCurrency(parseInt(tax.price))
if (i > -1) {
taxLines[i] = tax
}
else {
taxLines.push(tax)
}
})
}
else if (_.isObject(taxes)) {
const i = _.findIndex(taxLines, (s) => {
return s.name === taxes.name
})
// Make sure taxes price is a number
taxes.price = app.services.ProxyCartService.normalizeCurrency(parseInt(taxes.price))
if (i > -1) {
taxLines[i] = taxes
}
else {
taxLines.push(taxes)
}
}
this.tax_lines = taxLines
return this.save({transaction: options.transaction || null})
},
/**
*
* @param taxes
* @param options
* @returns {Promise.<T>}
*/
removeTaxes: function(taxes, options){
taxes = taxes || []
options = options || {}
const taxLines = this.tax_lines
if (_.isArray(taxes)) {
taxes.forEach(tax => {
const i = _.findIndex(taxLines, (s) => {
return s.name === tax.name
})
if (i > -1) {
taxLines.splice(i, 1)
}
})
}
else if (_.isObject(taxes)) {
const i = _.findIndex(taxLines, (s) => {
return s.name === taxes.name
})
if (i > -1) {
taxLines.splice(i, 1)
}
}
this.tax_lines = taxLines
return this.save({transaction: options.transaction || null})
},
/**
*
* @param status
* @param save
*/
close: function(status, save) {
this.status = status || CART_STATUS.CLOSED
if (save) {
return this.save(save)
}
return this //Promise.resolve(this)
},
/**
*
* @param status
* @param save
*/
draft: function (status, save) {
this.status = status || CART_STATUS.DRAFT
if (save) {
return this.save(save)
}
return this //Promise.resolve(this)
},
clear: function () {
this.line_items = []
return this
},
/**
*
* @param order
* @param save
*/
ordered: function(order, save) {
this.order_id = order.id
this.status = CART_STATUS.ORDERED
if (save) {
return this.save(save)
}
// console.log('WANTS PROMISE', this)
return this //Promise.resolve(this)
},
/**
*
* @param options
*/
buildOrder: function(options) {
options = options || {}
return {
// Request info
client_details: options.client_details || this.client_details || {},
ip: options.ip || null,
payment_details: options.payment_details,
payment_kind: options.payment_kind || app.config.proxyCart.orders.payment_kind,
transaction_kind: options.transaction_kind || app.config.proxyCart.orders.transaction_kind,
fulfillment_kind: options.fulfillment_kind || app.config.proxyCart.orders.fulfillment_kind,
processing_method: options.processing_method || PAYMENT_PROCESSING_METHOD.CHECKOUT,
shipping_address: options.shipping_address || this.shipping_address,
billing_address: options.billing_address || this.billing_address,
email: options.email || null,
// Customer Info
customer_id: options.customer_id || this.customer_id || null,
// User ID
user_id: options.user_id || this.user_id || null,
// Cart Info
cart_token: this.token,
currency: this.currency,
line_items: this.line_items || [],
tax_lines: this.tax_lines || [],
shipping_lines: this.shipping_lines || [],
discounted_lines: this.discounted_lines || [],
coupon_lines: this.coupon_lines || [],
subtotal_price: this.subtotal_price,
taxes_included: this.taxes_included,
total_discounts: this.total_discounts,
total_coupons: this.total_coupons,
total_line_items_price: this.total_line_items_price,
total_price: this.total_due,
total_due: this.total_due,
total_tax: this.total_tax,
total_weight: this.total_weight,
total_items: this.total_items,
shop_id: this.shop_id,
has_shipping: this.has_shipping,
has_taxes: this.has_taxes,
has_subscription: this.has_subscription,
notes: this.notes,
//Pricing Overrides
pricing_override_id: this.pricing_override_id,
pricing_overrides: this.pricing_overrides,
total_overrides: this.total_overrides
}
},
/**
*
* @param options
* @returns {*}
*/
calculatePricingOverrides: function(options) {
options = options || {}
this.line_items = this.line_items || []
this.pricing_overrides = this.pricing_overrides || []
let pricingOverrides = []
let deduction = 0
if (!this.customer_id) {
return Promise.resolve(this)
}
return Promise.resolve()
.then(() => {
if (this.Customer) {
return this.Customer
}
else {
return app.orm['Customer'].findById(this.customer_id, {
attributes: ['id', 'account_balance'],
transaction: options.transaction || null
})
}
})
.then(_customer => {
if (!_customer) {
return
}
const exclusions = this.line_items.filter(item => {
item.exclude_payment_types = item.exclude_payment_types || []
return item.exclude_payment_types.indexOf('Account Balance') !== -1
})
pricingOverrides = this.pricing_overrides.filter(override => override)
const accountBalanceIndex = pricingOverrides.findIndex(p => p.name === 'Account Balance')
if (_customer.account_balance > 0) {
// Apply Customer Account balance
const removeTotal = _.sumBy(exclusions, (e) => e.calculated_price)
const deductibleTotal = Math.max(0, this.total_due - removeTotal)
deduction = Math.min(deductibleTotal, (deductibleTotal - (deductibleTotal - _customer.account_balance)))
if (deduction > 0) {
// If account balance has not been applied
if (accountBalanceIndex === -1) {
pricingOverrides.push({
name: 'Account Balance',
price: deduction
})
}
else {
pricingOverrides[accountBalanceIndex].price = deduction
}
}
}
else {
if (accountBalanceIndex > -1) {
pricingOverrides.splice(accountBalanceIndex, 1)
}
}
return this.setPricingOverrides(pricingOverrides)
})
.catch(err => {
app.log.error(err)
return this
})
},
/**
*
* @param options
* @returns {Promise.<TResult>}
*/
calculateDiscounts(options) {
options = options || {}
const criteria = []
const productIds = this.line_items.map(item => item.product_id)
let collectionPairs = [], discountCriteria = [], checkHistory = []
let resDiscounts
return Promise.resolve()
.then(() => {
return this.getCollectionPairs({transaction: options.transaction || null})
})
.then(_collections => {
collectionPairs = _collections || []
if (this.id) {
criteria.push({
model: 'cart',
model_id: this.id
})
}
if (this.customer_id) {
criteria.push({
model: 'customer',
model_id: this.customer_id
})
}
if (productIds.length > 0) {
criteria.push({
model: 'product',
model_id: productIds
})
}
if (collectionPairs.length > 0) {
criteria.push({
model: 'collection',
model_id: collectionPairs.map(c => c.collection)
})
}
if (criteria.length > 0) {
return app.orm['ItemDiscount'].findAll({
where: {
$or: criteria
},
attributes: ['discount_id', 'model', 'model_id'],
transaction: options.transaction || null
})
}
else {
return []
}
})
.then(discounts => {
discounts.forEach(discount => {
const i = discountCriteria.findIndex(d => d.discount === discount.discount_id)
if (i > -1) {
if (!discountCriteria[i][discount.model]) {
discountCriteria[i][discount.model] = []
}
discountCriteria[i][discount.model].push(discount.model_id)
}
else {
discountCriteria.push({
discount: discount.discount_id,
[discount.model]: [discount.model_id]
})
}
})
discountCriteria = discountCriteria.map(d => {
if (d.collection) {
d.collection.forEach(colId => {
const i = collectionPairs.findIndex(c => c.collection = colId)
if (i > -1) {
d = _.merge(d, collectionPairs[i])
}
})
}
return d
})
if (discounts.length > 0) {
return app.orm['Discount'].findAll({
where: {
id: discounts.map(item => item.discount_id),
status: DISCOUNT_STATUS.ENABLED
},
transaction: options.transaction || null
})
}
else {
return []
}
})
.then(_discounts => {
_discounts = _discounts || []
resDiscounts = _discounts
resDiscounts.forEach(discount => {
if (discount.applies_once_per_customer && this.customer_id) {
checkHistory.push(discount)
}
})
if (checkHistory.length > 0) {
return Promise.all(checkHistory.map(discount => {
return discount.eligibleCustomer(this.customer_id, {transaction: options.transaction || null})
}))
}
else {
return []
}
})
.then(_eligible => {
_eligible = _eligible || []
_eligible.forEach(discount => {
const i = resDiscounts.findIndex(i => i.id === discount.id)
if (i > -1) {
resDiscounts.splice(i, 1)
}
})
return this.setItemsDiscountedLines(resDiscounts, discountCriteria)
})
.catch(err => {
app.log.error(err)
return this
})
},
/**
*
* @param options
* @returns {Promise.<TResult>}
*/
calculateShipping: function(options) {
options = options || {}
if (!this.has_shipping) {
return Promise.resolve(this)
}
return app.services.ShippingService.calculate(this, this.line_items, this.shipping_address, app.orm['Cart'], options)
.then(shippingResult => {
// console.log('WORKING ON SHIPPING RESULT', shippingResult.line_items)
this.setItemsShippingLines(shippingResult.line_items)
return this
})
.catch(err => {
app.log.error(err)
return this
})
},
/**
*
* @param options
* @returns {Promise.<TResult>}
*/
calculateTaxes: function(options) {
options = options || {}
if (!this.has_taxes) {
return Promise.resolve(this)
}
return app.services.TaxService.calculate(this, this.line_items, this.shipping_address, app.orm['Cart'], options)
.then(taxesResult => {
// console.log('WORKING ON TAXES RESULT', taxesResult.line_items)
this.setItemsTaxLines(taxesResult.line_items)
return this
})
.catch(err => {
app.log.error(err)
return this
})
},
/**
*
* @param options
* @returns {Promise.<TResult>}
*/
recalculate: function(options) {
options = options || {}
// Default Values
// const collections = []
this.resetDefaults()
this.setLineItems(this.line_items)
return Promise.resolve()
.then(() => {
return this.calculateDiscounts({transaction: options.transaction || null})
})
// .then(discounts => {
// // Calculate Coupons
// return app.services.CouponService.calculate(this, collections, app.orm['Cart'])
// })
.then(() => {
return this.calculateShipping({transaction: options.transaction || null})
})
.then(() => {
return this.calculateTaxes({transaction: options.transaction || null})
})
.then(() => {
// Calculate Customer Balance
return this.calculatePricingOverrides({transaction: options.transaction || null})
})
.then(() => {
return this.setTotals()
})
.catch(err => {
app.log.error(err)
return this
})
},
/**
*
* @param options
* @returns {*}
*/
resolveCustomer: function(options) {
options = options || {}
if (
this.Customer
&& this.Customer instanceof app.orm['Customer']
&& options.reload !== true
) {
return Promise.resolve(this)
}
// Some orders may not have a customer Id
else if (!this.customer_id) {
return Promise.resolve(this)
}
else {
return this.getCustomer({transaction: options.transaction || null})
.then(_customer => {
if (_customer) {
_customer = _customer || null
this.Customer = _customer
this.setDataValue('Customer', _customer)
this.set('Customer', _customer)
}