@fabrix/spool-cart
Version:
Spool - eCommerce Spool for Fabrix
920 lines (919 loc) • 31.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("@fabrix/fabrix/dist/common");
const spool_sequelize_1 = require("@fabrix/spool-sequelize");
const errors_1 = require("@fabrix/spool-sequelize/dist/errors");
const lodash_1 = require("lodash");
const shortId = require("shortid");
const queryDefaults_1 = require("../utils/queryDefaults");
const enums_1 = require("../../enums");
const enums_2 = require("../../enums");
class CustomerResolver extends spool_sequelize_1.SequelizeResolver {
findByIdDefault(id, options = {}) {
options = this.app.services.SequelizeService.mergeOptionDefaults(queryDefaults_1.Customer.default(this.app), options);
return this.findById(id, options);
}
findByTokenDefault(token, options = {}) {
options = this.app.services.SequelizeService.mergeOptionDefaults(queryDefaults_1.Customer.default(this.app), options, {
where: {
token: token
}
});
return this.findOne(options);
}
findAndCountDefault(options = {}) {
options = this.app.services.SequelizeService.mergeOptionDefaults(queryDefaults_1.Customer.default(this.app), options || {}, { distinct: true });
return this.findAndCountAll(options);
}
resolveByInstance(cart, options = {}) {
return Promise.resolve(cart);
}
resolveById(cart, options = {}) {
return this.findById(cart.id, options)
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Cart ${cart.id} not found`);
}
return resUser;
});
}
resolveByToken(cart, options = {}) {
return this.findOne(lodash_1.defaultsDeep({
where: {
token: cart.token
}
}, options))
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Cart token ${cart.token} not found`);
}
return resUser;
});
}
resolveByEmail(cart, options = {}) {
return this.findOne(lodash_1.defaultsDeep({
where: {
email: cart.email
}
}, options))
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Cart email ${cart.email} not found`);
}
return resUser;
});
}
resolveByNumber(cart, options = {}) {
return this.findById(cart, options)
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Cart ${cart.token} not found`);
}
return resUser;
});
}
resolveByString(cart, options = {}) {
return this.findOne(lodash_1.defaultsDeep({
where: {
token: cart
}
}, options))
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Cart ${cart} not found`);
}
return resUser;
});
}
resolve(customer, options = {}) {
options.create = options.create || true;
const CustomerModel = this;
if (customer instanceof CustomerModel.instance) {
return Promise.resolve(customer);
}
else if (customer && lodash_1.isObject(customer) && customer.id) {
return CustomerModel.findById(customer.id, options)
.then(resCustomer => {
if (!resCustomer && options.create !== false) {
return this.app.services.CustomerService.create(customer, options);
}
return resCustomer;
});
}
else if (customer && lodash_1.isObject(customer) && customer.email) {
return CustomerModel.findOne(this.app.services.SequelizeService.mergeOptionDefaults(options, {
where: {
email: customer.email
}
}))
.then(resCustomer => {
if (!resCustomer && options.create !== false) {
return this.app.services.CustomerService.create(customer, { transaction: options.transaction || null });
}
return resCustomer;
});
}
else if (customer && lodash_1.isNumber(customer)) {
return CustomerModel.findById(customer, options);
}
else if (customer && lodash_1.isString(customer)) {
return CustomerModel.findOne(this.app.services.SequelizeService.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 this.app.services.CustomerService.create(customer, options);
}
}
}
exports.CustomerResolver = CustomerResolver;
class Customer extends common_1.FabrixModel {
static get resolver() {
return CustomerResolver;
}
static config(app, Sequelize) {
return {
options: {
underscored: true,
enums: {
CUSTOMER_STATE: enums_1.CUSTOMER_STATE
},
scopes: {
live: {
where: {
live_mode: true
}
}
},
hooks: {
beforeCreate: [
(customer, options) => {
if (customer.ip) {
customer.create_ip = customer.ip;
}
if (!customer.token) {
customer.token = `customer_${shortId.generate()}`;
}
}
],
beforeUpdate: [
(customer, options) => {
if (customer.ip) {
customer.update_ip = customer.ip;
}
}
],
afterCreate: [
(customer, options) => {
return app.services.CustomerService.afterCreate(customer, options)
.catch(err => {
return Promise.reject(err);
});
}
],
afterUpdate: [
(customer, options) => {
return app.services.CustomerService.afterUpdate(customer, 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;
}
}
}
}
};
}
static schema(app, Sequelize) {
return {
token: {
type: Sequelize.STRING,
unique: true
},
accepts_marketing: {
type: Sequelize.BOOLEAN,
defaultValue: true
},
first_name: {
type: Sequelize.STRING
},
last_name: {
type: Sequelize.STRING
},
company: {
type: Sequelize.STRING
},
phone: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING,
validate: {
isEmail: true
},
set: function (val) {
return this.setDataValue('email', val ? val.toLowerCase() : null);
}
},
note: {
type: Sequelize.STRING
},
last_order_id: {
type: Sequelize.INTEGER,
},
last_order_name: {
type: Sequelize.STRING
},
state: {
type: Sequelize.ENUM,
values: lodash_1.values(enums_1.CUSTOMER_STATE),
defaultValue: enums_1.CUSTOMER_STATE.ENABLED
},
type: {
type: Sequelize.STRING,
defaultValue: 'default'
},
tax_exempt: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
total_spent: {
type: Sequelize.INTEGER,
defaultValue: 0
},
avg_spent: {
type: Sequelize.INTEGER,
defaultValue: 0
},
currency: {
type: Sequelize.STRING,
defaultValue: app.config.get('cart.default_currency') || enums_2.CUSTOMER_DEFAULTS.CURRENCY
},
total_orders: {
type: Sequelize.INTEGER,
defaultValue: 0
},
account_balance: {
type: Sequelize.INTEGER,
defaultValue: 0
},
verified_email: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
default_address_id: {
type: Sequelize.INTEGER
},
shipping_address_id: {
type: Sequelize.INTEGER
},
billing_address_id: {
type: Sequelize.INTEGER
},
ip: {
type: Sequelize.STRING
},
create_ip: {
type: Sequelize.STRING
},
update_ip: {
type: Sequelize.STRING
},
live_mode: {
type: Sequelize.BOOLEAN,
defaultValue: app.config.get('cart.live_mode')
}
};
}
static 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',
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'
});
}
}
exports.Customer = Customer;
Customer.prototype.getProductHistory = function (product, 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
};
});
};
Customer.prototype.hasPurchaseHistory = function (productId, options = {}) {
return this.app.models['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;
});
};
Customer.prototype.isSubscribed = function (productId, options = {}) {
return this.app.models['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;
});
};
Customer.prototype.getSalutation = function (options = {}) {
let salutation = 'Customer';
if (this.full_name) {
salutation = this.full_name;
}
else if (this.email) {
salutation = this.email;
}
return salutation;
};
Customer.prototype.getDefaultSource = function (options = {}) {
const Source = this.app.models['Source'];
return Source.findOne({
where: {
customer_id: this.id,
is_default: true
},
transaction: options.transaction || null
})
.then(source => {
if (!source) {
return Source.findOne({
where: {
customer_id: this.id
},
transaction: options.transaction || null
});
}
else {
return source;
}
})
.then(source => {
return source;
});
};
Customer.prototype.setLastOrder = function (order) {
this.last_order_name = order.name;
this.last_order_id = order.id;
return this;
};
Customer.prototype.setTotalSpent = function (orderTotalDue) {
this.total_spent = this.total_spent + orderTotalDue;
return this;
};
Customer.prototype.setTotalOrders = function () {
this.total_orders = this.total_orders + 1;
return this;
};
Customer.prototype.setAvgSpent = function () {
this.avg_spent = this.total_spent / this.total_orders;
return this;
};
Customer.prototype.setAccountBalance = function (newBalance) {
this.account_balance = newBalance;
return this;
};
Customer.prototype.logAccountBalance = function (type = 'debit', price = 0, currency = 'USD', accountId, orderId, options = {}) {
return this.createAccount_event({
type: type,
price: price,
account_id: accountId,
order_id: orderId
}, {
transaction: options.transaction || null
})
.then(_event => {
const currencySymbol = this.app.services.ProxyCartService.formatCurrency(price, currency);
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 ${currencySymbol} ${currency}`,
data: this
};
return this.app.services.EventsService.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);
});
};
Customer.prototype.notifyUsers = function (preNotification, 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 this.app.services.NotificationService.create(preNotification, this.users, { transaction: options.transaction || null })
.then(notes => {
this.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 [];
}
});
};
Customer.prototype.resolveCollections = function (options = {}) {
if (this.collections
&& this.collections.length > 0
&& this.collections.every(d => d instanceof this.app.models['Collection'].instance)
&& 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;
});
}
};
Customer.prototype.resolveDiscounts = function (options = {}) {
if (this.discounts
&& this.discounts.length > 0
&& this.discounts.every(d => d instanceof this.app.models['Discount'].instance)
&& 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;
});
}
};
Customer.prototype.resolveMetadata = function (options = {}) {
if (this.metadata
&& this.metadata instanceof this.app.models['Metadata'].instance
&& 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;
});
}
};
Customer.prototype.resolveUsers = function (options = {}) {
if (this.users
&& this.users.length > 0
&& this.users.every(u => u instanceof this.app.models['User'].instance)
&& 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;
});
}
};
Customer.prototype.resolveDefaultAddress = function (options = {}) {
if (this.default_address
&& this.default_address instanceof this.app.models['Address'].instance
&& options.reload !== true) {
return Promise.resolve(this);
}
else if (!this.default_address_id) {
this.default_address = this.app.models['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;
});
}
};
Customer.prototype.resolveShippingAddress = function (options = {}) {
if (this.shipping_address
&& this.shipping_address instanceof this.app.models['Address'].instance
&& options.reload !== true) {
return Promise.resolve(this);
}
else if (!this.shipping_address_id) {
this.shipping_address = this.app.models['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;
});
}
};
Customer.prototype.resolveBillingAddress = function (options = {}) {
if (this.billing_address
&& this.billing_address instanceof this.app.models['Address'].instance
&& options.reload !== true) {
return Promise.resolve(this);
}
else if (!this.billing_address_id) {
this.billing_address = this.app.models['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;
});
}
};
Customer.prototype.resolvePaymentDetailsToSources = (options = {}) => {
return this;
};
Customer.prototype.sendRetargetEmail = function (options = {}) {
return this.app.emails.Customer.retarget(this, {
send_email: this.app.config.get('cart.emails.customerRetarget')
}, {
transaction: options.transaction || null
})
.then(email => {
return this.notifyUsers(email, { transaction: options.transaction || null });
})
.catch(err => {
this.app.log.error(err);
return;
});
};
Customer.prototype.updateDefaultAddress = function (address, options = {}) {
const Address = this.app.models['Address'];
const defaultUpdate = Address.cleanAddress(address);
return this.resolveDefaultAddress({ transaction: options.transaction || null })
.then(() => {
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;
});
};
Customer.prototype.updateShippingAddress = function (address, options = {}) {
const Address = this.app.models['Address'];
const shippingUpdate = Address.cleanAddress(address);
return this.resolveShippingAddress({ transaction: options.transaction || null })
.then(() => {
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;
});
};
Customer.prototype.updateBillingAddress = function (address, options = {}) {
const Address = this.app.models['Address'];
const billingUpdate = Address.cleanAddress(address);
return this.resolveBillingAddress({ transaction: options.transaction || null })
.then(() => {
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;
});
};
Customer.prototype.toJSON = function () {
const resp = this instanceof this.app.models['Customer'].instance ? this.get({ plain: true }) : this;
if (resp.tags) {
resp.tags = resp.tags.map(tag => {
if (lodash_1.isString(tag)) {
return tag;
}
return tag.name;
});
}
else {
resp.tags = [];
}
if (resp.metadata) {
if (typeof resp.metadata.data !== 'undefined') {
resp.metadata = resp.metadata.data;
}
}
else {
resp.metadata = {};
}
return resp;
};