Purchase API Refactoring: Market Gear (#10010)

* convert buyGear to buyMarketGearOperation + tests

* move NotImplementedError
This commit is contained in:
negue
2018-03-17 21:56:19 +01:00
committed by Matteo Pagliazzi
parent 29a9deaeb8
commit 2a97915477
6 changed files with 253 additions and 87 deletions
+9
View File
@@ -40,3 +40,12 @@ export class NotFound extends CustomError {
this.message = customMessage || 'Not found.';
}
}
export class NotImplementedError extends CustomError {
constructor (str) {
super();
this.name = this.constructor.name;
this.message = `Method: '${str}' not implemented`;
}
}
@@ -0,0 +1,127 @@
import i18n from '../../i18n';
import {
NotAuthorized, NotImplementedError,
} from '../../libs/errors';
import _merge from 'lodash/merge';
import _get from 'lodash/get';
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;
this.quantity = _get(req, 'quantity', 1);
}
/**
* 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 = {}) {
return i18n.t.apply(null, [...arguments, this.req.language]);
}
/**
* If the Operation allows purchasing items by quantity
* @returns Boolean
*/
multiplePurchaseAllowed () {
throw new NotImplementedError('multiplePurchaseAllowed');
}
/**
* Method is called to save the params as class-fields in order to access them
*/
extractAndValidateParams () {
throw new NotImplementedError('extractAndValidateParams');
}
executeChanges () {
throw new NotImplementedError('executeChanges');
}
analyticsData () {
throw new NotImplementedError('sendToAnalytics');
}
purchase () {
if (!this.multiplePurchaseAllowed() && this.quantity > 1) {
throw new NotAuthorized(this.i18n('messageNotAbleToBuyInBulk'));
}
this.extractAndValidateParams(this.user, this.req);
let resultObj = this.executeChanges(this.user, this.item, this.req);
if (this.analytics) {
this.sendToAnalytics(this.analyticsData());
}
return resultObj;
}
sendToAnalytics (additionalData = {}) {
// spread-operator produces an "unexpected token" error
let analyticsData = _merge(additionalData, {
// ...additionalData,
uuid: this.user._id,
category: 'behavior',
headers: this.req.headers,
});
if (this.multiplePurchaseAllowed()) {
analyticsData.quantityPurchased = this.quantity;
}
this.analytics.track('acquire item', analyticsData);
}
}
export class AbstractGoldItemOperation extends AbstractBuyOperation {
constructor (user, req, analytics) {
super(user, req, analytics);
}
getItemValue (item) {
return item.value;
}
canUserPurchase (user, item) {
this.item = item;
let itemValue = this.getItemValue(item);
let userGold = user.stats.gp;
if (userGold < itemValue * this.quantity) {
throw new NotAuthorized(this.i18n('messageNotEnoughGold'));
}
if (item.canOwn && !item.canOwn(user)) {
throw new NotAuthorized(this.i18n('cannotBuyItem'));
}
}
substractCurrency (user, item, quantity = 1) {
let itemValue = this.getItemValue(item);
user.stats.gp -= itemValue * quantity;
}
analyticsData () {
return {
itemKey: this.item.key,
itemType: 'Market',
acquireMethod: 'Gold',
goldCost: this.getItemValue(this.item),
};
}
}
+6 -3
View File
@@ -5,7 +5,7 @@ import {
} from '../../libs/errors';
import buyHealthPotion from './buyHealthPotion';
import buyArmoire from './buyArmoire';
import buyGear from './buyGear';
import {BuyMarketGearOperation} from './buyMarketGear';
import buyMysterySet from './buyMysterySet';
import buyQuest from './buyQuest';
import buySpecialSpell from './buySpecialSpell';
@@ -58,9 +58,12 @@ module.exports = function buy (user, req = {}, analytics) {
case 'special':
buyRes = buySpecialSpell(user, req, analytics);
break;
default:
buyRes = buyGear(user, req, analytics);
default: {
const buyOp = new BuyMarketGearOperation(user, req, analytics);
buyRes = buyOp.purchase();
break;
}
}
return buyRes;
-82
View File
@@ -1,82 +0,0 @@
import content from '../../content/index';
import i18n from '../../i18n';
import get from 'lodash/get';
import pick from 'lodash/pick';
import splitWhitespace from '../../libs/splitWhitespace';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../../libs/errors';
import handleTwoHanded from '../../fns/handleTwoHanded';
import ultimateGear from '../../fns/ultimateGear';
import { removePinnedGearAddPossibleNewOnes } from '../pinnedGearUtils';
module.exports = function buyGear (user, req = {}, analytics) {
let key = get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let item = content.gear.flat[key];
if (!item) throw new NotFound(i18n.t('itemNotFound', {key}, req.language));
if (user.stats.gp < item.value) {
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
if (item.canOwn && !item.canOwn(user)) {
throw new NotAuthorized(i18n.t('cannotBuyItem', req.language));
}
let message;
if (user.items.gear.owned[item.key]) {
throw new NotAuthorized(i18n.t('equipmentAlreadyOwned', req.language));
}
let itemIndex = Number(item.index);
if (Number.isInteger(itemIndex) && content.classes.includes(item.klass)) {
let previousLevelGear = key.replace(/[0-9]/, itemIndex - 1);
let hasPreviousLevelGear = user.items.gear.owned[previousLevelGear];
let checkIndexToType = itemIndex > (item.type === 'weapon' || item.type === 'shield' && item.klass === 'rogue' ? 0 : 1);
if (checkIndexToType && !hasPreviousLevelGear) {
throw new NotAuthorized(i18n.t('previousGearNotOwned', req.language));
}
}
if (user.preferences.autoEquip) {
user.items.gear.equipped[item.type] = item.key;
message = handleTwoHanded(user, item, undefined, req);
}
removePinnedGearAddPossibleNewOnes(user, `gear.flat.${item.key}`, item.key);
if (item.last) ultimateGear(user);
user.stats.gp -= item.value;
if (!message) {
message = i18n.t('messageBought', {
itemText: item.text(req.language),
}, req.language);
}
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: key,
acquireMethod: 'Gold',
goldCost: item.value,
category: 'behavior',
headers: req.headers,
});
}
return [
pick(user, splitWhitespace('items achievements stats flags pinnedItems')),
message,
];
};
@@ -0,0 +1,78 @@
import content from '../../content/index';
import get from 'lodash/get';
import pick from 'lodash/pick';
import splitWhitespace from '../../libs/splitWhitespace';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../../libs/errors';
import handleTwoHanded from '../../fns/handleTwoHanded';
import ultimateGear from '../../fns/ultimateGear';
import {removePinnedGearAddPossibleNewOnes} from '../pinnedGearUtils';
import { AbstractGoldItemOperation } from './abstractBuyOperation';
export class BuyMarketGearOperation extends AbstractGoldItemOperation {
constructor (user, req, analytics) {
super(user, req, analytics);
}
multiplePurchaseAllowed () {
return false;
}
extractAndValidateParams (user, req) {
let key = this.key = get(req, 'params.key');
if (!key) throw new BadRequest(this.i18n('missingKeyParam'));
let item = content.gear.flat[key];
if (!item) throw new NotFound(this.i18n('itemNotFound', {key}));
this.canUserPurchase(user, item);
if (user.items.gear.owned[item.key]) {
throw new NotAuthorized(this.i18n('equipmentAlreadyOwned'));
}
let itemIndex = Number(item.index);
if (Number.isInteger(itemIndex) && content.classes.includes(item.klass)) {
let previousLevelGear = key.replace(/[0-9]/, itemIndex - 1);
let hasPreviousLevelGear = user.items.gear.owned[previousLevelGear];
let checkIndexToType = itemIndex > (item.type === 'weapon' || item.type === 'shield' && item.klass === 'rogue' ? 0 : 1);
if (checkIndexToType && !hasPreviousLevelGear) {
throw new NotAuthorized(this.i18n('previousGearNotOwned'));
}
}
}
executeChanges (user, item, req) {
let message;
if (user.preferences.autoEquip) {
user.items.gear.equipped[item.type] = item.key;
message = handleTwoHanded(user, item, undefined, req);
}
removePinnedGearAddPossibleNewOnes(user, `gear.flat.${item.key}`, item.key);
if (item.last) ultimateGear(user);
this.substractCurrency(user, item);
if (!message) {
message = this.i18n('messageBought', {
itemText: item.text(req.language),
});
}
return [
pick(user, splitWhitespace('items achievements stats flags pinnedItems')),
message,
];
}
}