mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 22:27:26 +01:00
Purchase API Refactoring: Market Gear (#10010)
* convert buyGear to buyMarketGearOperation + tests * move NotImplementedError
This commit is contained in:
@@ -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;
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
|
||||
127
website/common/script/ops/buy/abstractBuyOperation.js
Normal file
127
website/common/script/ops/buy/abstractBuyOperation.js
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
};
|
||||
78
website/common/script/ops/buy/buyMarketGear.js
Normal file
78
website/common/script/ops/buy/buyMarketGear.js
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user