trailpack-proxy-cart
Version:
eCommerce - Trailpack for Proxy Engine
1,384 lines (1,295 loc) • 62.6 kB
JavaScript
/* eslint new-cap: [0] */
/* eslint no-console: [0] */
'use strict'
const Model = require('trails/model')
const _ = require('lodash')
const moment = require('moment')
const Errors = require('proxy-engine-errors')
const helpers = require('proxy-engine-helpers')
const queryDefaults = require('../utils/queryDefaults')
const INTERVALS = require('../../lib').Enums.INTERVALS
const SUBSCRIPTION_CANCEL = require('../../lib').Enums.SUBSCRIPTION_CANCEL
const DISCOUNT_STATUS = require('../../lib').Enums.DISCOUNT_STATUS
const PAYMENT_PROCESSING_METHOD = require('../../lib').Enums.PAYMENT_PROCESSING_METHOD
/**
* @module Subscription
* @description Subscription Model
*/
module.exports = class Subscription extends Model {
static config (app, Sequelize) {
return {
options: {
underscored: true,
enums: {
INTERVALS: INTERVALS,
SUBSCRIPTION_CANCEL: SUBSCRIPTION_CANCEL
},
// defaultScope: {
// where: {
// live_mode: app.config.proxyEngine.live_mode
// }
// },
scopes: {
live: {
where: {
live_mode: true
}
},
active: {
where: {
active: true
}
},
deactivated: {
where: {
active: false,
cancelled: false
}
},
cancelled: {
where: {
cancelled: true
}
}
},
indexes: [
// Creates a gin index on data with the jsonb_path_ops operator
{
fields: ['line_items'],
using: 'gin',
operator: 'jsonb_path_ops'
}
],
hooks: {
beforeCreate: (values, options) => {
return app.services.SubscriptionService.beforeCreate(values)
.catch(err => {
return Promise.reject(err)
})
},
beforeUpdate: (values, options) => {
return app.services.SubscriptionService.beforeUpdate(values)
.catch(err => {
return Promise.reject(err)
})
},
afterCreate: (values, options) => {
return app.services.SubscriptionService.afterCreate(values, options)
.then(values => {
return values.save({transaction: options.transaction || null})
})
.catch(err => {
return Promise.reject(err)
})
},
afterUpdate: (values, options) => {
return app.services.SubscriptionService.afterCreate(values, options)
.catch(err => {
return Promise.reject(err)
})
}
},
classMethods: {
/**
* Associate the Model
* @param models
*/
associate: (models) => {
// The customer this subscription belongs to
models.Subscription.belongsTo(models.Customer, {
// as: 'customer_id'
})
models.Subscription.belongsTo(models.Shop, {
// as: 'customer_id'
})
// // The Shop that originated this order
// models.Subscription.belongsTo(models.Shop, {
// // as: 'shop_id'
// })
// The Order that Created this Subscription
models.Subscription.belongsTo(models.Order, {
as: 'original_order'
})
// The latest order that this subscription created.
models.Subscription.belongsTo(models.Order, {
as: 'last_order'
})
// The collection of subscriptions for a given customer
models.Subscription.belongsToMany(models.Collection, {
as: 'collections',
through: {
model: models.ItemCollection,
unique: false,
scope: {
model: 'subscription'
}
},
foreignKey: 'model_id',
constraints: false
})
models.Subscription.hasMany(models.Event, {
as: 'events',
foreignKey: 'object_id',
scope: {
object: 'subscription'
},
constraints: false
})
models.Subscription.hasMany(models.OrderItem, {
as: 'order_items',
foreignKey: 'subscription_id'
})
models.Subscription.belongsToMany(models.Discount, {
as: 'discounts',
through: {
model: models.ItemDiscount,
unique: false,
scope: {
model: 'subscription'
}
},
foreignKey: 'model_id',
constraints: false
})
},
/**
*
* @param criteria
* @param options
* @returns {*|Promise.<Instance>}
*/
findByIdDefault: function(criteria, options) {
options = app.services.ProxyEngineService.mergeOptionDefaults(
queryDefaults.Subscription.default(app),
options || {}
)
return this.findById(criteria, options)
},
/**
*
* @param token
* @param options
* @returns {*|Promise.<Instance>}
*/
findByTokenDefault: function(token, options) {
options = app.services.ProxyEngineService.mergeOptionDefaults(
queryDefaults.Subscription.default(app),
options || {},
{
where: {
token: token
}
}
)
return this.findOne(options)
},
/**
*
* @param options
* @param batch
* @returns Promise.<T>
*/
batch: function (options, batch) {
const self = this
options = options || {}
options.limit = options.limit || 10
options.offset = options.offset || 0
options.regressive = options.regressive || false
const recursiveQuery = function(options) {
let count = 0
return self.findAndCountAll(options)
.then(results => {
count = results.count
return batch(results.rows)
})
.then(() => {
if (count >= (options.regressive ? options.limit : options.offset + options.limit)) {
options.offset = options.regressive ? 0 : options.offset + options.limit
return recursiveQuery(options)
}
else {
return Promise.resolve()
}
})
}
return recursiveQuery(options)
},
/**
*
* @param subscription
* @param options
* @returns {*}
*/
resolve: function(subscription, options){
options = options || {}
// console.log('TYPEOF subscription',typeof subscription)
const Subscription = this
if (subscription instanceof Subscription){
return Promise.resolve(subscription)
}
else if (subscription && _.isObject(subscription) && subscription.id) {
return Subscription.findByIdDefault(subscription.id, options)
.then(resSubscription => {
if (!resSubscription) {
throw new Errors.FoundError(Error(`Subscription ${subscription.id} not found`))
}
return resSubscription
})
}
else if (subscription && _.isObject(subscription) && subscription.token) {
return Subscription.findByTokenDefault(subscription.token, options)
.then(resSubscription => {
if (!resSubscription) {
throw new Errors.FoundError(Error(`Subscription ${subscription.token} not found`))
}
return resSubscription
})
}
else if (subscription && _.isNumber(subscription)) {
return Subscription.findByIdDefault(subscription, options)
.then(resSubscription => {
if (!resSubscription) {
throw new Errors.FoundError(Error(`Subscription ${subscription} not found`))
}
return resSubscription
})
}
else if (subscription && _.isString(subscription)) {
return Subscription.findByTokenDefault(subscription, options)
.then(resSubscription => {
if (!resSubscription) {
throw new Errors.FoundError(Error(`Subscription ${subscription} not found`))
}
return resSubscription
})
}
else {
// TODO create proper error
const err = new Error(`Unable to resolve Subscription ${subscription}`)
return Promise.reject(err)
}
}
},
instanceMethods: {
activate: function() {
this.cancel_reason = null
this.cancelled_at = null
this.cancelled = false
this.active = true
// TODO, this may also need to trigger a new order
// Check if the dates need to be updated
const d = moment().startOf('hour')
const r = d.clone()
// console.log('CHECK DATE', d, this.renewed_at, this.renews_on)
if (this.unit === INTERVALS.DAY) {
// d.setDate(d.getDay() + this.interval)
d.add(this.interval, 'D')
}
else if (this.unit === INTERVALS.WEEK) {
// d.setMonth(d.getWeek() + this.interval);
d.add(this.interval, 'W')
}
else if (this.unit === INTERVALS.MONTH) {
// d.setMonth(d.getMonth() + this.interval)
d.add(this.interval, 'M')
// console.log(d)
}
else if (this.unit === INTERVALS.BIMONTH) {
d.add(this.interval * 2, 'M')
// d.setMonth(d.getMonth() + this.interval * 2)
}
else if (this.unit === INTERVALS.YEAR) {
d.add(this.interval, 'Y')
// d.setYear(d.getYear() + this.interval)
}
else if (this.unit === INTERVALS.BIYEAR) {
d.add(this.interval * 2, 'Y')
// d.setYear(d.getYear() + this.interval * 2)
}
// Reset Renews on date
if (moment() > moment(this.renews_on)) {
this.renewed_at = r.format('YYYY-MM-DD HH:mm:ss')
this.renews_on = d.format('YYYY-MM-DD HH:mm:ss')
}
return this
},
resolveCustomer: function(options) {
options = options || {}
if (
this.Customer
&& this.Customer instanceof app.orm['Customer']
&& options.reload !== true
) {
return Promise.resolve(this)
}
// A subscription always requires a customer, but just in case.
else if (!this.customer_id) {
return Promise.resolve(this)
}
else {
return this.getCustomer({transaction: options.transaction || null})
.then(_customer => {
_customer = _customer || null
this.Customer = _customer
this.setDataValue('Customer', _customer)
this.set('Customer', _customer)
return this
})
}
},
/**
*
* @param options
* @returns {Promise.<T>}
*/
resolveDiscounts(options) {
options = options || {}
if (
this.discounts
&& this.discounts.length > 0
&& this.discounts.every(d => d instanceof app.orm['Discount'])
&& options.reload !== true
) {
return Promise.resolve(this)
}
else {
return this.getDiscounts({transaction: options.transaction || null})
.then(_discounts => {
_discounts = _discounts || []
this.discounts = _discounts
this.setDataValue('discounts', _discounts)
this.set('discounts', _discounts)
return this
})
}
},
resolveLastOrder: function(options) {
options = options || {}
if (
this.last_order
&& this.last_order instanceof app.orm['Order']
&& options.reload !== true
) {
return Promise.resolve(this)
}
// A subscription always requires a customer, but just in case.
else if (!this.last_order_id) {
return Promise.resolve(this)
}
else {
return this.getLast_order({transaction: options.transaction || null})
.then(_order => {
_order = _order || null
this.last_order = _order
this.setDataValue('last_order', _order)
this.set('last_order', _order)
return this
})
}
},
resolveOriginalOrder: function(options) {
options = options || {}
if (
this.original_order
&& this.original_order instanceof app.orm['Order']
&& options.reload !== true
) {
return Promise.resolve(this)
}
// A subscription always requires a customer, but just in case.
else if (!this.original_order_id) {
return Promise.resolve(this)
}
else {
return this.getOriginal_order({transaction: options.transaction || null})
.then(_order => {
_order = _order || null
this.original_order = _order
this.setDataValue('original_order', _order)
this.set('original_order', _order)
return this
})
}
},
/**
*
* @param preNotification
* @param options
*/
notifyCustomer: function(preNotification, options) {
options = options || {}
if (this.customer_id) {
return this.resolveCustomer({
attributes: ['id','email','company','first_name','last_name','full_name'],
transaction: options.transaction || null,
reload: options.reload || null
})
.then(() => {
if (this.Customer && this.Customer instanceof app.orm['Customer']) {
return this.Customer.notifyUsers(preNotification, {transaction: options.transaction || null})
}
else {
return
}
})
.then(() => {
return this
})
}
else {
return Promise.resolve(this)
}
},
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_shipping = false
this.has_taxes = false
this.discounted_lines = []
this.coupon_lines = []
this.shipping_lines = []
this.tax_lines = []
// 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
},
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 lines
*/
setLineItems: function(lines) {
this.line_items = lines || []
this.total_items = 0
this.subtotal_price = 0
this.total_line_items_price = 0
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
this.has_shipping = true
}
// Check if at least one item requires taxes
if (item.requires_taxes) {
this.has_taxes = true
}
this.total_items = this.total_items + item.quantity
this.subtotal_price = this.subtotal_price + item.price // * item.quantity
this.total_line_items_price = this.total_line_items_price + item.price // * item.quantity
})
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
})
// console.log('Factored results', factoredDiscountedLines)
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) {
this.total_shipping = 0
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) {
this.total_tax = 0
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
},
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 = {
subscription_id: this.id,
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,
pricing_properties: 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: [],
// TODO handle disocunts
total_discounts: 0,
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,
average_shipping: data.Product.average_shipping,
exclude_payment_types: data.Product.exclude_payment_types,
vendor: data.Product.vendor ? data.Product.vendor.name || data.Product.vendor : data.Product.vendor,
live_mode: data.live_mode
}
return line
},
addLine: function(item, qty, properties, options) {
options = options || {}
// The quantity available of this variant
let lineQtyAvailable = -1
// 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 (itemIndex > -1) {
app.log.silly('Subscription.addLine NEW QTY', lineItems[itemIndex])
const maxQuantity = lineItems[itemIndex].max_quantity
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
}
else {
const maxQuantity = item.max_quantity
let calculatedQty = qty
if (maxQuantity > -1 && calculatedQty > maxQuantity) {
calculatedQty = maxQuantity
}
if (lineQtyAvailable > -1 && calculatedQty > lineQtyAvailable) {
calculatedQty = Math.max(0, lineQtyAvailable - calculatedQty)
}
item.quantity = calculatedQty
item.fulfillable_quantity = calculatedQty
item.max_quantity = maxQuantity
item.properties = properties
let line = this.line(item)
line = this.setLineProperties(line)
app.log.silly('Subscription.addLine NEW LINE', line)
lineItems.push(line)
this.line_items = lineItems
}
return this
})
},
removeLine: function(item, qty) {
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)
}
},
/**
*
* @returns Instance
*/
renew: function() {
this.renewed_at = new Date(Date.now())
this.renew_retry_at = null
this.total_renewal_attempts = 0
this.total_renewals++
// Active now because it was renewed
this.active = true
// Clear prior cancelled arguments
this.cancelled = false
this.cancel_reason = null
this.cancelled_at = null
// Clear the prior renewal notices
this.notice_sent = false
this.notice_sent_at = null
return this
},
willRenew: function() {
this.notice_sent = true
this.notice_sent_at = new Date(Date.now())
return this
},
/**
*
* @returns Instance
*/
retry: function() {
this.renew_retry_at = new Date(Date.now())
this.total_renewal_attempts++
return this
},
/**
*
* @param options
* @returns {Promise.<T>}
*/
sendActivateEmail(options) {
options = options || {}
return app.emails.Subscription.activated(this, {
send_email: app.config.proxyCart.emails.subscriptionRenewed
}, {
transaction: options.transaction || null
})
.then(email => {
return this.notifyCustomer(email, {transaction: options.transaction || null})
})
.catch(err => {
app.log.error(err)
return
})
},
/**
*
* @param options
* @returns {Promise.<T>}
*/
sendCancelledEmail(options) {
options = options || {}
return app.emails.Subscription.cancelled(this, {
send_email: app.config.proxyCart.emails.subscriptionCancelled
}, {
transaction: options.transaction || null
})
.then(email => {
return this.notifyCustomer(email, {transaction: options.transaction || null})
})
.catch(err => {
app.log.error(err)
return
})
},
/**
*
* @param options
* @returns {Promise.<T>}
*/
sendDeactivateEmail(options) {
options = options || {}
return app.emails.Subscription.deactivated(this, {
send_email: app.config.proxyCart.emails.subscriptionDeactivated
}, {
transaction: options.transaction || null
})
.then(email => {
return this.notifyCustomer(email, {transaction: options.transaction || null})
})
.catch(err => {
app.log.error(err)
return
})
},
/**
*
* @param options
* @returns {Promise.<T>}
*/
sendFailedEmail(options) {
options = options || {}
return app.emails.Subscription.failed(this, {
send_email: app.config.proxyCart.emails.subscriptionFailed
}, {
transaction: options.transaction || null
})
.then(email => {
return this.notifyCustomer(email, {transaction: options.transaction || null})
})
.catch(err => {
app.log.error(err)
return
})
},
/**
*
* @param options
* @returns {Promise.<T>}
*/
sendRenewedEmail(options) {
options = options || {}
return app.emails.Subscription.renewed(this, {
send_email: app.config.proxyCart.emails.subscriptionRenewed
}, {
transaction: options.transaction || null
})
.then(email => {
return this.notifyCustomer(email, {transaction: options.transaction || null})
})
.catch(err => {
app.log.error(err)
return
})
},
/**
*
* @param options
* @returns {Promise.<T>}
*/
sendUpdatedEmail(options) {
options = options || {}
return app.emails.Subscription.updated(this, {
send_email: app.config.proxyCart.emails.subscriptionUpdated
}, {
transaction: options.transaction || null
})
.then(email => {
return this.notifyCustomer(email, {transaction: options.transaction || null})
})
.catch(err => {
app.log.error(err)
return
})
},
/**
*
* @param options
* @returns {Promise.<T>}
*/
sendWillRenewEmail(options) {
options = options || {}
return app.emails.Subscription.willRenew(this, {
send_email: app.config.proxyCart.emails.subscriptionWillRenew
}, {
transaction: options.transaction || null
})
.then(email => {
return this.notifyCustomer(email, {transaction: options.transaction || null})
})
.catch(err => {
app.log.error(err)
return
})
},
/**
*
* @param data
*/
buildOrder: function(data) {
data = data || {}
return {
// Request info
client_details: data.client_details || this.client_details,
ip: data.ip || null,
payment_details: data.payment_details,
payment_kind: data.payment_kind || app.config.proxyCart.orders.payment_kind,
transaction_kind: data.transaction_kind || app.config.proxyCart.orders.transaction_kind,
fulfillment_kind: data.fulfillment_kind || app.config.proxyCart.orders.fulfillment_kind,
processing_method: data.processing_method || PAYMENT_PROCESSING_METHOD.SUBSCRIPTION,
shipping_address: data.shipping_address || this.shipping_address,
billing_address: data.billing_address || this.billing_address,
// Customer Info
customer_id: data.customer_id || this.customer_id || null,
email: data.email || null,
// User ID
user_id: data.user_id || this.user_id || null,
// Subscription Info
subscription_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_subscription: this.has_subscription,
has_taxes: this.has_taxes,
//Pricing Overrides
pricing_override_id: this.pricing_override_id,
pricing_overrides: this.pricing_overrides || [],
total_overrides: this.total_overrides
}
},
/**
*
* @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
})
app.log.debug('Subscription.calculateDiscount criteria', discountCriteria)
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>}
*/
getCollectionPairs: function(options) {
options = options || {}
const collectionPairs = []
const criteria = []
let productIds = this.line_items.map(item => item.product_id)
productIds = productIds.filter(i => i)
let variantIds = this.line_items.map(item => item.variant_id)
variantIds = variantIds.filter(i => i)
return Promise.resolve()
.then(() => {
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 (variantIds.length > 0) {
criteria.push({
model: 'productvariant',
model_id: variantIds
})