UNPKG

trailpack-proxy-cart

Version:

eCommerce - Trailpack for Proxy Engine

1,117 lines (1,085 loc) 36 kB
/* eslint new-cap: [0] */ /* eslint no-console: [0] */ 'use strict' const Model = require('trails/model') // const helpers = require('proxy-engine-helpers') const _ = require('lodash') const shortId = require('shortid') const queryDefaults = require('../utils/queryDefaults') const CUSTOMER_STATE = require('../../lib').Enums.CUSTOMER_STATE /** * @module Customer * @description Customer Model */ module.exports = class Customer extends Model { static config (app, Sequelize) { return { options: { underscored: true, enums: { CUSTOMER_STATE: CUSTOMER_STATE }, // defaultScope: { // where: { // live_mode: app.config.proxyEngine.live_mode // } // }, scopes: { live: { where: { live_mode: true } } }, hooks: { beforeCreate: (values, options) => { if (values.ip) { values.create_ip = values.ip } // If not token was already created, create it if (!values.token) { values.token = `customer_${shortId.generate()}` } }, beforeUpdate: (values, options) => { if (values.ip) { values.update_ip = values.ip } }, afterCreate: (values, options) => { return app.services.CustomerService.afterCreate(values, options) .catch(err => { return Promise.reject(err) }) }, afterUpdate: (values, options) => { return app.services.CustomerService.afterUpdate(values, options) .catch(err => { return Promise.reject(err) }) } }, getterMethods: { full_name: function() { if (this.first_name && this.last_name) { return `${ this.first_name } ${ this.last_name }` } else if (this.company) { return `${ this.company }` } else { return null } } }, classMethods: { /** * Associate the Model * @param models */ associate: (models) => { models.Customer.belongsToMany(models.User, { as: 'owners', through: { model: models.UserItem, scope: { item: 'cart' } }, foreignKey: 'item_id', constraints: false }) models.Customer.belongsToMany(models.Address, { as: 'addresses', // otherKey: 'address_id', foreignKey: 'model_id', through: { model: models.ItemAddress, scope: { model: 'customer' }, constraints: false }, constraints: false }) models.Customer.belongsTo(models.Address, { as: 'shipping_address' }) models.Customer.belongsTo(models.Address, { as: 'billing_address' }) models.Customer.belongsTo(models.Address, { as: 'default_address' }) models.Customer.belongsToMany(models.Order, { as: 'orders', through: { model: models.CustomerOrder, unique: true }, foreignKey: 'customer_id' }) models.Customer.belongsTo(models.Order, { as: 'last_order', foreignKey: 'last_order_id', constraints: false }) models.Customer.belongsToMany(models.Tag, { as: 'tags', through: { model: models.ItemTag, unique: false, scope: { model: 'customer' } }, foreignKey: 'model_id', constraints: false }) models.Customer.belongsToMany(models.Collection, { as: 'collections', through: { model: models.ItemCollection, unique: false, scope: { model: 'customer' } }, foreignKey: 'model_id', constraints: false }) models.Customer.belongsToMany(models.Customer, { as: 'customers', through: { model: models.ItemCustomer, unique: false, scope: { model: 'customer' }, constraints: false }, foreignKey: 'model_id', otherKey: 'customer_id', constraints: false }) models.Customer.hasOne(models.Metadata, { as: 'metadata', foreignKey: 'customer_id' }) models.Customer.belongsToMany(models.Account, { as: 'accounts', through: { model: models.CustomerAccount, unique: false }, foreignKey: 'customer_id' }) models.Customer.belongsToMany(models.Source, { as: 'sources', through: { model: models.CustomerSource, unique: false }, foreignKey: 'customer_id' }) models.Customer.belongsToMany(models.User, { as: 'users', through: { model: models.CustomerUser, unique: true, }, foreignKey: 'customer_id' }) models.Customer.belongsToMany(models.Discount, { as: 'discounts', through: { model: models.ItemDiscount, unique: false, scope: { model: 'customer' } }, foreignKey: 'model_id', constraints: false }) models.Customer.hasOne(models.Cart, { as: 'default_cart', foreignKey: 'default_cart_id' }) models.Customer.belongsToMany(models.Cart, { as: 'carts', through: { model: models.CustomerCart, unique: false }, foreignKey: 'customer_id', constraints: false }) models.Customer.hasMany(models.Event, { as: 'events', foreignKey: 'object_id', scope: { object: 'customer' }, constraints: false }) models.Customer.belongsToMany(models.Event, { as: 'event_items', through: { model: models.EventItem, unique: false, scope: { object: 'customer' } }, foreignKey: 'object_id', constraints: false }) models.Customer.belongsToMany(models.Image, { as: 'images', through: { model: models.ItemImage, unique: false, scope: { model: 'customer' }, constraints: false }, foreignKey: 'model_id', constraints: false }) models.Customer.hasMany(models.DiscountEvent, { as: 'discount_events', foreignKey: 'customer_id' }) models.Customer.hasMany(models.AccountEvent, { as: 'account_events', foreignKey: 'customer_id' }) // models.Customer.hasOne(models.Order, { // targetKey: 'last_order_id', // foreignKey: 'id' // }) // models.Customer.hasMany(models.Order, { // as: 'orders', // foreignKey: 'customer_id' // }) }, /** * * @param id * @param options * @returns {*|Promise.<Instance>} */ findByIdDefault: function(id, options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Customer.default(app), options || {} ) return this.findById(id, options) }, /** * * @param token * @param options * @returns {*|Promise.<Instance>} */ findByTokenDefault: function(token, options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Customer.default(app), options || {}, { where: { token: token } } ) return this.findOne(options) }, /** * * @param options * @returns {Promise.<Object>} */ findAndCountDefault: function(options) { options = app.services.ProxyEngineService.mergeOptionDefaults( queryDefaults.Customer.default(app), options || {}, {distinct: true} ) return this.findAndCount(options) }, /** * Resolves a Customer by instance or by identifier * @param customer * @param options * @returns {*} */ resolve: function(customer, options){ options = options || {} options.create = options.create || true const Customer = this if (customer instanceof Customer){ return Promise.resolve(customer) } else if (customer && _.isObject(customer) && customer.id) { return Customer.findById(customer.id, options) .then(resCustomer => { if (!resCustomer && options.create !== false) { return app.services.CustomerService.create(customer, options) } return resCustomer }) } else if (customer && _.isObject(customer) && customer.email) { return Customer.findOne( app.services.ProxyEngineService.mergeOptionDefaults( options, { where: { email: customer.email } } ) ) .then(resCustomer => { if (!resCustomer && options.create !== false) { return app.services.CustomerService.create(customer, {transaction: options.transaction || null}) } return resCustomer }) } else if (customer && _.isNumber(customer)) { return Customer.findById(customer, options) } else if (customer && _.isString(customer)) { return Customer.findOne( app.services.ProxyEngineService.mergeOptionDefaults( options, { where: { email: customer } } ) ) } else if (options.create === false) { const err = new Error('Customer could not be resolved or created') return Promise.reject(err) } else { return app.services.CustomerService.create(customer, options) } } }, instanceMethods: { /** * * @param product * @param options * @returns {Promise.<TResult>} */ getProductHistory(product, options) { options = options || {} let hasPurchaseHistory = false, isSubscribed = false return this.hasPurchaseHistory(product.id, options) .then(pHistory => { hasPurchaseHistory = pHistory return this.isSubscribed(product.id, options) }) .then(pHistory => { isSubscribed = pHistory return { has_purchase_history: hasPurchaseHistory, is_subscribed: isSubscribed } }) .catch(err => { return { has_purchase_history: hasPurchaseHistory, is_subscribed: isSubscribed } }) }, /** * * @param productId * @param options * @returns {Promise.<boolean>} */ hasPurchaseHistory: function(productId, options) { options = options || {} return app.orm['OrderItem'].findOne({ where: { customer_id: this.id, product_id: productId, fulfillment_status: { $not: ['cancelled','pending','none'] } }, attributes: ['id'], transaction: options.transaction || null }) .then(pHistory => { if (pHistory) { return true } else { return false } }) .catch(err => { return false }) }, isSubscribed: function(productId, options) { options = options || {} return app.orm['Subscription'].findOne({ where: { customer_id: this.id, active: true, line_items: { $contains: [{ product_id: productId }] } }, attributes: ['id'], transaction: options.transaction || null }) .then(pHistory => { if (pHistory) { return true } else { return false } }) .catch(err => { return false }) }, /** * * @param options * @returns {string} */ getSalutation: function(options) { options = options || {} let salutation = 'Customer' if (this.full_name) { salutation = this.full_name } else if (this.email) { salutation = this.email } return salutation }, /** * * @param options * @returns {Promise.<TResult>} */ getDefaultSource: function (options) { options = options || {} const Source = app.orm['Source'] return Source.findOne({ where: { customer_id: this.id, is_default: true }, transaction: options.transaction || null }) .then(source => { // If there is no default, find one for the customer if (!source) { return Source.findOne({ where: { customer_id: this.id }, transaction: options.transaction || null }) } else { return source } }) .then(source => { return source }) }, /** * * @param order */ setLastOrder: function(order){ this.last_order_name = order.name this.last_order_id = order.id return this }, /** * * @param orderTotalDue */ setTotalSpent: function(orderTotalDue) { this.total_spent = this.total_spent + orderTotalDue return this }, setTotalOrders: function() { this.total_orders = this.total_orders + 1 return this }, setAvgSpent: function() { this.avg_spent = this.total_spent / this.total_orders return this }, /** * * @param newBalance */ // TODO Discussion: should this be pulled with each query or set after order? setAccountBalance: function(newBalance){ this.account_balance = newBalance return this }, logAccountBalance: function(type, price, currency, accountId, orderId, options) { options = options || {} type = type || 'debit' price = price || 0 currency = currency || 'USD' return this.createAccount_event({ type: type, price: price, account_id: accountId, order_id: orderId }, { transaction: options.transaction || null }) .then(_event => { const event = { object_id: this.id, object: 'customer', objects: [{ customer: this.id }], type: `customer.account_balance.${type}`, message: `Customer ${ this.email || 'ID ' + this.id } account balance was ${type}ed by ${ app.services.ProxyCartService.formatCurrency(price, currency) } ${currency}`, data: this } return app.services.ProxyEngineService.publish(event.type, event, { save: true, transaction: options.transaction || null }) }) .then(_event => { const newBalance = type === 'debit' ? Math.max(0, this.account_balance - price) : this.account_balance + price return this.setAccountBalance(newBalance) }) }, /** * * @param preNotification * @param options * @returns {Promise.<T>} */ notifyUsers: function(preNotification, options) { options = options || {} return this.resolveUsers({ attributes: ['id','email','username'], transaction: options.transaction || null, reload: options.reload || null, }) .then(() => { if (this.users && this.users.length > 0) { return app.services.NotificationService.create(preNotification, this.users, {transaction: options.transaction || null}) .then(notes => { app.log.debug('NOTIFY', this.id, this.email, this.users.map(u => u.id), preNotification.send_email, notes.users.map(u => u.id)) return notes }) } else { return [] } }) }, /** * * @param options * @returns {Promise.<T>} */ resolveCollections(options) { options = options || {} if ( this.collections && this.collections.length > 0 && this.collections.every(d => d instanceof app.orm['Collection']) && options.reload !== true ) { return Promise.resolve(this) } else { return this.getCollections({transaction: options.transaction || null}) .then(_collections => { _collections = _collections || [] this.collections = _collections this.setDataValue('collections', _collections) this.set('collections', _collections) 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 }) } }, /** * * @param options * @returns {*} */ resolveMetadata: function(options) { options = options || {} if ( this.metadata && this.metadata instanceof app.orm['Metadata'] && options.reload !== true ) { return Promise.resolve(this) } else { return this.getMetadata({transaction: options.transaction || null}) .then(_metadata => { _metadata = _metadata || {customer_id: this.id} this.metadata = _metadata this.setDataValue('metadata', _metadata) this.set('metadata', _metadata) return this }) } }, /** * * @param options * @returns {Promise.<T>} */ resolveUsers(options) { options = options || {} if ( this.users && this.users.length > 0 && this.users.every(u => u instanceof app.orm['User']) && options.reload !== true ) { return Promise.resolve(this) } else { return this.getUsers({transaction: options.transaction || null}) .then(_users => { _users = _users || [] this.users = _users this.setDataValue('users', _users) this.set('users', _users) return this }) } }, /** * * @param options * @returns {*} */ resolveDefaultAddress: function(options) { options = options || {} if ( this.default_address && this.default_address instanceof app.orm['Address'] && options.reload !== true ) { return Promise.resolve(this) } // Some carts may not have a default address Id else if (!this.default_address_id) { this.default_address = app.orm['Address'].build({}) return Promise.resolve(this) } else { return this.getDefault_address({transaction: options.transaction || null}) .then(address => { address = address || null this.default_address = address this.setDataValue('default_address', address) this.set('default_address', address) return this }) } }, /** * * @param options * @returns {*} */ resolveShippingAddress: function(options) { options = options || {} if ( this.shipping_address && this.shipping_address instanceof app.orm['Address'] && options.reload !== true ) { return Promise.resolve(this) } // Some carts may not have a shipping address Id else if (!this.shipping_address_id) { this.shipping_address = app.orm['Address'].build({}) return Promise.resolve(this) } else { return this.getShipping_address({transaction: options.transaction || null}) .then(address => { address = address || null this.shipping_address = address this.setDataValue('shipping_address', address) this.set('shipping_address', address) return this }) } }, /** * * @param options * @returns {*} */ resolveBillingAddress: function(options) { options = options || {} if ( this.billing_address && this.billing_address instanceof app.orm['Address'] && options.reload !== true ) { return Promise.resolve(this) } // Some carts may not have a billing address Id else if (!this.billing_address_id) { this.billing_address = app.orm['Address'].build({}) return Promise.resolve(this) } else { return this.getBilling_address({transaction: options.transaction || null}) .then(address => { address = address || null this.billing_address = address this.setDataValue('billing_address', address) this.set('billing_address', address) return this }) } }, // TODO resolvePaymentDetailsToSources: function(options) { options = options || {} }, /** * Email to notify user's that there are pending items in cart * @param options * @returns {Promise.<T>} */ sendRetargetEmail(options) { options = options || {} return app.emails.Customer.retarget(this, { send_email: app.config.proxyCart.emails.customerRetarget }, { transaction: options.transaction || null }) .then(email => { return this.notifyUsers(email, {transaction: options.transaction || null}) }) .catch(err => { app.log.error(err) return }) }, /** * * @param address * @param options * @returns {Promise.<TResult>|*} */ updateDefaultAddress(address, options) { options = options || {} const Address = app.orm['Address'] const defaultUpdate = Address.cleanAddress(address) return this.resolveDefaultAddress({transaction: options.transaction || null}) .then(() => { // If this address has an ID, thenw e should try and update it if (address.id || address.token) { return Address.resolve(address, {transaction: options.transaction || null}) .then(address => { return address.update(defaultUpdate, {transaction: options.transaction || null}) }) } else { return this.default_address .merge(defaultUpdate) .save({transaction: options.transaction || null}) } }) .then(defaultAddress => { this.default_address = defaultAddress this.setDataValue('default_address', defaultAddress) this.set('default_address', defaultAddress) if (this.default_address_id !== defaultAddress.id) { return this.setDefault_address(defaultAddress.id, {transaction: options.transaction || null}) } return this }) }, /** * * @param address * @param options * @returns {Promise.<TResult>|*} */ updateShippingAddress(address, options) { options = options || {} const Address = app.orm['Address'] const shippingUpdate = Address.cleanAddress(address) return this.resolveShippingAddress({transaction: options.transaction || null}) .then(() => { // If this address has an ID, thenw e should try and update it if (address.id || address.token) { return Address.resolve(address, {transaction: options.transaction || null}) .then(address => { return address.update(shippingUpdate, {transaction: options.transaction || null}) }) } else { return this.shipping_address .merge(shippingUpdate) .save({transaction: options.transaction || null}) } }) .then(shippingAddress => { this.shipping_address = shippingAddress this.setDataValue('shipping_address', shippingAddress) this.set('shipping_address', shippingAddress) if (this.shipping_address_id !== shippingAddress.id) { return this.setShipping_address(shippingAddress.id, {transaction: options.transaction || null}) } return this }) }, /** * * @param address * @param options * @returns {Promise.<TResult>|*} */ updateBillingAddress(address, options) { options = options || {} const Address = app.orm['Address'] const billingUpdate = Address.cleanAddress(address) return this.resolveBillingAddress({transaction: options.transaction || null}) .then(() => { // If this address has an ID, thenw e should try and update it if (address.id || address.token) { return Address.resolve(address, {transaction: options.transaction || null}) .then(address => { return address.update(billingUpdate, {transaction: options.transaction || null}) }) } else { return this.billing_address .merge(billingUpdate) .save({transaction: options.transaction || null}) } }) .then(billingAddress => { this.billing_address = billingAddress this.setDataValue('billing_address', billingAddress) this.set('billing_address', billingAddress) if (this.billing_address_id !== billingAddress.id) { return this.setBilling_address(billingAddress.id, {transaction: options.transaction || null}) } return this }) }, /** * */ toJSON: function() { const resp = this instanceof app.orm['Customer'] ? this.get({ plain: true }) : this // Transform Tags to array on toJSON if (resp.tags) { resp.tags = resp.tags.map(tag => { if (_.isString(tag)) { return tag } return tag.name }) } else { resp.tags = [] } // Transform Metadata to plain on toJSON if (resp.metadata) { if (typeof resp.metadata.data !== 'undefined') { resp.metadata = resp.metadata.data } } else { resp.metadata = {} } return resp } } } } } static schema (app, Sequelize) { return { // Unique identifier for a particular customer. token: { type: Sequelize.STRING, unique: true }, // accepts_marketing: { type: Sequelize.BOOLEAN, defaultValue: true }, // Customer First Name if not a Company first_name: { type: Sequelize.STRING }, // Customer Last Name if not a Company last_name: { type: Sequelize.STRING }, // Customer Company if not a User company: { type: Sequelize.STRING }, // Customer Phone phone: { type: Sequelize.STRING }, // Customers Email if there is one email: { type: Sequelize.STRING, validate: { isEmail: true }, set: function(val) { return this.setDataValue('email', val ? val.toLowerCase() : null) } }, // note: { type: Sequelize.STRING }, // // The name of the Last order this Customer Placed last_order_id: { type: Sequelize.INTEGER, // references: { // model: 'Order', // key: 'id' // } }, last_order_name: { type: Sequelize.STRING }, // TODO make this part of the Default Query // orders_count: { // type: Sequelize.INTEGER, // defaultValue: 0 // }, // The standing state of the customer: enabled, disabled, invited, declined state: { type: Sequelize.ENUM, values: _.values(CUSTOMER_STATE), defaultValue: CUSTOMER_STATE.ENABLED }, type: { type: Sequelize.STRING, defaultValue: 'default' }, // If customer is tax exempt tax_exempt: { type: Sequelize.BOOLEAN, defaultValue: false }, // The total amount the customer has spent total_spent: { type: Sequelize.INTEGER, defaultValue: 0 }, // The total amount the customer has spent avg_spent: { type: Sequelize.INTEGER, defaultValue: 0 }, // The total count of orders created total_orders: { type: Sequelize.INTEGER, defaultValue: 0 }, // The amount the customer has as a credit on their account account_balance: { type: Sequelize.INTEGER, defaultValue: 0 }, // If the customer's email address is verified verified_email: { type: Sequelize.BOOLEAN, defaultValue: false }, // Addresses default_address_id: { type: Sequelize.INTEGER }, shipping_address_id: { type: Sequelize.INTEGER }, billing_address_id: { type: Sequelize.INTEGER }, // IP addresses ip: { type: Sequelize.STRING }, create_ip: { type: Sequelize.STRING }, update_ip: { type: Sequelize.STRING }, // Live Mode live_mode: { type: Sequelize.BOOLEAN, defaultValue: app.config.proxyEngine.live_mode } } } }