Files
PetitTetonMeteor/imports/api/Product.js

309 lines
11 KiB
JavaScript
Raw Normal View History

import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
import {SimpleSchema} from 'meteor/aldeed:simple-schema';
/**
* Notes:
* The Product object has a prices field which is an object whose fields names are Measure ID's. Each field value (for each Measure ID) is an object that has a 'price', 'effectiveDate', and 'previousPrice'.
* The effectiveDate field stores the date as a number in the format YYYYMMDD. Converting this number into a local date is done with moment(sale.date.toString(), "YYYYMMDD").toDate(), and converting it to a number from a date can be accomplished with ~~(moment(date).format("YYYYMMDD")), where the ~~ is a bitwise not and converts a string to a number quickly and reliably.
* Because the structure of the Product object is so complicated, the normal checking that is done by the framework cannot be used.
*
* A product is first deactivated, then hidden.
* A deactivated product is one that is no longer intended to be used (produced), but needs to show up in some lists because there may be some still floating around in inventory (needing to be sold). It should show with a yellow indicator if displayed.
* A product that is hidden is one that exists in the system as a historical artifact due to there still being data attached to it (sales for example). It should not normally show in lists, and should show up with a red indicator if it is displayed.
*/
Products = new Mongo.Collection('Products');
const ProductsSchema = new SimpleSchema({
name: {
type: String,
label: "Name",
optional: false,
trim: true,
index: 1,
unique: true
},
tags: { //An array of ProductTag names. Note that we are not using the ProductTag ID's because I want a looser connection (if a ProductTag is deleted, it isn't a big deal if it isn't maintained in the Product records).
type: [String],
label: "Tags",
optional: false,
defaultValue: []
},
measures: { //A JSON array of Measure ID's.
type: Array,
label: "Measures",
optional: false,
defaultValue: []
},
'measures.$': {
type: String,
label: "Measure ID",
regEx: SimpleSchema.RegEx.Id
},
aliases: { //A JSON array of alternate names.
type: Array,
label: "Aliases",
optional: false,
defaultValue: []
},
'aliases.$': {
type: String
},
prices: { //A JSON object mapping Measure ID's to price data for this item. Price data is an object with price, effectiveDate, previousPrice. Example: prices: {XZ5Z3CM49NDrJNADA: {price: 10.5, effectiveDate: ISODate("2017-01-12T13:14:18.876-08:00"), previousPrice: 9}, ...}
type: Object,
//blackbox: true,
custom: function() {
//console.log("In custom validation for prices");
//return true;
// if(this.value != undefined) {
// console.log(this.value);
// //check(this, Object);
// if(!_.isObject(this.value)) {
// return "expectObject";
// }
//
// for(let measureId of _.allKeys(this.value)) {
// //check(measureId, String); //Should be a Mongo ID
// if(!_.isString(measureId)) {
// console.log("Expected a Mongo ID as attribute names of the Product.prices object.");
// return "expectString";
// }
// //check(this.value[measureId], Object);
// let measureData = this.value[measureId];
// if(!_.isObject(measureData)) {
// console.log("Expected an Object containing price, and optionally (previousPrice & effectiveDate).");
// return "expectObject";
// }
//
// if(_.has(measureData, "price")) {
// //check(measureData.price, Number);
// if(!_.isNumber(measureData.price)) {
// console.log("Expected a Number for 'price'.");
// return "expectNumber";
// }
//
// //If previous price exists then it must be a number, and effective date must exist and be a date.
// if(_.has(measureData, "previousPrice")) {
// //check(measureData.effectiveDate, Date);
// if(!_.isDate(measureData.effectiveDate)) {
// console.log("Expected a Date for 'effectiveDate'.");
// return "expectDate";
// }
// //check(measureData.previousPrice, Number);
// if(!_.isDate(measureData.previousPrice)) {
// console.log("Expected a Number for 'previousPrice'.");
// return "expectNumber";
// }
// }
// else {
// //check(measureData.effectiveDate, undefined);
// if(_.isSet(measureData.effectiveDate)) {
// console.log("Expected 'effectiveDate' to be undefined.");
// return "notAllowed";
// }
// }
// }
// else {
// //check(measureData.effectiveDate, undefined);
// if(_.isSet(measureData.effectiveDate)) {
// console.log("Expected 'effectiveDate' to be undefined.");
// return "notAllowed";
// }
// //check(measureData.previousPrice, undefined);
// if(_.isSet(measureData.previousPrice)) {
// console.log("Expected 'previousPrice' to be undefined.");
// return "notAllowed";
// }
// }
// }
// }
}
},
createdAt: {
type: Date,
label: "Created On",
optional: false
},
updatedAt: {
type: Date,
label: "Updated On",
optional: true
},
deactivated: {
type: Boolean,
label: "Deactivated",
optional: true
},
hidden: {
type: Boolean,
label: "Hidden",
optional: true
}
});
//Note: Could not figure out how to setup a schema for prices where prices is an object whose keys are id's of measures, and whose values are objects containing price data (see comments above for more details on price data).
// 'prices.$': {
// type: new SimpleSchema({
// measureId: {
// type: String,
// label: "Measure Id",
// trim: false,
// regEx: SimpleSchema.RegEx.Id,
// optional: false
// },
// price: {
// type: Number,
// label: "Price",
// min: 0,
// exclusiveMin: true,
// optional: false
// }
// })
// },
//Products.attachSchema(ProductsSchema);
//https://github.com/zimme/meteor-collection-softremovable
// Products.attachBehaviour("softRemovable", {
// removed: 'deleted',
// removedAt: 'deletedAt',
// removedBy: 'removedBy',
// restoredAt: 'restoredAt',
// restoredBy: 'restoredBy'
// });
if(Meteor.isServer) {
Meteor.publish('products', function() {
return Products.find({}, {sort: {name: 1}});
});
Meteor.methods({
createProduct: function(name, tags, aliases, measures) {
check(name, String);
if(tags) check(tags, [String]);
if(aliases) check(aliases, [String]);
if(measures) check(measures, [String]);
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
Products.insert({name, tags, aliases, measures, createdAt: new Date()}, {bypassCollection2: true}, function(err, id) {
if(err) console.log(err);
});
}
else throw new Meteor.Error(403, "Not authorized.");
},
convertProduct: function(productId, alternateProductId) {
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
check(productId, String);
check(alternateProductId, String);
// Replace all sale references to the given ID with the provided alternate product ID.
Meteor.collections.Sales.update({productId: productId}, {$set: {productId: alternateProductId}}, {multi: true});
}
else throw new Meteor.Error(403, "Not authorized.");
},
deactivateProduct: function(id) {
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
//Products.remove(id);
Products.update(id, {$set: {deactivated: true}}, {bypassCollection2: true});
}
else throw new Meteor.Error(403, "Not authorized.");
},
reactivateProduct: function(id) {
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
Products.update(id, {$set: {deactivated: false}}, {bypassCollection2: true});
}
else throw new Meteor.Error(403, "Not authorized.");
},
hideProduct: function(id) { //One step past deactivated - will only show in the products list if hidden products are enabled.
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
//Products.remove(id);
Products.update(id, {$set: {hidden: true}}, {bypassCollection2: true});
}
else throw new Meteor.Error(403, "Not authorized.");
},
showProduct: function(id) { //Returns the product to being simply deactivated. Will again show in lists.
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
Products.update(id, {$set: {hidden: false}}, {bypassCollection2: true});
}
else throw new Meteor.Error(403, "Not authorized.");
},
updateProduct: function(id, name, tags, aliases, measures) {
check(id, String);
check(name, String);
if(tags) check(tags, [String]);
if(aliases) check(aliases, [String]);
if(measures) check(measures, [String]);
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
Products.update(id, {$set: {name: name, tags: tags, aliases: aliases, measures: measures, updatedAt: new Date()}}, {bypassCollection2: true});
}
else throw new Meteor.Error(403, "Not authorized.");
},
clearProductPrice: function(productIds, measureId) {
check(productIds, [String]);
check(measureId, String);
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
let attr = "prices." + measureId;
Products.update({_id: {$in: productIds}}, {$unset: {[attr]: true}}, {validate: false, bypassCollection2: true});
}
else throw new Meteor.Error(403, "Not authorized.");
},
setProductPrice: function(productIds, measureId, price, setPrevious, effectiveDate) {
check(productIds, [String]);
check(measureId, String);
check(price, Number);
if(setPrevious) check(setPrevious, Boolean);
if(effectiveDate) check(effectiveDate, Number); // TODO: Check that the format is YYYYMMDD
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
let products = Products.find({_id: {$in: productIds}}, {fields: {prices: 1}}).fetch();
for(let product of products) {
let prices = product.prices ? product.prices : {};
let measurePriceData = prices[measureId];
if(!measurePriceData) {
measurePriceData = {};
prices[measureId] = measurePriceData;
}
if(setPrevious && measurePriceData.price) {
measurePriceData.previousPrice = measurePriceData.price;
measurePriceData.effectiveDate = effectiveDate;
}
measurePriceData.price = price;
if(ProductsSchema.newContext().isValid()) {
Products.update(product._id, {$set: {prices: prices, updateAt: new Date()}}, {validate: false, bypassCollection2: true});
}
else console.log("Invalid schema for product");
}
}
else throw new Meteor.Error(403, "Not authorized.");
},
tagProducts: function(productIds, tagId) {
//Tags the products if any products don't have the tag, otherwise removes the tag from all products.
check(productIds, [String]);
check(tagId, String);
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
let productsWithTag = Products.find({_id: {$in: productIds}, tags: {$all: [tagId]}}).count();
if(productsWithTag == productIds.length) {
Products.update({_id: {$in: productIds}}, {$pullAll: {tags: [tagId]}}, {bypassCollection2: true, multi: true});
}
else {
Products.update({_id: {$in: productIds}}, {$addToSet: {tags: tagId}}, {bypassCollection2: true, multi: true});
}
}
else throw new Meteor.Error(403, "Not authorized.");
}
});
}
export default Products;