trailpack-proxy-cart
Version:
eCommerce - Trailpack for Proxy Engine
854 lines (802 loc) • 22.2 kB
JavaScript
/* eslint no-console: [0] */
'use strict'
const Service = require('trails/service')
const _ = require('lodash')
const Errors = require('proxy-engine-errors')
const joi = require('joi')
const sharp = require('sharp')
const lib = require('../../lib')
const geolib = require('geolib')
const currencyFormatter = require('currency-formatter')
const removeMd = require('remove-markdown')
const stripTags = require('striptags')
const request = require('request')
/**
* @module ProxyCartService
* @description ProxyCart Service
*/
module.exports = class ProxyCartService extends Service {
constructor(app) {
super(app)
// Middleware exports
this._key = 'proxyCart'
this.initialize = require('../../lib/middleware/initialize')
this.authenticate = require('../../lib/middleware/authenticate')
this.cart = require('../../lib/middleware/cart')
this.customer = require('../../lib/middleware/customer')
}
notifyAdmins(preNotification, options) {
options = options || {}
const User = this.app.orm.User
if (!preNotification) {
return Promise.reject('No Notification to send to Admins')
}
return User.findAll({
include: [
{
model: this.app.orm['Role'],
as: 'roles',
where: {
name: 'admin'
}
}
]
})
.then(users => {
if (!users || users.length === 0) {
throw new Error('No Admins to send emails to')
}
return this.app.services.NotificationService.create(preNotification, users, {transaction: options.transaction || null})
.then(notes => {
this.app.log.debug('NOTIFY ADMINS', users.map(u => u.id), preNotification.send_email, notes.users.map(u => u.id))
return notes
})
})
}
/**
*
* @param url
* @returns {Promise}
*/
downloadImage(url) {
return new Promise((resolve, reject) => {
const req = request.defaults({ encoding: null })
req.get(url, (err, res, body) => {
if (err) {
this.app.log.error(err)
return reject(err)
}
return resolve(body)
})
})
}
/**
*
* @param imageUrl
* @param options
* @returns {Promise}
*/
buildImages(imageUrl, options) {
return new Promise((resolve, reject) =>{
const images = {
full: imageUrl,
thumbnail: imageUrl,
small: imageUrl,
medium: imageUrl,
large: imageUrl
}
let buffer
this.downloadImage(imageUrl)
.then(resBuffer => {
buffer = resBuffer
return sharp(buffer)
.resize(200)
.toBuffer()
})
.then(thumbnailBuffer => {
return this.uploadImage(thumbnailBuffer, images.thumbnail)
})
.then(thumbnail => {
images.thumbnail = thumbnail.url
return sharp(buffer)
.resize(300)
.toBuffer()
})
.then(smallBuffer => {
return this.uploadImage(smallBuffer, images.small)
})
.then(small => {
images.small = small.url
return sharp(buffer)
.resize(400)
.toBuffer()
})
.then(mediumBuffer => {
return this.uploadImage(mediumBuffer, images.medium)
})
.then(medium => {
images.medium = medium.url
return sharp(buffer)
.resize(500)
.toBuffer()
})
.then(largeBuffer => {
return this.uploadImage(largeBuffer, images.large)
})
.then((large) => {
images.large = large.url
return resolve(images)
})
.catch((err) => {
this.app.log.error(err)
return resolve(images)
})
})
}
/**
*
* @param image
* @param orgUrl
* @returns {*}
*/
uploadImage(image, orgUrl) {
return this.app.services.DataStoreGenericService.upload(image)
.catch(err => {
return {
url: orgUrl
}
})
}
/**
*
* @param text
* @returns {string|null}
*/
handle(text) {
if (!text) {
return null
}
return text.toString().trim()
// .replace(/(\r\n|\n|\r)/g,'') // Replace new lines
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/&/g, '-and-') // Replace & with 'and'
.replace(/[^\w\-]+/g, '') // Remove all non-word chars but hyphens
.replace(/\-\-+/g, '-') // Remove double hyphens
.toLowerCase() // Make lowercase
.substring(0, 255) // Allow only 255 characters
}
/**
*
* @param text
* @returns {string|null}
*/
splitHandle(text) {
if (!text) {
return null
}
return text.toString().trim()
// .replace(/(\r\n|\n|\r)/g, '') // Replace new lines
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/&/g, '-and-') // Replace & with 'and'
.replace(/[^\w:\-]+/g, '') // Remove all non-word chars but colons and hyphens
.replace(/\-\-+/g, '-') // Remove double hyphens
.toLowerCase() // Make Lowercase
}
/**
*
* @param text
* @returns {string|null}
*/
sku(text) {
if (!text) {
return null
}
text = text.toString().trim()
.replace(/[^\w:\-]+/g, '') // Remove all non-word chars but colons and hyphens
return removeMd(stripTags(text)).toString()
}
/**
*
* @param text
* @returns {string|null}
*/
title(text) {
if (!text) {
return null
}
text = text.toString().trim()
return removeMd(stripTags(text)).toString().substring(0, 255)
}
/**
*
* @param text
* @returns {string|null}
*/
name(text) {
if (!text) {
return null
}
text = text.toString().trim()
return removeMd(stripTags(text)).toString().toLowerCase().substring(0, 255)
}
/**
*
* @param text
* @returns {string|null}
*/
description(text) {
if (!text) {
return null
}
text = text.toString().trim()
return removeMd(stripTags(text)).toString()
}
/**
*
* @param ounces
* @returns {number}
*/
ouncesToGrams(ounces) {
return ounces * 28.3495231
}
/**
*
* @param pounds
* @returns {number}
*/
poundsToGrams(pounds) {
return pounds * 16 * 28.3495231
}
/**
*
* @param kilogram
* @returns {number}
*/
kilogramsToGrams(kilogram) {
return kilogram / 1000
}
/**
*
* @param weight
* @param weightUnit
* @returns {*}
*/
resolveConversion(weight, weightUnit){
switch (weightUnit) {
case 'kg':
return this.kilogramsToGrams(weight)
case 'oz':
return this.ouncesToGrams(weight)
case 'lb':
return this.poundsToGrams(weight)
default:
return weight
}
}
/**
*
* @param str
* @returns {boolean}
*/
isJson(str) {
try {
JSON.parse(str)
}
catch (e) {
return false
}
return true
}
/**
*
* @param num
* @param currency
* @returns {*}
*/
formatCurrency(num, currency) {
currency = currency || this.app.config.get('proxyCart.default_currency')
return currencyFormatter.format(num / 100, { code: currency.toUpperCase() })
}
/**
*
* @param address
* @returns {*}
*/
validateAddress(address) {
try {
joi.validate(address, lib.Schemas.address.address)
}
catch (err) {
throw new Errors.ValidationError(err)
}
try {
address = this.normalizeAddress(address)
}
catch (err) {
throw new Error(err)
}
return address
}
/**
*
* @param address
* @returns {*}
*/
normalizeAddress(address){
const ProxyCountryService = this.app.services.ProxyCountryService
const Address = this.app.orm['Address']
const countryNorm = address.country_code || address.country || address.country_name
const provinceNorm = address.province_code || address.province
let normalizedProvince, ext
if (!provinceNorm || !countryNorm) {
// throw new Error(`Unable to normalize ${provinceNorm}, ${countryNorm}`)
return address
}
normalizedProvince = ProxyCountryService.province(countryNorm, provinceNorm)
if (!normalizedProvince) {
throw new Error(`Unable to normalize ${provinceNorm}, ${countryNorm}`)
}
ext = {
country: normalizedProvince.country.name,
country_name: normalizedProvince.country.name,
country_code: normalizedProvince.country.ISO.alpha2,
province: normalizedProvince.name,
province_code: normalizedProvince.code
}
if (address instanceof Address) {
address = address.merge(ext)
}
else {
address = _.merge(address, ext)
}
return address
}
/**
* Converts a currency with a decimal to it's exponent of 0's following the decimal
* @param amount
* @returns number
*/
normalizeCurrency(amount) {
if (!_.isNumber(amount) || _.isNil(amount)) {
throw new Error('Amount must be a number')
}
const amountString = amount.toString().split('.')
if (amountString[1]) {
const exponent = amountString[1].length
amount = amount * Math.pow(10, exponent)
}
return amount
}
/**
*
* @param obj
* @param shippingAddress
* @param options
* @returns {Promise}
*/
// TODO
resolveSendFromTo(obj, shippingAddress, options) {
options = options || {}
return new Promise((resolve, reject) => {
const Cart = this.app.orm['Cart']
const Subscription = this.app.orm['Subscription']
const Customer = this.app.orm['Customer']
const Shop = this.app.orm['Shop']
const Address = this.app.orm['Address']
if (!(obj instanceof Cart) && !(obj instanceof Subscription)) {
const err = new Error('Object must be an instance!')
return reject(err)
}
Shop.findById(obj.shop_id, {
include: [
{
model: Address,
as: 'address'
},
],
transaction: options.transaction || null
})
.then(shop => {
if (!shop && !shop.address) {
return resolve(null)
}
const from = {
name: shop.name,
address_1: shop.address.address_1,
address_2: shop.address.address_2,
address_3: shop.address.address_3,
company: shop.address.company,
city: shop.address.city,
province: shop.address.province,
province_code: shop.address.province_code,
country: shop.address.country,
country_name: shop.address.country_name,
country_code: shop.address.country_code
}
// If provided a shipping address
if (shippingAddress && this.app.services.ProxyCartService.validateAddress(shippingAddress)) {
const res = {
to: shippingAddress,
from: from
}
return resolve(res)
}
else if (obj.shipping_address_id) {
Address.findById(obj.shipping_address_id)
.then(address => {
const to = address.get({plain: true})
const res = {
to: to,
from: from
}
return resolve(res)
})
.catch(err => {
return reject(err)
})
}
else if (obj.customer_id) {
Customer.findById(obj.customer_id, {
attributes: ['id'],
include: [
{
model: Address,
as: 'default_address'
},
{
model: Address,
as: 'shipping_address'
}
],
transaction: options.transaction || null
})
.then(customer => {
if ( customer.shipping_address instanceof Address) {
customer.shipping_address = customer.shipping_address.get({plain: true})
}
if ( customer.default_address instanceof Address) {
customer.default_address = customer.default_address.get({plain: true})
}
const to = customer.shipping_address ? customer.shipping_address : customer.default_address
const res = {
to: to,
from: from
}
return resolve(res)
})
.catch(err => {
return reject(err)
})
}
else {
return resolve(null)
}
})
.catch(err => {
return reject(err)
})
})
}
/**
*
* @param obj
* @param lineItems
* @param shippingAddress
* @param options
* @returns {Promise}
*/
resolveItemsFromTo(obj, lineItems, shippingAddress, options) {
options = options || {}
const Cart = this.app.orm['Cart']
const Order = this.app.orm['Order']
const Subscription = this.app.orm['Subscription']
const Customer = this.app.orm['Customer']
const Shop = this.app.orm['Shop']
const Address = this.app.orm['Address']
if (!(obj instanceof Cart) && !(obj instanceof Subscription) && !(obj instanceof Order)) {
throw new Error('Object must be an instance of a Cart, Subscription or Order!')
}
let nexuses, to
return Promise.resolve()
.then(() => {
// If provided a shipping address
if (shippingAddress && this.app.services.ProxyCartService.validateAddress(shippingAddress)) {
return shippingAddress
}
else if (obj.shipping_address_id) {
return Address.findById(obj.shipping_address_id)
.then(address => {
return address.get({plain: true})
})
}
else if (obj.customer_id) {
return Customer.findById(obj.customer_id, {
attributes: ['id'],
include: [
{
model: Address,
as: 'default_address'
},
{
model: Address,
as: 'shipping_address'
}
],
transaction: options.transaction || null
})
.then(customer => {
if ( customer.shipping_address instanceof Address) {
customer.shipping_address = customer.shipping_address.get({plain: true})
}
if ( customer.default_address instanceof Address) {
customer.default_address = customer.default_address.get({plain: true})
}
return customer.shipping_address ? customer.shipping_address : customer.default_address
})
}
else {
return
}
})
.then(_to => {
if (!_to) {
return
}
else {
to = _to
return Shop.sequelize.Promise.mapSeries(lineItems, item => {
return this.resolveItemNexusTo(item, to, {transaction: options.transaction || null})
})
}
})
.then(_nexuses => {
if (!_nexuses) {
return _nexuses
}
// If nothing came back, but we should attempt to use the default store.
else if (_nexuses.filter(n => n).length === 0) {
return Shop.findById(obj.shop_id, {
attributes: ['id', 'name', 'address_id'],
include: [
{
model: Address,
as: 'address'
},
],
transaction: options.transaction || null
})
.then(_shop => {
if (!_shop && !_shop.address) {
return null
}
_nexuses[0] = {
name: _shop.name,
address_1: _shop.address.address_1,
address_2: _shop.address.address_2,
address_3: _shop.address.address_3,
company: _shop.address.company,
city: _shop.address.city,
province: _shop.address.province,
province_code: _shop.address.province_code,
postal_code: _shop.address.postal_code,
country: _shop.address.country,
country_name: _shop.address.country_name,
country_code: _shop.address.country_code
}
// Filter out all the null nexuses
return _nexuses.filter(n => n)
})
.catch(err => {
this.app.log.error(err)
return
})
}
else {
// Filter out all the null nexuses
return _nexuses.filter(n => n)
}
})
.then(_nexuses => {
if (_nexuses) {
nexuses = _nexuses
}
if (!nexuses || !to) {
return null
}
else {
return {
nexus_addresses: nexuses,
to_address: to
}
}
})
}
/**
* Resolves what shop address an item is shipping from
* @param item
* @param options
* @returns {Promise.<T>}
*/
resolveItemNexusTo(item, to, options) {
options = options || {}
const Shop = this.app.orm['Shop']
const Address = this.app.orm['Address']
if (!item.shop_id) {
return Promise.resolve(null)
}
// Find a nexus from item shop id if present
return Shop.findById(item.shop_id, {
include: [
{
model: Address,
as: 'address'
}
],
transaction: options.transaction || null
})
.then(_shop => {
if (!_shop || !_shop.address) {
return null
}
return {
name: _shop.name,
address_1: _shop.address.address_1,
address_2: _shop.address.address_2,
address_3: _shop.address.address_3,
company: _shop.address.company,
city: _shop.address.city,
province: _shop.address.province,
province_code: _shop.address.province_code,
country: _shop.address.country,
country_name: _shop.address.country_name,
country_code: _shop.address.country_code
}
})
.catch(err => {
return null
})
}
/**
*
* @param shops
* @param address
* @returns {Array|*}
*/
nearestToAddress(shops, address) {
shops = _.map(shops, shop => {
shop.distance = geolib.getDistance(
{
latitude: shop.latitude,
longitude: shop.longitude
},
{
latitude: address.latitude,
longitude: address.longitude
}
)
return shop
})
shops = _.sortBy(shops, 'distance')
return shops
}
/**
*
* @param user
* @param options
* @returns {Promise.<T>}
*/
afterUserCreate(user, options) {
options = options || {}
const Customer = this.app.orm['Customer']
const Cart = this.app.orm['Cart']
// return Promise.resolve(user)
return Customer.resolve({
id: user.current_customer_id,
email: user.email,
phone: user.phone,
first_name: user.first_name,
last_name: user.last_name,
accepts_marketing: user.accepts_marketing,
users: [user]
}, {
transaction: options.transaction || null,
create: true
})
.then(customer => {
if (!customer) {
return {
id: null
}
}
// Set the user's current customer id
user.current_customer_id = customer.id
// Update the customer email address if it doesn't already have one
if (!customer.email && user.email) {
customer.email = user.email
}
return this.app.services.CustomerService.addUser(customer, user, {transaction: options.transaction || null})
.then(() => {
return customer.save({transaction: options.transaction || null})
})
})
.then(customer => {
return Cart.resolve({
id: user.current_cart_id,
customer: customer.id
}, {
transaction: options.transaction || null
})
})
.then(cart => {
// Set the user's current cart id
user.current_cart_id = cart.id
return user.save({
fields: [
'current_cart_id',
'current_customer_id'
],
transaction: options.transaction || null
})
})
}
/**
*
* @param cart
* @param next
*/
serializeCart(cart, next) {
if (typeof next != 'function') {
throw new Error('instance#serializeCart requires a callback function')
}
next(null, cart.id)
}
/**
*
* @param id
* @param options
* @param next
*/
deserializeCart(id, options, next) {
options = options || {}
if (typeof next != 'function') {
throw new Error('instance#deserializeCart requires a callback function')
}
this.app.orm['Cart'].findById(id, {transaction: options.transaction || null})
.then(cart => {
next(null, cart)
})
.catch(err => {
next(err)
})
}
/**
*
* @param customer
* @param next
*/
serializeCustomer(customer, next) {
if (typeof next !== 'function') {
throw new Error('instance#serializeCustomer requires a callback function')
}
next(null, customer.id)
}
/**
*
* @param id
* @param options
* @param next
*/
deserializeCustomer(id, options, next) {
options = options || {}
if (typeof next !== 'function') {
throw new Error('instance#deserializeCustomer requires a callback function')
}
this.app.orm['Customer'].findById(id, {transaction: options.transaction || null})
.then(customer => {
next(null, customer)
})
.catch(err => {
next(err)
})
}
}