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

View File

@@ -4,14 +4,20 @@ import sinon from 'sinon'; // eslint-disable-line no-shadow
import {
generateUser,
} from '../../../helpers/common.helper';
import buyGear from '../../../../website/common/script/ops/buy/buyGear';
import {BuyMarketGearOperation} from '../../../../website/common/script/ops/buy/buyMarketGear';
import shared from '../../../../website/common/script';
import {
BadRequest, NotAuthorized, NotFound,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
describe('shared.ops.buyGear', () => {
function buyGear (user, req, analytics) {
let buyOp = new BuyMarketGearOperation(user, req, analytics);
return buyOp.purchase();
}
describe('shared.ops.buyMarketGear', () => {
let user;
let analytics = {track () {}};
@@ -111,6 +117,31 @@ describe('shared.ops.buyGear', () => {
}
});
it('does not buy equipment of different class', (done) => {
user.stats.gp = 82;
user.stats.class = 'warrior';
try {
buyGear(user, {params: {key: 'weapon_special_winter2018Rogue'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
done();
}
});
it('does not buy equipment in bulk', (done) => {
user.stats.gp = 82;
try {
buyGear(user, {params: {key: 'armor_warrior_1'}, quantity: 3});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAbleToBuyInBulk'));
done();
}
});
// TODO after user.ops.equip is done
xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', () => {
user.stats.gp = 100;

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`;
}
}

View File

@@ -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),
};
}
}

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;

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,
];
};

View File

@@ -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,
];
}
}