habitica/website/common/script/ops/buy/abstractBuyOperation.js

210 lines
5.4 KiB
JavaScript
Raw Normal View History

2019-10-09 14:51:17 +00:00
/* eslint-disable max-classes-per-file */
2019-10-08 14:57:10 +00:00
import _merge from 'lodash/merge';
import _get from 'lodash/get';
import i18n from '../../i18n';
import {
NotAuthorized,
NotImplementedError,
BadRequest,
} from '../../libs/errors';
export class AbstractBuyOperation {
/**
* @param {User} user - the User-Object
* @param {Request} req - the Request-Object
* @param {analytics} analytics
*/
constructor (user, req, analytics) {
this.user = user;
this.req = req || {};
this.analytics = analytics;
2019-10-08 14:57:10 +00:00
const quantity = _get(req, 'quantity');
this.quantity = quantity ? Number(quantity) : 1;
if (this.quantity < 1 || !Number.isInteger(this.quantity)) throw new BadRequest(this.i18n('invalidQuantity'));
}
/**
* Returns the item value
* @param item
* @returns {number}
*/
2019-10-09 18:08:36 +00:00
getItemValue (item) { // eslint-disable-line class-methods-use-this
return item.value;
}
/**
* Returns the item key
* @param item
* @returns {String}
*/
2019-10-09 18:08:36 +00:00
getItemKey (item) { // eslint-disable-line class-methods-use-this
return item.key;
}
/**
* Returns the item type
* @param item
* @returns {String}
*/
2019-10-09 18:08:36 +00:00
getItemType (item) { // eslint-disable-line class-methods-use-this
2019-10-08 14:57:10 +00:00
if (!item.type) throw new NotImplementedError('item doesn\'t have a type property');
return item.type;
}
/**
* Shortcut to get the translated string without passing `req.language`
* @param {String} key - translation key
* @param {*=} params
* @returns {*|string}
*/
// eslint-disable-next-line no-unused-vars
i18n (key, params = {}) {
2019-10-09 14:51:17 +00:00
return i18n.t.apply(null, [...arguments, this.req.language]); // eslint-disable-line prefer-rest-params, max-len
}
/**
* If the Operation allows purchasing items by quantity
* @returns Boolean
*/
2019-10-09 18:08:36 +00:00
multiplePurchaseAllowed () { // eslint-disable-line class-methods-use-this
throw new NotImplementedError('multiplePurchaseAllowed');
}
/**
* Method is called to save the params as class-fields in order to access them
*/
2019-10-09 18:08:36 +00:00
extractAndValidateParams () { // eslint-disable-line class-methods-use-this
throw new NotImplementedError('extractAndValidateParams');
}
2019-10-09 18:08:36 +00:00
executeChanges () { // eslint-disable-line class-methods-use-this
throw new NotImplementedError('executeChanges');
}
2019-10-09 18:08:36 +00:00
analyticsData () { // eslint-disable-line class-methods-use-this
throw new NotImplementedError('sendToAnalytics');
}
purchase () {
if (!this.multiplePurchaseAllowed() && this.quantity > 1) {
throw new NotAuthorized(this.i18n('messageNotAbleToBuyInBulk'));
}
this.extractAndValidateParams(this.user, this.req);
const resultObj = this.executeChanges(this.user, this.item, this.req, this.analytics);
if (this.analytics) {
this.sendToAnalytics(this.analyticsData());
}
return resultObj;
}
2019-10-09 18:08:36 +00:00
analyticsLabel () { // eslint-disable-line class-methods-use-this
return 'acquire item';
}
sendToAnalytics (additionalData = {}) {
// spread-operator produces an "unexpected token" error
2019-10-08 14:57:10 +00:00
const analyticsData = _merge(additionalData, {
// ...additionalData,
uuid: this.user._id,
category: 'behavior',
headers: this.req.headers,
});
if (this.multiplePurchaseAllowed()) {
analyticsData.quantityPurchased = this.quantity;
}
this.analytics.track(this.analyticsLabel(), analyticsData);
}
}
export class AbstractGoldItemOperation extends AbstractBuyOperation {
canUserPurchase (user, item) {
this.item = item;
2019-10-08 14:57:10 +00:00
const itemValue = this.getItemValue(item);
2019-10-08 14:57:10 +00:00
const userGold = user.stats.gp;
if (userGold < itemValue * this.quantity) {
throw new NotAuthorized(this.i18n('messageNotEnoughGold'));
}
if (item && item.canOwn && !item.canOwn(user)) {
throw new NotAuthorized(this.i18n('cannotBuyItem'));
}
}
subtractCurrency (user, item) {
2019-10-08 14:57:10 +00:00
const itemValue = this.getItemValue(item);
user.stats.gp -= itemValue * this.quantity;
}
analyticsData () {
return {
itemKey: this.getItemKey(this.item),
itemType: this.getItemType(this.item),
acquireMethod: 'Gold',
goldCost: this.getItemValue(this.item),
};
}
}
export class AbstractGemItemOperation extends AbstractBuyOperation {
canUserPurchase (user, item) {
this.item = item;
2019-10-08 14:57:10 +00:00
const itemValue = this.getItemValue(item);
if (!item.canBuy(user)) {
throw new NotAuthorized(this.i18n('messageNotAvailable'));
}
if (!user.balance || user.balance < itemValue * this.quantity) {
throw new NotAuthorized(this.i18n('notEnoughGems'));
}
}
subtractCurrency (user, item) {
2019-10-08 14:57:10 +00:00
const itemValue = this.getItemValue(item);
user.balance -= itemValue * this.quantity;
}
analyticsData () {
return {
itemKey: this.getItemKey(this.item),
itemType: this.getItemType(this.item),
acquireMethod: 'Gems',
gemCost: this.getItemValue(this.item) * 4,
};
}
}
export class AbstractHourglassItemOperation extends AbstractBuyOperation {
canUserPurchase (user, item) {
this.item = item;
if (user.purchased.plan.consecutive.trinkets <= 0) {
throw new NotAuthorized(this.i18n('notEnoughHourglasses'));
}
}
2019-10-09 18:08:36 +00:00
subtractCurrency (user) { // eslint-disable-line class-methods-use-this
user.purchased.plan.consecutive.trinkets -= 1;
}
analyticsData () {
return {
itemKey: this.item.key,
acquireMethod: 'Hourglass',
};
}
}