@fabrix/spool-cart
Version:
Spool - eCommerce Spool for Fabrix
570 lines (569 loc) • 20.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("@fabrix/fabrix/dist/common");
const errors_1 = require("@fabrix/spool-sequelize/dist/errors");
const spool_sequelize_1 = require("@fabrix/spool-sequelize");
const lodash_1 = require("lodash");
const enums_1 = require("../../enums");
const enums_2 = require("../../enums");
const enums_3 = require("../../enums");
class DiscountUploadResolver extends spool_sequelize_1.SequelizeResolver {
batch(options, batch) {
const self = this;
options.limit = options.limit || 100;
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(batched => {
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);
}
resolveByInstance(discount, options = {}) {
return Promise.resolve(discount);
}
resolveById(discount, options = {}) {
return this.findById(discount.id, options)
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Discount ${discount.id} not found`);
}
return resUser;
});
}
resolveByToken(discount, options = {}) {
return this.findOne(this.app.services.SequelizeService.mergeOptionDefaults(options, {
where: {
token: discount.token
}
}))
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Discount token ${discount.token} not found`);
}
return resUser;
});
}
resolveByCode(discount, options = {}) {
return this.findOne(this.app.services.SequelizeService.mergeOptionDefaults(options, {
where: {
code: discount.code
}
}))
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Discount code ${discount.code} not found`);
}
return resUser;
});
}
resolveByNumber(discount, options = {}) {
return this.findById(discount, options)
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Discount ${discount.token} not found`);
}
return resUser;
});
}
resolveByString(discount, options = {}) {
return this.findOne(this.app.services.SequelizeService.mergeOptionDefaults(options, {
where: {
code: discount
}
}))
.then(resUser => {
if (!resUser && options.reject !== false) {
throw new errors_1.ModelError('E_NOT_FOUND', `Discount ${discount} not found`);
}
return resUser;
});
}
resolve(discount, options = {}) {
const resolvers = {
'instance': discount instanceof this.instance,
'id': !!(discount && lodash_1.isObject(discount) && discount.id),
'token': !!(discount && lodash_1.isObject(discount) && discount.token),
'code': !!(discount && lodash_1.isObject(discount) && discount.code),
'number': !!(discount && lodash_1.isNumber(discount)),
'string': !!(discount && lodash_1.isString(discount))
};
const type = Object.keys(resolvers).find((key) => resolvers[key]);
switch (type) {
case 'instance': {
return this.resolveByInstance(discount, options);
}
case 'id': {
return this.resolveById(discount, options);
}
case 'token': {
return this.resolveByToken(discount, options);
}
case 'code': {
return this.resolveByCode(discount, options);
}
case 'number': {
return this.resolveByNumber(discount, options);
}
case 'string': {
return this.resolveByString(discount, options);
}
default: {
const err = new Error(`Unable to resolve Discount ${discount}`);
return Promise.reject(err);
}
}
}
transformDiscounts(discounts = [], options = {}) {
const DiscountModel = this.app.models['Discount'];
const Sequelize = DiscountModel.sequelize;
discounts = discounts.map(discount => {
if (discount && lodash_1.isNumber(discount)) {
return { id: discount };
}
else if (discount && lodash_1.isString(discount)) {
return {
handle: this.app.services.ProxyCartService.handle(discount),
name: discount
};
}
else if (discount && lodash_1.isObject(discount) && (discount.name || discount.handle)) {
discount.handle = this.app.services.ProxyCartService.handle(discount.handle)
|| this.app.services.ProxyCartService.handle(discount.name);
return discount;
}
});
discounts = discounts.filter(discount => discount);
return Sequelize.Promise.mapSeries(discounts, discount => {
return DiscountModel.findOne({
where: lodash_1.pick(discount, ['id', 'handle']),
attributes: ['id', 'handle', 'name'],
transaction: options.transaction || null
})
.then(_discount => {
if (_discount) {
return lodash_1.extend(_discount, discount);
}
else {
return this.app.services.DiscountService.create(discount, {
transaction: options.transaction || null
});
}
});
});
}
}
exports.DiscountUploadResolver = DiscountUploadResolver;
class Discount extends common_1.FabrixModel {
static get resolver() {
return DiscountUploadResolver;
}
static config(app, Sequelize) {
return {
options: {
underscored: true,
enums: {
DISCOUNT_TYPES: enums_1.DISCOUNT_TYPES,
DISCOUNT_STATUS: enums_2.DISCOUNT_STATUS,
DISCOUNT_SCOPE: enums_3.DISCOUNT_SCOPE
},
scopes: {
live: {
where: {
live_mode: true
}
},
expired: () => {
return {
where: {
ends_at: {
$gte: new Date()
}
}
};
},
active: () => {
return {
where: {
status: enums_2.DISCOUNT_STATUS.ENABLED,
starts_at: {
$gte: new Date()
},
ends_at: {
$lte: new Date()
}
}
};
}
},
hooks: {
beforeValidate: [
(discount, options) => {
if (!discount.handle && discount.name) {
discount.handle = discount.name;
}
}
],
beforeCreate: [
(discount, options) => {
if (discount.body) {
const bodyDoc = app.services.RenderGenericService.renderSync(discount.body);
discount.body_html = bodyDoc.document;
}
}
],
beforeUpdate: [
(discount, options) => {
if (discount.body) {
const bodyDoc = app.services.RenderGenericService.renderSync(discount.body);
discount.body_html = bodyDoc.document;
}
}
]
}
}
};
}
static schema(app, Sequelize) {
return {
handle: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
set: function (val) {
this.setDataValue('handle', app.services.ProxyCartService.splitHandle(val) || null);
}
},
name: {
type: Sequelize.STRING
},
description: {
type: Sequelize.TEXT
},
body: {
type: Sequelize.TEXT
},
body_html: {
type: Sequelize.TEXT
},
code: {
type: Sequelize.STRING,
notNull: true
},
discount_scope: {
type: Sequelize.ENUM,
values: lodash_1.values(enums_3.DISCOUNT_SCOPE),
defaultValue: enums_3.DISCOUNT_SCOPE.INDIVIDUAL
},
discount_type: {
type: Sequelize.ENUM,
values: lodash_1.values(enums_1.DISCOUNT_TYPES),
defaultValue: enums_1.DISCOUNT_TYPES.RATE
},
discount_rate: {
type: Sequelize.INTEGER
},
discount_threshold: {
type: Sequelize.INTEGER
},
discount_percentage: {
type: Sequelize.FLOAT,
defaultValue: 0.0
},
discount_shipping: {
type: Sequelize.FLOAT,
defaultValue: 0.0
},
discount_product_include: {
type: Sequelize.JSONB,
defaultValue: []
},
discount_product_exclude: {
type: Sequelize.JSONB,
defaultValue: []
},
discount_customer_include: {
type: Sequelize.JSONB,
defaultValue: []
},
discount_customer_exclude: {
type: Sequelize.JSONB,
defaultValue: []
},
shipping_product_exclude: {
type: Sequelize.JSONB,
defaultValue: []
},
tax_product_exclude: {
type: Sequelize.JSONB,
defaultValue: []
},
ends_at: {
type: Sequelize.DATE
},
starts_at: {
type: Sequelize.DATE
},
status: {
type: Sequelize.ENUM,
values: lodash_1.values(enums_2.DISCOUNT_STATUS),
defaultValue: enums_2.DISCOUNT_STATUS.ENABLED
},
minimum_order_amount: {
type: Sequelize.INTEGER,
defaultValue: 0
},
usage_limit: {
type: Sequelize.INTEGER,
defaultValue: 0
},
times_used: {
type: Sequelize.INTEGER,
defaultValue: 0
},
applies_once: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
applies_once_per_customer: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
applies_compound: {
type: Sequelize.BOOLEAN,
defaultValue: true
},
live_mode: {
type: Sequelize.BOOLEAN,
defaultValue: app.config.get('cart.live_mode')
}
};
}
static associate(models) {
models.Discount.belongsToMany(models.Order, {
as: 'orders',
through: {
model: models.ItemDiscount,
unique: false,
scope: {
model: 'order'
}
},
foreignKey: 'discount_id',
constraints: false
});
models.Discount.belongsToMany(models.Cart, {
as: 'carts',
through: {
model: models.ItemDiscount,
unique: false,
scope: {
model: 'cart'
}
},
foreignKey: 'discount_id',
constraints: false
});
models.Discount.belongsToMany(models.Product, {
as: 'products',
through: {
model: models.ItemDiscount,
unique: false,
scope: {
model: 'product'
}
},
foreignKey: 'discount_id',
constraints: false
});
models.Discount.belongsToMany(models.ProductVariant, {
as: 'variants',
through: {
model: models.ItemDiscount,
unique: false,
scope: {
model: 'productvariant'
}
},
foreignKey: 'discount_id',
constraints: false
});
models.Discount.belongsToMany(models.Customer, {
as: 'customers',
through: {
model: models.ItemDiscount,
unique: false,
scope: {
model: 'customer'
}
},
foreignKey: 'discount_id',
constraints: false
});
models.Discount.belongsToMany(models.Collection, {
as: 'collections',
through: {
model: models.ItemDiscount,
unique: false,
scope: {
model: 'collection'
}
},
foreignKey: 'discount_id',
constraints: false
});
models.Discount.hasMany(models.DiscountEvent, {
as: 'discount_events',
foreignKey: 'discount_id'
});
}
}
exports.Discount = Discount;
Discount.prototype.start = function () {
this.status = enums_2.DISCOUNT_STATUS.ENABLED;
return this;
};
Discount.prototype.stop = function () {
this.status = enums_2.DISCOUNT_STATUS.DISABLED;
return this;
};
Discount.prototype.depleted = function () {
this.status = enums_2.DISCOUNT_STATUS.DEPLETED;
return this;
};
Discount.prototype.logUsage = function (orderId, customerId, price, options) {
this.times_used++;
if (this.usage_limit > 0 && this.times_used >= this.usage_limit) {
this.depleted();
}
return this.createDiscount_event({
customer_id: customerId,
order_id: orderId,
price: price
}, {
transaction: options.transaction || null
})
.then(() => {
return this.save({ transaction: options.transaction || null });
});
};
Discount.prototype.eligibleCustomer = function (customerId, options = {}) {
return this.getDiscount_events({
where: {
customer_id: customerId
},
limit: 1,
attributes: ['id', 'discount_id'],
transaction: options.transaction || null
})
.then(_previousUsages => {
_previousUsages = _previousUsages || [];
if (this.applies_once_per_customer && _previousUsages.length > 0) {
return this;
}
else {
return true;
}
})
.catch(err => {
this.app.log.error(err);
return;
});
};
Discount.prototype.discountItem = function (item, criteria = []) {
item.discounted_lines = item.discounted_lines || [];
item.shipping_lines = item.shipping_lines || [];
item.calculated_price = item.calculated_price || item.price;
item.total_discounts = item.total_discounts || 0;
const discountedLine = {
id: this.id,
model: 'discount',
type: null,
name: this.name,
scope: this.discount_scope,
price: 0,
applies: false,
rules: {
start: this.starts_at,
end: this.ends_at,
applies_once: this.applies_once,
applies_once_per_customer: this.applies_once_per_customer,
applies_compound: this.applies_compound,
minimum_order_amount: this.minimum_order_amount
}
};
let totalDeducted = 0;
if (this.status !== enums_2.DISCOUNT_STATUS.ENABLED) {
return item;
}
if (this.usage_limit > 0 && this.times_used > this.usage_limit) {
return item;
}
if (this.discount_product_exclude.length > 0
&& this.discount_product_exclude.indexOf(item.type) > -1) {
return item;
}
if (this.discount_product_include.length > 0
&& this.discount_product_include.indexOf(item.type) === -1) {
return item;
}
if (item.discounted_lines && item.discounted_lines.some(discount => discount.id === this.id)) {
return item;
}
if (this.discount_scope === enums_3.DISCOUNT_SCOPE.INDIVIDUAL) {
const criteriaPair = criteria.find(d => d.discount === this.id);
if (!criteriaPair) {
return item;
}
else if (item.product_id
&& criteriaPair['product']
&& criteriaPair['product'].indexOf(item.product_id) === -1) {
return item;
}
else if (item.variant_id
&& criteriaPair['productvariant']
&& criteriaPair['productvariant'].indexOf(item.variant_id) === -1) {
return item;
}
}
if (this.discount_type === enums_1.DISCOUNT_TYPES.RATE) {
discountedLine.rate = this.discount_rate;
discountedLine.type = enums_1.DISCOUNT_TYPES.RATE;
discountedLine.price = discountedLine.rate;
}
else if (this.discount_type === enums_1.DISCOUNT_TYPES.THRESHOLD) {
discountedLine.threshold = this.discount_threshold;
discountedLine.type = enums_1.DISCOUNT_TYPES.THRESHOLD;
discountedLine.price = discountedLine.threshold;
}
else if (this.discount_type === enums_1.DISCOUNT_TYPES.PERCENTAGE) {
discountedLine.percentage = this.discount_percentage;
discountedLine.type = enums_1.DISCOUNT_TYPES.PERCENTAGE;
discountedLine.price = Math.round((item.price * (discountedLine.percentage / 100)));
}
else if (this.discount_type === enums_1.DISCOUNT_TYPES.SHIPPING) {
return item;
}
totalDeducted = Math.min(item.price, (item.price - (item.price - discountedLine.price)));
if (totalDeducted > 0) {
if (discountedLine.type === enums_1.DISCOUNT_TYPES.THRESHOLD) {
this.discount_threshold = Math.max(0, this.discount_threshold - totalDeducted);
}
discountedLine.price = totalDeducted;
item.discounted_lines.push(discountedLine);
}
return item;
};