Refactor Purchase API - Part 1 (#9714)

* move to shops/purchase

* move files to /buy/ instead of /purchase/

* refactor buy.js - add more itemtypes

* revert moving special purchases to buy

* only use buyOp from api-routes

* fix buying potion client-side

* undo import buy instead of purchase

* enable potion bulk purchase - use buyGear as fallback (as before)

* move quantity purchase inside buyHealthPotion

* move quantity purchase inside buyQuest

* move quantity purchase inside buySpecialSpell + add analytics

* remove unused quantity variable - set req.type on specialKeys

* fix `buyKnownKeys` on buy api

* test buy-special-spell if not enough gold

* more buy ops coverage

* fix lint

* buyMysterySet: test for window.confirm, buyQuest: check for Masterclassers unlock

* fix test & lint

* re-create package-lock.json to travis build ?

* use global.window instead of method argument

* add back canOwn checks

* remove buyMysterySet confirm request
This commit is contained in:
negue
2018-02-10 11:14:40 +01:00
committed by Matteo Pagliazzi
parent 7abfd1d510
commit 54153ec299
27 changed files with 340 additions and 181 deletions

View File

@@ -40,4 +40,19 @@ describe('POST /user/buy-special-spell/:key', () => {
itemText: item.text(),
}));
});
it('returns an error if user does not have enough gold', async () => {
let key = 'thankyou';
await user.update({
'stats.gp': 5,
});
await expect(user.post(`/user/buy-special-spell/${key}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageNotEnoughGold'),
});
});
});

View File

@@ -2,7 +2,7 @@
import {
generateUser,
} from '../../../helpers/common.helper';
import buy from '../../../../website/common/script/ops/buy';
import buy from '../../../../website/common/script/ops/buy/buy';
import {
BadRequest,
} from '../../../../website/common/script/libs/errors';
@@ -11,6 +11,7 @@ import content from '../../../../website/common/script/content/index';
describe('shared.ops.buy', () => {
let user;
let analytics = {track () {}};
beforeEach(() => {
user = generateUser({
@@ -26,6 +27,12 @@ describe('shared.ops.buy', () => {
},
stats: { gp: 200 },
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
it('returns error when key is not provided', (done) => {
@@ -40,8 +47,10 @@ describe('shared.ops.buy', () => {
it('recovers 15 hp', () => {
user.stats.hp = 30;
buy(user, {params: {key: 'potion'}});
buy(user, {params: {key: 'potion'}}, analytics);
expect(user.stats.hp).to.eql(45);
expect(analytics.track).to.be.calledOnce;
});
it('adds equipment to inventory', () => {

View File

@@ -4,7 +4,7 @@ import {
generateUser,
} from '../../../helpers/common.helper';
import count from '../../../../website/common/script/count';
import buyArmoire from '../../../../website/common/script/ops/buyArmoire';
import buyArmoire from '../../../../website/common/script/ops/buy/buyArmoire';
import randomVal from '../../../../website/common/script/libs/randomVal';
import content from '../../../../website/common/script/content/index';
import {
@@ -31,6 +31,7 @@ describe('shared.ops.buyArmoire', () => {
let YIELD_EQUIPMENT = 0.5;
let YIELD_FOOD = 0.7;
let YIELD_EXP = 0.9;
let analytics = {track () {}};
beforeEach(() => {
user = generateUser({
@@ -45,10 +46,12 @@ describe('shared.ops.buyArmoire', () => {
user.items.food = {};
sandbox.stub(randomVal, 'trueRandom');
sinon.stub(analytics, 'track');
});
afterEach(() => {
randomVal.trueRandom.restore();
analytics.track.restore();
});
context('failure conditions', () => {
@@ -141,7 +144,7 @@ describe('shared.ops.buyArmoire', () => {
expect(_.size(user.items.gear.owned)).to.equal(2);
buyArmoire(user);
buyArmoire(user, {}, analytics);
expect(_.size(user.items.gear.owned)).to.equal(3);
@@ -149,6 +152,7 @@ describe('shared.ops.buyArmoire', () => {
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 2);
expect(user.stats.gp).to.eql(100);
expect(analytics.track).to.be.calledOnce;
});
});
});

View File

@@ -4,15 +4,16 @@ import sinon from 'sinon'; // eslint-disable-line no-shadow
import {
generateUser,
} from '../../../helpers/common.helper';
import buyGear from '../../../../website/common/script/ops/buyGear';
import buyGear from '../../../../website/common/script/ops/buy/buyGear';
import shared from '../../../../website/common/script';
import {
NotAuthorized,
BadRequest, NotAuthorized, NotFound,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
describe('shared.ops.buyGear', () => {
let user;
let analytics = {track () {}};
beforeEach(() => {
user = generateUser({
@@ -31,18 +32,20 @@ describe('shared.ops.buyGear', () => {
sinon.stub(shared, 'randomVal');
sinon.stub(shared.fns, 'predictableRandom');
sinon.stub(analytics, 'track');
});
afterEach(() => {
shared.randomVal.restore();
shared.fns.predictableRandom.restore();
analytics.track.restore();
});
context('Gear', () => {
it('adds equipment to inventory', () => {
user.stats.gp = 31;
buyGear(user, {params: {key: 'armor_warrior_1'}});
buyGear(user, {params: {key: 'armor_warrior_1'}}, analytics);
expect(user.items.gear.owned).to.eql({
weapon_warrior_0: true,
@@ -55,6 +58,7 @@ describe('shared.ops.buyGear', () => {
eyewear_special_whiteTopFrame: true,
eyewear_special_yellowTopFrame: true,
});
expect(analytics.track).to.be.calledOnce;
});
it('deducts gold from user', () => {
@@ -139,6 +143,38 @@ describe('shared.ops.buyGear', () => {
}
});
it('returns error when key is not provided', (done) => {
try {
buyGear(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('missingKeyParam'));
done();
}
});
it('returns error when item is not found', (done) => {
let params = {key: 'armor_warrior_notExisting'};
try {
buyGear(user, {params});
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(i18n.t('itemNotFound', params));
done();
}
});
it('does not buyGear equipment without the previous equipment', (done) => {
try {
buyGear(user, {params: {key: 'armor_warrior_2'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('previousGearNotOwned'));
done();
}
});
it('does not buyGear equipment if user does not own prior item in sequence', (done) => {
user.stats.gp = 200;

View File

@@ -2,7 +2,7 @@
import {
generateUser,
} from '../../../helpers/common.helper';
import buyHealthPotion from '../../../../website/common/script/ops/buyHealthPotion';
import buyHealthPotion from '../../../../website/common/script/ops/buy/buyHealthPotion';
import {
NotAuthorized,
} from '../../../../website/common/script/libs/errors';
@@ -10,6 +10,7 @@ import i18n from '../../../../website/common/script/i18n';
describe('shared.ops.buyHealthPotion', () => {
let user;
let analytics = {track () {}};
beforeEach(() => {
user = generateUser({
@@ -25,13 +26,19 @@ describe('shared.ops.buyHealthPotion', () => {
},
stats: { gp: 200 },
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('Potion', () => {
it('recovers 15 hp', () => {
user.stats.hp = 30;
buyHealthPotion(user);
buyHealthPotion(user, {}, analytics);
expect(user.stats.hp).to.eql(45);
expect(analytics.track).to.be.calledOnce;
});
it('does not increase hp above 50', () => {

View File

@@ -3,8 +3,9 @@
import {
generateUser,
} from '../../../helpers/common.helper';
import buyMysterySet from '../../../../website/common/script/ops/buyMysterySet';
import buyMysterySet from '../../../../website/common/script/ops/buy/buyMysterySet';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../../../../website/common/script/libs/errors';
@@ -12,6 +13,7 @@ import i18n from '../../../../website/common/script/i18n';
describe('shared.ops.buyMysterySet', () => {
let user;
let analytics = {track () {}};
beforeEach(() => {
user = generateUser({
@@ -23,6 +25,11 @@ describe('shared.ops.buyMysterySet', () => {
},
},
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('Mystery Sets', () => {
@@ -57,12 +64,22 @@ describe('shared.ops.buyMysterySet', () => {
done();
}
});
it('returns error when key is not provided', (done) => {
try {
buyMysterySet(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('missingKeyParam'));
done();
}
});
});
context('successful purchases', () => {
it('buys Steampunk Accessories Set', () => {
user.purchased.plan.consecutive.trinkets = 1;
buyMysterySet(user, {params: {key: '301404'}});
buyMysterySet(user, {params: {key: '301404'}}, analytics);
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
@@ -70,6 +87,7 @@ describe('shared.ops.buyMysterySet', () => {
expect(user.items.gear.owned).to.have.property('armor_mystery_301404', true);
expect(user.items.gear.owned).to.have.property('head_mystery_301404', true);
expect(user.items.gear.owned).to.have.property('eyewear_mystery_301404', true);
expect(analytics.track).to.be.called;
});
});
});

View File

@@ -1,8 +1,9 @@
import {
generateUser,
} from '../../../helpers/common.helper';
import buyQuest from '../../../../website/common/script/ops/buyQuest';
import buyQuest from '../../../../website/common/script/ops/buy/buyQuest';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../../../../website/common/script/libs/errors';
@@ -10,9 +11,15 @@ import i18n from '../../../../website/common/script/i18n';
describe('shared.ops.buyQuest', () => {
let user;
let analytics = {track () {}};
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
it('buys a Quest scroll', () => {
@@ -21,11 +28,12 @@ describe('shared.ops.buyQuest', () => {
params: {
key: 'dilatoryDistress1',
},
});
}, analytics);
expect(user.items.quests).to.eql({
dilatoryDistress1: 1,
});
expect(user.stats.gp).to.equal(5);
expect(analytics.track).to.be.calledOnce;
});
it('does not buy Quests without enough Gold', (done) => {
@@ -62,6 +70,22 @@ describe('shared.ops.buyQuest', () => {
}
});
it('does not buy the Mystery of the Masterclassers', (done) => {
try {
buyQuest(user, {
params: {
key: 'lostMasterclasser1',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('questUnlockLostMasterclasser'));
expect(user.items.quests).to.eql({});
done();
}
});
it('does not buy Gem-premium Quests', (done) => {
user.stats.gp = 9999;
try {
@@ -78,4 +102,14 @@ describe('shared.ops.buyQuest', () => {
done();
}
});
it('returns error when key is not provided', (done) => {
try {
buyQuest(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('missingKeyParam'));
done();
}
});
});

View File

@@ -1,4 +1,4 @@
import buySpecialSpell from '../../../../website/common/script/ops/buySpecialSpell';
import buySpecialSpell from '../../../../website/common/script/ops/buy/buySpecialSpell';
import {
BadRequest,
NotFound,
@@ -12,9 +12,15 @@ import content from '../../../../website/common/script/content/index';
describe('shared.ops.buySpecialSpell', () => {
let user;
let analytics = {track () {}};
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
it('throws an error if params.key is missing', (done) => {
@@ -64,7 +70,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: {
key: 'thankyou',
},
});
}, analytics);
expect(user.stats.gp).to.equal(1);
expect(user.items.special.thankyou).to.equal(1);
@@ -75,5 +81,6 @@ describe('shared.ops.buySpecialSpell', () => {
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
});

View File

@@ -1,19 +1,25 @@
import hourglassPurchase from '../../../website/common/script/ops/hourglassPurchase';
import hourglassPurchase from '../../../../website/common/script/ops/buy/hourglassPurchase';
import {
BadRequest,
NotAuthorized,
} from '../../../website/common/script/libs/errors';
import i18n from '../../../website/common/script/i18n';
import content from '../../../website/common/script/content/index';
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import content from '../../../../website/common/script/content/index';
import {
generateUser,
} from '../../helpers/common.helper';
} from '../../../helpers/common.helper';
describe('common.ops.hourglassPurchase', () => {
let user;
let analytics = {track () {}};
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('failure conditions', () => {
@@ -126,11 +132,12 @@ describe('common.ops.hourglassPurchase', () => {
it('buys a pet', () => {
user.purchased.plan.consecutive.trinkets = 2;
let [, message] = hourglassPurchase(user, {params: {type: 'pets', key: 'MantisShrimp-Base'}});
let [, message] = hourglassPurchase(user, {params: {type: 'pets', key: 'MantisShrimp-Base'}}, analytics);
expect(message).to.eql(i18n.t('hourglassPurchase'));
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.items.pets).to.eql({'MantisShrimp-Base': 5});
expect(analytics.track).to.be.calledOnce;
});
it('buys a mount', () => {

View File

@@ -1,14 +1,14 @@
import purchase from '../../../website/common/script/ops/purchase';
import planGemLimits from '../../../website/common/script/libs/planGemLimits';
import purchase from '../../../../website/common/script/ops/buy/purchase';
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../../../website/common/script/libs/errors';
import i18n from '../../../website/common/script/i18n';
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import {
generateUser,
} from '../../helpers/common.helper';
} from '../../../helpers/common.helper';
import forEach from 'lodash/forEach';
import moment from 'moment';
@@ -17,11 +17,20 @@ describe('shared.ops.purchase', () => {
let user;
let goldPoints = 40;
let gemsBought = 40;
let analytics = {track () {}};
before(() => {
user = generateUser({'stats.class': 'rogue'});
});
beforeEach(() => {
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('failure conditions', () => {
it('returns an error when type is not provided', (done) => {
try {
@@ -129,6 +138,19 @@ describe('shared.ops.purchase', () => {
done();
}
});
it('returns error when item is not found', (done) => {
let params = {key: 'notExisting', type: 'food'};
try {
purchase(user, {params});
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(i18n.t('contentKeyNotFound', params));
done();
}
});
});
context('successful purchase', () => {
@@ -142,12 +164,13 @@ describe('shared.ops.purchase', () => {
});
it('purchases gems', () => {
let [, message] = purchase(user, {params: {type: 'gems', key: 'gem'}});
let [, message] = purchase(user, {params: {type: 'gems', key: 'gem'}}, analytics);
expect(message).to.equal(i18n.t('plusOneGem'));
expect(user.balance).to.equal(userGemAmount + 0.25);
expect(user.purchased.plan.gemsBought).to.equal(1);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
expect(analytics.track).to.be.calledOnce;
});
it('purchases gems with a different language than the default', () => {
@@ -163,9 +186,10 @@ describe('shared.ops.purchase', () => {
let type = 'eggs';
let key = 'Wolf';
purchase(user, {params: {type, key}});
purchase(user, {params: {type, key}}, analytics);
expect(user.items[type][key]).to.equal(1);
expect(analytics.track).to.be.calledOnce;
});
it('purchases hatchingPotions', () => {

View File

@@ -1,8 +1,8 @@
import axios from 'axios';
import buyOp from 'common/script/ops/buy';
import buyOp from 'common/script/ops/buy/buy';
import content from 'common/script/content/index';
import purchaseOp from 'common/script/ops/purchaseWithSpell';
import hourglassPurchaseOp from 'common/script/ops/hourglassPurchase';
import purchaseOp from 'common/script/ops/buy/purchaseWithSpell';
import hourglassPurchaseOp from 'common/script/ops/buy/hourglassPurchase';
import sellOp from 'common/script/ops/sell';
import unlockOp from 'common/script/ops/unlock';
import rerollOp from 'common/script/ops/reroll';
@@ -101,7 +101,7 @@ export function purchase (store, params) {
export function purchaseMysterySet (store, params) {
const user = store.state.user.data;
let opResult = buyOp(user, {params, noConfirm: true, type: 'mystery'});
let opResult = buyOp(user, {params, type: 'mystery'});
return {
result: opResult,

View File

@@ -138,21 +138,12 @@ import sleep from './ops/sleep';
import allocateNow from './ops/stats/allocateNow';
import allocate from './ops/stats/allocate';
import allocateBulk from './ops/stats/allocateBulk';
import buy from './ops/buy';
import buyGear from './ops/buyGear';
import buyHealthPotion from './ops/buyHealthPotion';
import buyArmoire from './ops/buyArmoire';
import buyMysterySet from './ops/buyMysterySet';
import buyQuest from './ops/buyQuest';
import buySpecialSpell from './ops/buySpecialSpell';
import buy from './ops/buy/buy';
import hatch from './ops/hatch';
import feed from './ops/feed';
import equip from './ops/equip';
import changeClass from './ops/changeClass';
import disableClasses from './ops/disableClasses';
import purchase from './ops/purchase';
import purchaseWithSpell from './ops/purchaseWithSpell';
import purchaseHourglass from './ops/hourglassPurchase';
import readCard from './ops/readCard';
import openMysteryItem from './ops/openMysteryItem';
import releasePets from './ops/releasePets';
@@ -177,21 +168,12 @@ api.ops = {
allocate,
allocateBulk,
buy,
buyGear,
buyHealthPotion,
buyArmoire,
buyMysterySet,
buySpecialSpell,
buyQuest,
allocateNow,
hatch,
feed,
equip,
changeClass,
disableClasses,
purchase,
purchaseWithSpell,
purchaseHourglass,
readCard,
openMysteryItem,
releasePets,

View File

@@ -237,7 +237,7 @@ module.exports = function getItemInfo (user, type, item, officialPinnedItems, la
notes: item.notes(language),
value: item.value,
currency: 'gold',
purchaseType: 'potions',
purchaseType: 'potion',
class: `shop_${item.key}`,
path: 'potion',
pinType: 'potion',

View File

@@ -1,14 +1,16 @@
import i18n from '../i18n';
import i18n from '../../i18n';
import get from 'lodash/get';
import {
BadRequest,
} from '../libs/errors';
} from '../../libs/errors';
import buyHealthPotion from './buyHealthPotion';
import buyArmoire from './buyArmoire';
import buyGear from './buyGear';
import buyMysterySet from './buyMysterySet';
import buyQuest from './buyQuest';
import buySpecialSpell from './buySpecialSpell';
import purchaseOp from './purchase';
import hourglassPurchase from './hourglassPurchase';
// @TODO: remove the req option style. Dependency on express structure is an anti-pattern
// We should either have more parms or a set structure validated by a Type checker
@@ -22,29 +24,43 @@ module.exports = function buy (user, req = {}, analytics) {
// @TODO: Slowly remove the need for key and use type instead
// This should evenutally be the 'factory' function with vendor classes
let type = get(req, 'type');
if (!type) type = get(req, 'params.type');
if (!type) type = key;
// @TODO: For now, builk purchasing is here, but we should probably have a parent vendor
// class that calls the factory and handles larger operations. If there is more than just bulk
let quantity = 1;
if (req.quantity) quantity = req.quantity;
let buyRes;
for (let i = 0; i < quantity; i += 1) {
if (type === 'potion') {
buyRes = buyHealthPotion(user, req, analytics);
} else if (type === 'armoire') {
switch (type) {
case 'armoire':
buyRes = buyArmoire(user, req, analytics);
} else if (type === 'mystery') {
break;
case 'mystery':
buyRes = buyMysterySet(user, req, analytics);
} else if (type === 'quest') {
break;
case 'potion':
buyRes = buyHealthPotion(user, req, analytics);
break;
case 'eggs':
case 'hatchingPotions':
case 'food':
case 'quests':
case 'gear':
case 'bundles':
case 'gems':
buyRes = purchaseOp(user, req, analytics);
break;
case 'pets':
case 'mounts':
buyRes = hourglassPurchase(user, req, analytics);
break;
case 'quest':
buyRes = buyQuest(user, req, analytics);
} else if (type === 'special') {
break;
case 'special':
buyRes = buySpecialSpell(user, req, analytics);
} else {
break;
default:
buyRes = buyGear(user, req, analytics);
}
break;
}
return buyRes;

View File

@@ -1,15 +1,15 @@
import content from '../content/index';
import i18n from '../i18n';
import content from '../../content/index';
import i18n from '../../i18n';
import filter from 'lodash/filter';
import isEmpty from 'lodash/isEmpty';
import pick from 'lodash/pick';
import count from '../count';
import splitWhitespace from '../libs/splitWhitespace';
import count from '../../count';
import splitWhitespace from '../../libs/splitWhitespace';
import {
NotAuthorized,
} from '../libs/errors';
import randomVal from '../libs/randomVal';
import { removeItemByPath } from './pinnedGearUtils';
} from '../../libs/errors';
import randomVal from '../../libs/randomVal';
import { removeItemByPath } from '../pinnedGearUtils';
// TODO this is only used on the server
// move out of common?

View File

@@ -1,17 +1,17 @@
import content from '../content/index';
import i18n from '../i18n';
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 splitWhitespace from '../../libs/splitWhitespace';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
import handleTwoHanded from '../fns/handleTwoHanded';
import ultimateGear from '../fns/ultimateGear';
} from '../../libs/errors';
import handleTwoHanded from '../../fns/handleTwoHanded';
import ultimateGear from '../../fns/ultimateGear';
import { removePinnedGearAddPossibleNewOnes } from './pinnedGearUtils';
import { removePinnedGearAddPossibleNewOnes } from '../pinnedGearUtils';
module.exports = function buyGear (user, req = {}, analytics) {
let key = get(req, 'params.key');

View File

@@ -1,13 +1,14 @@
import content from '../content/index';
import i18n from '../i18n';
import content from '../../content/index';
import i18n from '../../i18n';
import {
NotAuthorized,
} from '../libs/errors';
} from '../../libs/errors';
module.exports = function buyHealthPotion (user, req = {}, analytics) {
let item = content.potion;
let quantity = req.quantity || 1;
if (user.stats.gp < item.value) {
if (user.stats.gp < item.value * quantity) {
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
@@ -23,12 +24,12 @@ module.exports = function buyHealthPotion (user, req = {}, analytics) {
throw new NotAuthorized(i18n.t('messageHealthAlreadyMin', req.language));
}
user.stats.hp += 15;
user.stats.hp += 15 * quantity;
if (user.stats.hp > 50) {
user.stats.hp = 50;
}
user.stats.gp -= item.value;
user.stats.gp -= item.value * quantity;
let message = i18n.t('messageBought', {
itemText: item.text(req.language),
@@ -43,6 +44,7 @@ module.exports = function buyHealthPotion (user, req = {}, analytics) {
goldCost: item.value,
category: 'behavior',
headers: req.headers,
quantityPurchased: quantity,
});
}

View File

@@ -1,12 +1,12 @@
import i18n from '../i18n';
import content from '../content/index';
import i18n from '../../i18n';
import content from '../../content/index';
import get from 'lodash/get';
import each from 'lodash/each';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
} from '../../libs/errors';
module.exports = function buyMysterySet (user, req = {}, analytics) {
let key = get(req, 'params.key');
@@ -23,10 +23,6 @@ module.exports = function buyMysterySet (user, req = {}, analytics) {
throw new NotFound(i18n.t('mysterySetNotFound', req.language));
}
if (typeof window !== 'undefined' && !req.noConfirm && window.confirm) { // TODO move to client
if (!window.confirm(i18n.t('hourglassBuyEquipSetConfirm'))) return;
}
each(mysterySet.items, item => {
user.items.gear.owned[item.key] = true;
if (analytics) {

View File

@@ -1,15 +1,17 @@
import i18n from '../i18n';
import content from '../content/index';
import i18n from '../../i18n';
import content from '../../content/index';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
} from '../../libs/errors';
import get from 'lodash/get';
// buy a quest with gold
module.exports = function buyQuest (user, req = {}, analytics) {
let key = get(req, 'params.key');
let quantity = req.quantity || 1;
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let item = content.quests[key];
@@ -22,13 +24,13 @@ module.exports = function buyQuest (user, req = {}, analytics) {
if (!(item.category === 'gold' && item.goldValue)) {
throw new NotAuthorized(i18n.t('questNotGoldPurchasable', {key}, req.language));
}
if (user.stats.gp < item.goldValue) {
if (user.stats.gp < item.goldValue * quantity) {
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
user.items.quests[item.key] = user.items.quests[item.key] || 0;
user.items.quests[item.key]++;
user.stats.gp -= item.goldValue;
user.items.quests[item.key] += quantity;
user.stats.gp -= item.goldValue * quantity;
if (analytics) {
analytics.track('acquire item', {
@@ -36,6 +38,7 @@ module.exports = function buyQuest (user, req = {}, analytics) {
itemKey: item.key,
itemType: 'Market',
goldCost: item.goldValue,
quantityPurchased: quantity,
acquireMethod: 'Gold',
category: 'behavior',
headers: req.headers,

View File

@@ -0,0 +1,47 @@
import i18n from '../../i18n';
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';
module.exports = function buySpecialSpell (user, req = {}, analytics) {
let key = get(req, 'params.key');
let quantity = req.quantity || 1;
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let item = content.special[key];
if (!item) throw new NotFound(i18n.t('spellNotFound', {spellId: key}, req.language));
if (user.stats.gp < item.value * quantity) {
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
user.stats.gp -= item.value * quantity;
user.items.special[key] += quantity;
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: item.key,
itemType: 'Market',
goldCost: item.goldValue,
quantityPurchased: quantity,
acquireMethod: 'Gold',
category: 'behavior',
headers: req.headers,
});
}
return [
pick(user, splitWhitespace('items stats')),
i18n.t('messageBought', {
itemText: item.text(req.language),
}, req.language),
];
};

View File

@@ -1,12 +1,12 @@
import content from '../content/index';
import i18n from '../i18n';
import content from '../../content/index';
import i18n from '../../i18n';
import get from 'lodash/get';
import includes from 'lodash/includes';
import keys from 'lodash/keys';
import {
BadRequest,
NotAuthorized,
} from '../libs/errors';
} from '../../libs/errors';
module.exports = function purchaseHourglass (user, req = {}, analytics) {
let key = get(req, 'params.key');

View File

@@ -1,18 +1,18 @@
import content from '../content/index';
import i18n from '../i18n';
import content from '../../content/index';
import i18n from '../../i18n';
import get from 'lodash/get';
import pick from 'lodash/pick';
import forEach from 'lodash/forEach';
import splitWhitespace from '../libs/splitWhitespace';
import planGemLimits from '../libs/planGemLimits';
import splitWhitespace from '../../libs/splitWhitespace';
import planGemLimits from '../../libs/planGemLimits';
import {
NotFound,
NotAuthorized,
BadRequest,
} from '../libs/errors';
} from '../../libs/errors';
import { removeItemByPath } from './pinnedGearUtils';
import getItemInfo from '../libs/getItemInfo';
import { removeItemByPath } from '../pinnedGearUtils';
import getItemInfo from '../../libs/getItemInfo';
function buyGems (user, analytics, req, key) {
let convRate = planGemLimits.convRate;

View File

@@ -0,0 +1,12 @@
import buy from './buy';
import get from 'lodash/get';
module.exports = function purchaseWithSpell (user, req = {}, analytics) {
const type = get(req.params, 'type');
if (type === 'spells') {
req.type = 'special';
}
return buy(user, req, analytics);
};

View File

@@ -1,32 +0,0 @@
import i18n from '../i18n';
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';
module.exports = function buySpecialSpell (user, req = {}) {
let key = get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let item = content.special[key];
if (!item) throw new NotFound(i18n.t('spellNotFound', {spellId: key}, req.language));
if (user.stats.gp < item.value) {
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
user.stats.gp -= item.value;
user.items.special[key]++;
return [
pick(user, splitWhitespace('items stats')),
i18n.t('messageBought', {
itemText: item.text(req.language),
}, req.language),
];
};

View File

@@ -18,19 +18,10 @@ import clearPMs from './clearPMs';
import deletePM from './deletePM';
import blockUser from './blockUser';
import feed from './feed';
import buySpecialSpell from './buySpecialSpell';
import purchase from './purchase';
import purchaseWithSpell from './purchaseWithSpell';
import releasePets from './releasePets';
import releaseMounts from './releaseMounts';
import releaseBoth from './releaseBoth';
import buy from './buy';
import buyGear from './buyGear';
import buyHealthPotion from './buyHealthPotion';
import buyArmoire from './buyArmoire';
import buyQuest from './buyQuest';
import buyMysterySet from './buyMysterySet';
import hourglassPurchase from './hourglassPurchase';
import buy from './buy/purchase';
import sell from './sell';
import equip from './equip';
import hatch from './hatch';
@@ -63,19 +54,10 @@ module.exports = {
deletePM,
blockUser,
feed,
buySpecialSpell,
purchase,
purchaseWithSpell,
releasePets,
releaseMounts,
releaseBoth,
buy,
buyGear,
buyHealthPotion,
buyArmoire,
buyQuest,
buyMysterySet,
hourglassPurchase,
sell,
equip,
hatch,

View File

@@ -1,12 +0,0 @@
import buy from './buy';
import purchaseOp from './purchase';
import get from 'lodash/get';
module.exports = function purchaseWithSpell (user, req = {}, analytics) {
const type = get(req.params, 'type');
// Set up type for buy function - different than the above type.
req.type = 'special';
return type === 'spells' ? buy(user, req, analytics) : purchaseOp(user, req, analytics);
};

View File

@@ -744,6 +744,9 @@ api.sleep = {
},
};
const buySpecialKeys = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'];
const buyKnownKeys = ['armoire', 'mystery', 'potion', 'quest', 'special'];
/**
* @api {post} /api/v3/user/buy/:key Buy gear, armoire or potion
* @apiDescription Under the hood uses UserBuyGear, UserBuyPotion and UserBuyArmoire
@@ -781,14 +784,13 @@ api.buy = {
let user = res.locals.user;
let buyRes;
let specialKeys = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'];
// @TODO: Remove this when mobile passes type in body
let type = req.params.key;
if (specialKeys.indexOf(req.params.key) !== -1) {
type = 'special';
if (buySpecialKeys.indexOf(type) !== -1) {
req.type = 'special';
} else if (buyKnownKeys.indexOf(type) === -1) {
req.type = 'marketGear';
}
req.type = type;
// @TODO: right now common follow express structure, but we should decouple the dependency
if (req.body.type) req.type = req.body.type;
@@ -1267,7 +1269,7 @@ api.purchase = {
if (req.body.quantity) quantity = req.body.quantity;
req.quantity = quantity;
let purchaseRes = common.ops.purchaseWithSpell(user, req, res.analytics);
let purchaseRes = common.ops.buy(user, req, res.analytics);
await user.save();
res.respond(200, ...purchaseRes);
},
@@ -1298,7 +1300,7 @@ api.userPurchaseHourglass = {
url: '/user/purchase-hourglass/:type/:key',
async handler (req, res) {
let user = res.locals.user;
let purchaseHourglassRes = common.ops.purchaseHourglass(user, req, res.analytics);
let purchaseHourglassRes = common.ops.buy(user, req, res.analytics);
await user.save();
res.respond(200, ...purchaseHourglassRes);
},