diff --git a/test/api/v3/integration/user/buy/POST-user_buy_special_spell.test.js b/test/api/v3/integration/user/buy/POST-user_buy_special_spell.test.js index 50984d0cb0..f6c16febee 100644 --- a/test/api/v3/integration/user/buy/POST-user_buy_special_spell.test.js +++ b/test/api/v3/integration/user/buy/POST-user_buy_special_spell.test.js @@ -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'), + }); + }); }); diff --git a/test/common/ops/buy/buy.js b/test/common/ops/buy/buy.js index 20f849b748..ab72d1f2b7 100644 --- a/test/common/ops/buy/buy.js +++ b/test/common/ops/buy/buy.js @@ -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', () => { diff --git a/test/common/ops/buy/buyArmoire.js b/test/common/ops/buy/buyArmoire.js index 8d0c529995..0bad38927d 100644 --- a/test/common/ops/buy/buyArmoire.js +++ b/test/common/ops/buy/buyArmoire.js @@ -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; }); }); }); diff --git a/test/common/ops/buy/buyGear.js b/test/common/ops/buy/buyGear.js index f627d48f57..390cb29c67 100644 --- a/test/common/ops/buy/buyGear.js +++ b/test/common/ops/buy/buyGear.js @@ -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; diff --git a/test/common/ops/buy/buyHealthPotion.js b/test/common/ops/buy/buyHealthPotion.js index e0d49678b7..195ccb2f8b 100644 --- a/test/common/ops/buy/buyHealthPotion.js +++ b/test/common/ops/buy/buyHealthPotion.js @@ -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', () => { diff --git a/test/common/ops/buy/buyMysterySet.js b/test/common/ops/buy/buyMysterySet.js index 9524de3a03..5334d3e671 100644 --- a/test/common/ops/buy/buyMysterySet.js +++ b/test/common/ops/buy/buyMysterySet.js @@ -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; }); }); }); diff --git a/test/common/ops/buy/buyQuest.js b/test/common/ops/buy/buyQuest.js index 3c67abc4e5..4a2c5b2b75 100644 --- a/test/common/ops/buy/buyQuest.js +++ b/test/common/ops/buy/buyQuest.js @@ -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(); + } + }); }); diff --git a/test/common/ops/buy/buySpecialSpell.js b/test/common/ops/buy/buySpecialSpell.js index 70709fe46b..97aade05ae 100644 --- a/test/common/ops/buy/buySpecialSpell.js +++ b/test/common/ops/buy/buySpecialSpell.js @@ -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; }); }); diff --git a/test/common/ops/hourglassPurchase.js b/test/common/ops/buy/hourglassPurchase.js similarity index 89% rename from test/common/ops/hourglassPurchase.js rename to test/common/ops/buy/hourglassPurchase.js index e35739eefc..3684f7d416 100644 --- a/test/common/ops/hourglassPurchase.js +++ b/test/common/ops/buy/hourglassPurchase.js @@ -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', () => { diff --git a/test/common/ops/purchase.js b/test/common/ops/buy/purchase.js similarity index 88% rename from test/common/ops/purchase.js rename to test/common/ops/buy/purchase.js index 6431d02ba0..ac099b087e 100644 --- a/test/common/ops/purchase.js +++ b/test/common/ops/buy/purchase.js @@ -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', () => { diff --git a/website/client/store/actions/shops.js b/website/client/store/actions/shops.js index 168df9d9cc..3045742653 100644 --- a/website/client/store/actions/shops.js +++ b/website/client/store/actions/shops.js @@ -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, diff --git a/website/common/script/index.js b/website/common/script/index.js index fdedbb11f0..d5de49a42b 100644 --- a/website/common/script/index.js +++ b/website/common/script/index.js @@ -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, diff --git a/website/common/script/libs/getItemInfo.js b/website/common/script/libs/getItemInfo.js index e71fd1fd68..ad78810ecb 100644 --- a/website/common/script/libs/getItemInfo.js +++ b/website/common/script/libs/getItemInfo.js @@ -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', diff --git a/website/common/script/ops/buy.js b/website/common/script/ops/buy/buy.js similarity index 64% rename from website/common/script/ops/buy.js rename to website/common/script/ops/buy/buy.js index aee81ecf7b..1ecb149c9c 100644 --- a/website/common/script/ops/buy.js +++ b/website/common/script/ops/buy/buy.js @@ -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; diff --git a/website/common/script/ops/buyArmoire.js b/website/common/script/ops/buy/buyArmoire.js similarity index 91% rename from website/common/script/ops/buyArmoire.js rename to website/common/script/ops/buy/buyArmoire.js index 5f4ca82da1..886db0d397 100644 --- a/website/common/script/ops/buyArmoire.js +++ b/website/common/script/ops/buy/buyArmoire.js @@ -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? diff --git a/website/common/script/ops/buyGear.js b/website/common/script/ops/buy/buyGear.js similarity index 85% rename from website/common/script/ops/buyGear.js rename to website/common/script/ops/buy/buyGear.js index cab4be405e..7250f74e3f 100644 --- a/website/common/script/ops/buyGear.js +++ b/website/common/script/ops/buy/buyGear.js @@ -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'); diff --git a/website/common/script/ops/buyHealthPotion.js b/website/common/script/ops/buy/buyHealthPotion.js similarity index 76% rename from website/common/script/ops/buyHealthPotion.js rename to website/common/script/ops/buy/buyHealthPotion.js index 8ca087817a..81ce8bc184 100644 --- a/website/common/script/ops/buyHealthPotion.js +++ b/website/common/script/ops/buy/buyHealthPotion.js @@ -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, }); } diff --git a/website/common/script/ops/buyMysterySet.js b/website/common/script/ops/buy/buyMysterySet.js similarity index 81% rename from website/common/script/ops/buyMysterySet.js rename to website/common/script/ops/buy/buyMysterySet.js index 7e0edc90ec..3a8b17a011 100644 --- a/website/common/script/ops/buyMysterySet.js +++ b/website/common/script/ops/buy/buyMysterySet.js @@ -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) { diff --git a/website/common/script/ops/buyQuest.js b/website/common/script/ops/buy/buyQuest.js similarity index 81% rename from website/common/script/ops/buyQuest.js rename to website/common/script/ops/buy/buyQuest.js index 289286670a..f04504fdd3 100644 --- a/website/common/script/ops/buyQuest.js +++ b/website/common/script/ops/buy/buyQuest.js @@ -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, diff --git a/website/common/script/ops/buy/buySpecialSpell.js b/website/common/script/ops/buy/buySpecialSpell.js new file mode 100644 index 0000000000..d6a8492c81 --- /dev/null +++ b/website/common/script/ops/buy/buySpecialSpell.js @@ -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), + ]; +}; diff --git a/website/common/script/ops/hourglassPurchase.js b/website/common/script/ops/buy/hourglassPurchase.js similarity index 93% rename from website/common/script/ops/hourglassPurchase.js rename to website/common/script/ops/buy/hourglassPurchase.js index a9cc9d50fa..d9afc4b440 100644 --- a/website/common/script/ops/hourglassPurchase.js +++ b/website/common/script/ops/buy/hourglassPurchase.js @@ -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'); diff --git a/website/common/script/ops/purchase.js b/website/common/script/ops/buy/purchase.js similarity index 93% rename from website/common/script/ops/purchase.js rename to website/common/script/ops/buy/purchase.js index 27fac8cf32..093fdfb59c 100644 --- a/website/common/script/ops/purchase.js +++ b/website/common/script/ops/buy/purchase.js @@ -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; diff --git a/website/common/script/ops/buy/purchaseWithSpell.js b/website/common/script/ops/buy/purchaseWithSpell.js new file mode 100644 index 0000000000..7f0a087190 --- /dev/null +++ b/website/common/script/ops/buy/purchaseWithSpell.js @@ -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); +}; diff --git a/website/common/script/ops/buySpecialSpell.js b/website/common/script/ops/buySpecialSpell.js deleted file mode 100644 index 3b3c588114..0000000000 --- a/website/common/script/ops/buySpecialSpell.js +++ /dev/null @@ -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), - ]; -}; diff --git a/website/common/script/ops/index.js b/website/common/script/ops/index.js index 306e21e865..dadecd6815 100644 --- a/website/common/script/ops/index.js +++ b/website/common/script/ops/index.js @@ -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, diff --git a/website/common/script/ops/purchaseWithSpell.js b/website/common/script/ops/purchaseWithSpell.js deleted file mode 100644 index 1e0872afaa..0000000000 --- a/website/common/script/ops/purchaseWithSpell.js +++ /dev/null @@ -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); -}; diff --git a/website/server/controllers/api-v3/user.js b/website/server/controllers/api-v3/user.js index 23b7c5de27..4d3815dcaf 100644 --- a/website/server/controllers/api-v3/user.js +++ b/website/server/controllers/api-v3/user.js @@ -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); },