/**
* The Transaction module
* a base class for Reservations and Orders
* Share similar manipulating of: status, dates, items, contacts, locations, comments, attachments
* @module transaction
* @implements Base
* @copyright CHECKROOM NV 2015
*/
define([
"jquery",
"api",
"base",
"location",
"dateHelper",
"helper"], /** @lends Base */ function ($, api, Base, Location, DateHelper, Helper) {
var DEFAULTS = {
status: "creating",
from: null,
to: null,
due: null,
contact: null,
location: null,
number: "",
items: [],
conflicts: [],
by: null,
archived: null,
itemSummary: null,
name: null
};
// Allow overriding the ctor during inheritance
// http://stackoverflow.com/questions/4152931/javascript-inheritance-call-super-constructor-or-use-prototype-chain
var tmp = function(){};
tmp.prototype = Base.prototype;
/**
* @name Transaction
* @class Transaction
* @constructor
* @extends Base
* @property {boolean} autoCleanup - Automatically cleanup the transaction if it becomes empty?
* @property {DateHelper} dateHelper - A DateHelper object ref
* @property {string} status - The transaction status
* @property {moment} from - The transaction from date
* @property {moment} to - The transaction to date
* @property {moment} due - The transaction due date
* @property {string} number - The booking number
* @property {string} contact - The Contact.id for this transaction
* @property {string} location - The Location.id for this transaction
* @property {Array} items - A list of Item.id strings
* @property {Array} conflicts - A list of conflict hashes
*/
var Transaction = function(opt) {
var spec = $.extend({}, opt);
Base.call(this, spec);
this.dsItems = spec.dsItems; // we'll also access the /items collection
// should we automatically delete the transaction from the database?
this.autoCleanup = (spec.autoCleanup!=null) ? spec.autoCleanup : false;
this.dateHelper = spec.dateHelper || new DateHelper();
this.helper = spec.helper || new Helper();
this.status = spec.status || DEFAULTS.status; // the status of the order or reservation
this.from = spec.from || DEFAULTS.from; // a date in the future
this.to = spec.to || DEFAULTS.to; // a date in the future
this.due = spec.due || DEFAULTS.due; // a date even further in the future, we suggest some standard avg durations
this.number = spec.number || DEFAULTS.number; // a booking number
this.contact = spec.contact || DEFAULTS.contact; // a contact id
this.location = spec.location || DEFAULTS.location; // a location id
this.items = spec.items || DEFAULTS.items.slice(); // an array of item ids
this.conflicts = spec.conflicts || DEFAULTS.conflicts.slice(); // an array of Conflict objects
this.by = spec.by || DEFAULTS.by;
this.itemSummary = spec.itemSummary || DEFAULTS.itemSummary;
this.name = spec.name || DEFAULTS.name;
};
Transaction.prototype = new tmp();
Transaction.prototype.constructor = Base;
//
// Date helpers (possibly overwritten)
//
/**
* Gets the now time
* @returns {Moment}
*/
Transaction.prototype.getNow = function() {
return this._getDateHelper().getNow();
};
/**
* Gets the now time rounded
* @returns {Moment}
*/
Transaction.prototype.getNowRounded = function() {
return this._getDateHelper().roundTimeFrom(this.getNow());
};
/**
* Gets the next time slot after a date, by default after now
* @returns {Moment}
*/
Transaction.prototype.getNextTimeSlot = function(d) {
d = d || this.getNowRounded();
var next = moment(d).add(this._getDateHelper().roundMinutes, "minutes");
if (next.isSame(d)) {
next = next.add(this._getDateHelper().roundMinutes, "minutes");
}
return next
};
/**
* Gets the lowest possible from date, by default now
* @method
* @name Transaction#getMinDateFrom
* @returns {Moment}
*/
Transaction.prototype.getMinDateFrom = function() {
return this.getMinDate();
};
/**
* Gets the highest possible from date, by default years from now
* @method
* @name Transaction#getMaxDateFrom
* @returns {Moment}
*/
Transaction.prototype.getMaxDateFrom = function() {
return this.getMaxDate();
};
/**
* Gets the lowest possible to date, by default from +1 timeslot
* @method
* @name Transaction#getMinDateTo
* @returns {Moment}
*/
Transaction.prototype.getMinDateTo = function() {
// to can only be one timeslot after the min from date
return this.getNextTimeSlot(this.getMinDateFrom());
};
/**
* Gets the highest possible to date, by default years from now
* @method
* @name Transaction#getMaxDateTo
* @returns {Moment}
*/
Transaction.prototype.getMaxDateTo = function() {
return this.getMaxDate();
};
/**
* Gets the lowest possible due date, by default same as getMinDateTo
* @method
* @name Transaction#getMinDateDue
* @returns {Moment}
*/
Transaction.prototype.getMinDateDue = function() {
return this.getMinDateTo();
};
/**
* Gets the highest possible due date, by default same as getMaxDateDue
* @method
* @name Transaction#getMaxDateDue
* @returns {Moment}
*/
Transaction.prototype.getMaxDateDue = function() {
return this.getMaxDateTo();
};
/**
* DEPRECATED
* Gets the lowest possible date to start this transaction
* @method
* @name Transaction#getMinDate
* @returns {Moment} min date
*/
Transaction.prototype.getMinDate = function() {
return this.getNow();
};
/**
* DEPRECATED
* Gets the latest possible date to end this transaction
* @method
* @name Transaction#getMaxDate
* @returns {Moment} max date
*/
Transaction.prototype.getMaxDate = function() {
var dateHelper = this._getDateHelper();
var now = dateHelper.getNow();
var next = dateHelper.roundTimeTo(now);
return next.add(2, "years");
};
/**
* suggestEndDate, makes a new moment() object with a suggested end date,
* already rounded up according to the group.profile settings
* @method suggestEndDate
* @name Transaction#suggestEndDate
* @param {Moment} m a suggested end date for this transaction
* @returns {*}
*/
Transaction.prototype.suggestEndDate = function(m) {
var dateHelper = this._getDateHelper();
var end = dateHelper.addAverageDuration(m || dateHelper.getNow());
return dateHelper.roundTimeTo(end);
};
//
// Base overrides
//
/**
* Checks if the transaction is empty
* @method isEmpty
* @name Transaction#isEmpty
* @returns {boolean}
*/
Transaction.prototype.isEmpty = function() {
return (
(Base.prototype.isEmpty.call(this)) &&
(this.status==DEFAULTS.status) &&
(this.crtype == "cheqroom.types.order"?true:this.from==DEFAULTS.from) &&
(this.to==DEFAULTS.to) &&
(this.due==DEFAULTS.due) &&
(this.number==DEFAULTS.number) &&
(this.contact==DEFAULTS.contact) &&
(this.location==DEFAULTS.location) &&
(this.items.length==0) // not DEFAULTS.items? :)
);
};
/**
* Checks if the transaction is dirty and needs saving
* @method
* @name Transaction#isDirty
* @returns {boolean}
*/
Transaction.prototype.isDirty = function() {
return (
Base.prototype.isDirty.call(this) ||
this._isDirtyBasic() ||
this._isDirtyDates() ||
this._isDirtyLocation() ||
this._isDirtyContact() ||
this._isDirtyItems()
);
};
Transaction.prototype._isDirtyBasic = function() {
if (this.raw) {
var status = this.raw.status || DEFAULTS.status;
return (this.status!=status);
} else {
return false;
}
};
Transaction.prototype._isDirtyDates = function() {
if (this.raw) {
var from = this.raw.from || DEFAULTS.from;
var to = this.raw.to || DEFAULTS.to;
var due = this.raw.due || DEFAULTS.due;
return (
(this.from!=from) ||
(this.to!=to) ||
(this.due!=due));
} else {
return false;
}
};
Transaction.prototype._isDirtyLocation = function() {
if (this.raw) {
var location = DEFAULTS.location;
if (this.raw.location) {
location = (this.raw.location._id) ? this.raw.location._id : this.raw.location;
}
return (this.location!=location);
} else {
return false;
}
};
Transaction.prototype._isDirtyContact = function() {
if (this.raw) {
var contact = DEFAULTS.contact;
if (this.raw.customer) {
contact = (this.raw.customer._id) ? this.raw.customer._id : this.raw.customer;
}
return (this.contact!=contact);
} else {
return false;
}
};
Transaction.prototype._isDirtyItems = function() {
if (this.raw) {
var items = DEFAULTS.items.slice();
if (this.raw.items) {
// TODO!!
}
return false;
} else {
return false;
}
};
Transaction.prototype._getDefaults = function() {
return DEFAULTS;
};
/**
* Writes out some shared fields for all transactions
* Inheriting classes will probably add more to this
* @param options
* @returns {object}
* @private
*/
Transaction.prototype._toJson = function(options) {
var data = Base.prototype._toJson.call(this, options);
//data.started = this.from; // VT: Will be set during checkout
//data.finished = this.to; // VT: Will be set during final checkin
data.due = this.due;
if (this.location) {
// Make sure we send the location as id, not the entire object
data.location = this._getId(this.location);
}
if (this.contact) {
// Make sure we send the contact as id, not the entire object
// VT: It's still called the "customer" field on the backend!
data.customer = this._getId(this.contact);
}
return data;
};
/**
* Reads the transaction from a json object
* @param data
* @param options
* @returns {promise}
* @private
*/
Transaction.prototype._fromJson = function(data, options) {
var that = this;
return Base.prototype._fromJson.call(this, data, options)
.then(function() {
that.cover = null; // don't read cover property for Transactions
that.status = data.status || DEFAULTS.status;
that.number = data.number || DEFAULTS.number;
that.location = data.location || DEFAULTS.location;
that.contact = data.customer || DEFAULTS.contact;
that.items = data.items || DEFAULTS.items.slice();
that.by = data.by || DEFAULTS.by;
that.archived = data.archived || DEFAULTS.archived;
that.itemSummary = data.itemSummary || DEFAULTS.itemSummary;
that.name = data.name || DEFAULTS.name;
return that._getConflicts()
.then(function(conflicts) {
that.conflicts = conflicts;
});
});
};
Transaction.prototype._toLog = function(options) {
var obj = this._toJson(options);
obj.minDateFrom = this.getMinDateFrom().toJSONDate();
obj.maxDateFrom = this.getMaxDateFrom().toJSONDate();
obj.minDateDue = this.getMinDateDue().toJSONDate();
obj.maxDateDue = this.getMaxDateDue().toJSONDate();
obj.minDateTo = this.getMinDateTo().toJSONDate();
obj.maxDateTo = this.getMaxDateTo().toJSONDate();
console.log(obj);
};
Transaction.prototype._checkFromDateBetweenMinMax = function(d) {
return this._checkDateBetweenMinMax(d, this.getMinDateFrom(), this.getMaxDateFrom());
};
Transaction.prototype._checkDueDateBetweenMinMax = function(d) {
return this._checkDateBetweenMinMax(d, this.getMinDateDue(), this.getMaxDateDue());
};
Transaction.prototype._checkToDateBetweenMinMax = function(d) {
return this._checkDateBetweenMinMax(d, this.getMinDateTo(), this.getMaxDateTo());
};
Transaction.prototype._getUniqueItemIds = function(ids){
ids = ids || [];
//https://stackoverflow.com/questions/38373364/the-best-way-to-remove-duplicate-strings-in-an-array
return ids.reduce(function(p,c,i,a){
if (p.indexOf(c) == -1) p.push(c);
return p;
}, []);
}
// Setters
// ----
// From date setters
/**
* Clear the transaction from date
* @method
* @name Transaction#clearFromDate
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.clearFromDate = function(skipRead) {
this.from = DEFAULTS.from;
return this._handleTransaction(skipRead);
};
/**
* Sets the transaction from date
* @method
* @name Transaction#setFromDate
* @param date
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.setFromDate = function(date, skipRead) {
this.from = this._getDateHelper().roundTimeFrom(date);
return this._handleTransaction(skipRead);
};
// To date setters
/**
* Clear the transaction to date
* @method
* @name Transaction#clearToDate
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.clearToDate = function(skipRead) {
this.to = DEFAULTS.to;
return this._handleTransaction(skipRead);
};
/**
* Sets the transaction to date
* @method
* @name Transaction#setToDate
* @param date
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.setToDate = function(date, skipRead) {
this.to = this._getDateHelper().roundTimeTo(date);
return this._handleTransaction(skipRead);
};
// Due date setters
/**
* Clear the transaction due date
* @method
* @name Transaction#clearDueDate
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.clearDueDate = function(skipRead) {
this.due = DEFAULTS.due;
return this._handleTransaction(skipRead);
};
/**
* Set the transaction due date
* @method
* @name Transaction#setDueDate
* @param date
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.setDueDate = function(date, skipRead) {
this.due = this._getDateHelper().roundTimeTo(date);
return this._handleTransaction(skipRead);
};
// Location setters
/**
* Sets the location for this transaction
* @method
* @name Transaction#setLocation
* @param locationId
* @param skipRead skip parsing the returned json response into the transaction
* @returns {promise}
*/
Transaction.prototype.setLocation = function(locationId, skipRead) {
this.location = locationId;
if (this.existsInDb()) {
return this._doApiCall({method: 'setLocation', params: {location: locationId}, skipRead: skipRead});
} else {
return this._createTransaction(skipRead);
}
};
/**
* Clears the location for this transaction
* @method
* @name Transaction#clearLocation
* @param skipRead skip parsing the returned json response into the transaction
* @returns {promise}
*/
Transaction.prototype.clearLocation = function(skipRead) {
var that = this;
this.location = DEFAULTS.location;
return this._doApiCall({method: 'clearLocation', skipRead: skipRead})
.then(function() {
return that._ensureTransactionDeleted();
});
};
// Contact setters
/**
* Sets the contact for this transaction
* @method
* @name Transaction#setContact
* @param contactId
* @param skipRead skip parsing the returned json response into the transaction
* @returns {promise}
*/
Transaction.prototype.setContact = function(contactId, skipRead) {
this.contact = contactId;
if (this.existsInDb()) {
return this._doApiCall({method: 'setCustomer', params: {customer: contactId}, skipRead: skipRead});
} else {
return this._createTransaction(skipRead);
}
};
/**
* Clears the contact for this transaction
* @method
* @name Transaction#clearContact
* @param skipRead skip parsing the returned json response into the transaction
* @returns {promise}
*/
Transaction.prototype.clearContact = function(skipRead) {
var that = this;
this.contact = DEFAULTS.contact;
return this._doApiCall({method: 'clearCustomer', skipRead: skipRead})
.then(function() {
return that._ensureTransactionDeleted();
});
};
/**
* Sets transaction name
* @method
* @name Transaction#setName
* @param name
* @param skipRead skip parsing the returned json response into the transaction
* @returns {promise}
*/
Transaction.prototype.setName = function(name, skipRead){
return this._doApiCall({method: 'setName', params: { name: name }, skipRead: skipRead});
};
/**
* Clears transaction name
* @method
* @name Transaction#clearName
* @param skipRead skip parsing the returned json response into the transaction
* @returns {promise}
*/
Transaction.prototype.clearName = function(skipRead){
return this._doApiCall({method: 'clearName', skipRead: skipRead});
};
// Business logic
// ----
// Inheriting classes will use the setter functions below to update the object in memory
// the _handleTransaction will create, update or delete the actual document via the API
/**
* addItems; adds a bunch of Items to the transaction using a list of item ids
* It creates the transaction if it doesn't exist yet
* @name Transaction#addItems
* @method
* @param items
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.addItems = function(items, skipRead) {
var that = this;
//Remove duplicate item ids
items = that._getUniqueItemIds(items);
return this._ensureTransactionExists(skipRead)
.then(function() {
return that._doApiCall({
method: 'addItems',
params: {items: items },
skipRead: skipRead
});
});
};
/**
* removeItems; removes a bunch of Items from the transaction using a list of item ids
* It deletes the transaction if it's empty afterwards and autoCleanup is true
* @name Transaction#removeItems
* @method
* @param items
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.removeItems = function(items, skipRead) {
var that = this;
if (!this.existsInDb()) {
return $.Deferred().reject(new Error("Cannot removeItems from document without id"));
}
//Remove duplicate item ids
items = that._getUniqueItemIds(items);
return this._doApiCall({
method: 'removeItems',
params: {items: items},
skipRead: skipRead
})
.then(function(data) {
return that._ensureTransactionDeleted().then(function(){
return data;
});
});
};
/**
* clearItems; removes all Items from the transaction
* It deletes the transaction if it's empty afterwards and autoCleanup is true
* @name Transaction#clearItems
* @method
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.clearItems = function(skipRead) {
if (!this.existsInDb()) {
return $.Deferred().reject(new Error("Cannot clearItems from document without id"));
}
var that = this;
return this._doApiCall({
method: 'clearItems',
skipRead: skipRead
})
.then(function(data) {
return that._ensureTransactionDeleted().then(function(){
return data;
});
});
};
/**
* swapItem; swaps one item for another in a transaction
* @name Transaction#swapItem
* @method
* @param fromItem
* @param toItem
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.swapItem = function(fromItem, toItem, skipRead) {
if (!this.existsInDb()) {
return $.Deferred().reject(new Error("Cannot swapItem from document without id"));
}
// swapItem cannot create or delete a transaction
return this._doApiCall({
method: 'swapItem',
params: {fromItem: fromItem, toItem: toItem},
skipRead: skipRead
});
};
/**
* hasItems; Gets a list of items that are already part of the transaction
* @name Transaction#hasItems
* @method
* @param itemIds array of string values
* @returns {Array}
*/
Transaction.prototype.hasItems = function(itemIds) {
var allItems = this.items || [];
var duplicates = [];
var found = null;
$.each(itemIds, function(i, itemId) {
$.each(allItems, function(i,it){
if(it._id == itemId){
found = itemId;
return false;
}
});
if (found!=null) {
duplicates.push(found);
}
});
return duplicates;
};
/**
* Archive a transaction
* @name Transaction#archive
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.archive = function(skipRead) {
if (!this.canArchive()) {
return $.Deferred().reject(new Error("Cannot archive document"));
}
return this._doApiCall({
method: 'archive',
params: {},
skipRead: skipRead
});
};
/**
* Undo archive of a transaction
* @name Transaction#undoArchive
* @param skipRead
* @returns {promise}
*/
Transaction.prototype.undoArchive = function(skipRead) {
if (!this.canUndoArchive()) {
return $.Deferred().reject(new Error("Cannot unarchive document"));
}
return this._doApiCall({
method: 'undoArchive',
params: {},
skipRead: skipRead
});
};
/**
* Checks if we can archive a transaction (based on status)
* @name Transaction#canArchive
* @returns {boolean}
*/
Transaction.prototype.canArchive = function() {
return (
(this.archived==null) &&
((this.status == "cancelled") || (this.status == "closed")));
};
/**
* Checks if we can unarchive a transaction (based on status)
* @name Transaction#canUndoArchive
* @returns {boolean}
*/
Transaction.prototype.canUndoArchive = function() {
return (
(this.archived!=null) &&
((this.status == "cancelled") || (this.status == "closed")));
};
Transaction.prototype.setField = function(field, value, skipRead){
var that = this;
return this._ensureTransactionExists(skipRead)
.then(function() {
return that._doApiCall({
method: 'setField',
params: {field: field, value: value},
skipRead: skipRead
});
});
}
//
// Implementation stuff
//
/**
* Gets a list of Conflict objects for this transaction
* Will be overriden by inheriting classes
* @returns {promise}
* @private
*/
Transaction.prototype._getConflicts = function() {
return $.Deferred().resolve([]);
};
Transaction.prototype._getDateHelper = function() {
return this.dateHelper;
};
/**
* Searches for Items that are available for this transaction
* @param params: a dict with params, just like items/search
* @param listName: restrict search to a certain list
* @param useAvailabilities (uses items/searchAvailable instead of items/search)
* @param onlyUnbooked (true by default, only used when useAvailabilities=true)
* @param skipItems array of item ids that should be skipped
* @private
* @returns {*}
*/
Transaction.prototype._searchItems = function(params, listName, useAvailabilities, onlyUnbooked, skipItems) {
if (this.dsItems==null) {
return $.Deferred().reject(new api.ApiBadRequest(this.crtype+" has no DataSource for items"));
}
// Restrict the search to just the Items that are:
// - at this location
// - in the specified list (if any)
params = params || {};
params.location = this._getId(this.location);
if( (listName!=null) &&
(listName.length>0)) {
params.listName = listName
}
// Make sure we only pass the item ids,
// and not the entire items
var that = this;
var skipList = null;
if( (skipItems) &&
(skipItems.length)) {
skipList = skipItems.slice(0);
$.each(skipList, function(i, item) {
skipList[i] = that._getId(item);
});
}
if (useAvailabilities==true) {
// We'll use a more advanced API call /items/searchAvailable
// It's a bit slower and the .count result is not usable
// It requires some more parameters to be set
params.onlyUnbooked = (onlyUnbooked!=null) ? onlyUnbooked : true;
params.fromDate = this.from;
params.toDate = this.to || this.due; //need due date for orders!!!!!
params._limit = params._limit || 20;
params._skip = params._skip || 0;
if( (skipList) &&
(skipList.length)) {
params.skipItems = skipList;
}
return this.dsItems.call(null, 'searchAvailable', params);
} else {
// We don't need to use availabilities,
// we should better use the regular /search
// it's faster and has better paging :)
if( (skipList) &&
(skipList.length)) {
params.pk__nin = skipList;
}
return this.dsItems.search(params);
}
};
/**
* Returns a rejected promise when a date is not between min and max date
* Otherwise the deferred just resolves to the date
* It's used to do some quick checks of transaction dates
* @param date
* @returns {*}
* @private
*/
Transaction.prototype._checkDateBetweenMinMax = function(date, minDate, maxDate) {
minDate = minDate || this.getMinDate();
maxDate = maxDate || this.getMaxDate();
if( (date<minDate) ||
(date>maxDate)) {
var msg = "date " + date.toJSONDate() + " is outside of min max range " + minDate.toJSONDate() +"->" + maxDate.toJSONDate();
return $.Deferred().reject(new api.ApiUnprocessableEntity(msg));
} else {
return $.Deferred().resolve(date);
}
};
/**
* _handleTransaction: creates, updates or deletes a transaction document
* @returns {*}
* @private
*/
Transaction.prototype._handleTransaction = function(skipRead) {
var isEmpty = this.isEmpty();
if (this.existsInDb()) {
if (isEmpty) {
if (this.autoCleanup) {
return this._deleteTransaction();
} else {
return $.Deferred().resolve();
}
} else {
return this._updateTransaction(skipRead);
}
} else if (!isEmpty) {
return this._createTransaction(skipRead);
} else {
return $.Deferred().resolve();
}
};
Transaction.prototype._deleteTransaction = function() {
return this.delete();
};
Transaction.prototype._updateTransaction = function(skipRead) {
return this.update(skipRead);
};
Transaction.prototype._createTransaction = function(skipRead) {
return this.create(skipRead);
};
Transaction.prototype._ensureTransactionExists = function(skipRead) {
return (!this.existsInDb()) ? this._createTransaction(skipRead) : $.Deferred().resolve();
};
Transaction.prototype._ensureTransactionDeleted = function() {
return ((this.isEmpty()) && (this.autoCleanup)) ? this._deleteTransaction() : $.Deferred().resolve();
};
return Transaction;
});