diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 73fbcca279..7e763f56e0 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -136,5 +136,6 @@ "notAccteptedType": "Type must be in [eggs, hatchingPotions, food, quests, gear]", "contentKeyNotFound": "Key not found for Content <%= type %>", "plusOneGem": "+1 Gem", - "purchased": "You purchsed a <%= key %> <%= type %>" + "purchased": "You purchsed a <%= key %> <%= type %>", + "notAllowedHourglass": "Pet/Mount not available for purchase with Mystic Hourglass." } diff --git a/common/locales/en/subscriber.json b/common/locales/en/subscriber.json index 087213d679..0e9158e878 100644 --- a/common/locales/en/subscriber.json +++ b/common/locales/en/subscriber.json @@ -113,7 +113,7 @@ "hourglassBuyItemConfirm": "Buy this item for 1 Mystic Hourglass?", "petsAlreadyOwned": "Pet already owned.", "mountsAlreadyOwned": "Mount already owned.", - "typeNotAllowedHourglass": "Item type not supported for purchase with Mystic Hourglass. Allowed types: ", + "typeNotAllowedHourglass": "Item type not supported for purchase with Mystic Hourglass. Allowed types: <%= allowedTypes %>", "petsNotAllowedHourglass": "Pet not available for purchase with Mystic Hourglass.", "mountsNotAllowedHourglass": "Mount not available for purchase with Mystic Hourglass.", "hourglassPurchase": "Purchased an item using a Mystic Hourglass!", diff --git a/common/script/index.js b/common/script/index.js index 8edc435cae..7d3bc21314 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -115,6 +115,7 @@ import equip from './ops/equip'; import changeClass from './ops/changeClass'; import disableClasses from './ops/disableClasses'; import purchase from './ops/purchase'; +import purchaseHourglass from './ops/hourglassPurchase'; api.ops = { scoreTask, @@ -131,6 +132,7 @@ api.ops = { changeClass, disableClasses, purchase, + purchaseHourglass, }; import handleTwoHanded from './fns/handleTwoHanded'; diff --git a/common/script/ops/hourglassPurchase.js b/common/script/ops/hourglassPurchase.js index b97f581bb6..b627704d7b 100644 --- a/common/script/ops/hourglassPurchase.js +++ b/common/script/ops/hourglassPurchase.js @@ -3,53 +3,58 @@ import i18n from '../i18n'; import _ from 'lodash'; import splitWhitespace from '../libs/splitWhitespace'; import pickDeep from '../libs/pickDeep'; +import { + BadRequest, + NotAuthorized, +} from '../libs/errors'; + +module.exports = function purchaseHourglass (user, req = {}, analytics) { + let key = _.get(req, 'params.key'); + if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language)); + + let type = _.get(req, 'params.type'); + if (!type) throw new BadRequest(i18n.t('missingTypeParam', req.language)); -module.exports = function(user, req, cb, analytics) { - var analyticsData, key, ref, type; - ref = req.params, type = ref.type, key = ref.key; if (!content.timeTravelStable[type]) { - return typeof cb === "function" ? cb({ - code: 403, - message: i18n.t('typeNotAllowedHourglass', req.language) + JSON.stringify(_.keys(content.timeTravelStable)) - }) : void 0; + throw new NotAuthorized(i18n.t('typeNotAllowedHourglass', {allowedTypes: _.keys(content.timeTravelStable).toString()}, req.language)); } + if (!_.contains(_.keys(content.timeTravelStable[type]), key)) { - return typeof cb === "function" ? cb({ - code: 403, - message: i18n.t(type + 'NotAllowedHourglass', req.language) - }) : void 0; + throw new NotAuthorized(i18n.t('notAllowedHourglass', req.language)); } + if (user.items[type][key]) { - return typeof cb === "function" ? cb({ - code: 403, - message: i18n.t(type + 'AlreadyOwned', req.language) - }) : void 0; + throw new NotAuthorized(i18n.t(`${type}AlreadyOwned`, req.language)); } - if (!(user.purchased.plan.consecutive.trinkets > 0)) { - return typeof cb === "function" ? cb({ - code: 403, - message: i18n.t('notEnoughHourglasses', req.language) - }) : void 0; + + if (user.purchased.plan.consecutive.trinkets <= 0) { + throw new NotAuthorized(i18n.t('notEnoughHourglasses', req.language)); } + user.purchased.plan.consecutive.trinkets--; + if (type === 'pets') { user.items.pets[key] = 5; } + if (type === 'mounts') { user.items.mounts[key] = true; } - analyticsData = { - uuid: user._id, - itemKey: key, - itemType: type, - acquireMethod: 'Hourglass', - category: 'behavior' - }; - if (analytics != null) { - analytics.track('acquire item', analyticsData); + + if (analytics) { + analytics.track('acquire item', { + uuid: user._id, + itemKey: key, + itemType: type, + acquireMethod: 'Hourglass', + category: 'behavior', + }); } - return typeof cb === "function" ? cb({ - code: 200, - message: i18n.t('hourglassPurchase', req.language) - }, pickDeep(user, splitWhitespace('items purchased.plan.consecutive'))) : void 0; + + let res = { + data: pickDeep(user, splitWhitespace('items purchased.plan.consecutive')), + message: i18n.t('hourglassPurchase', req.language), + }; + + return res; }; diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index 6208dfbeb5..d00294b83c 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -28,7 +28,6 @@ const COMMON_FILES = [ '!./common/script/ops/deleteWebhook.js', '!./common/script/ops/getTag.js', '!./common/script/ops/getTags.js', - '!./common/script/ops/hourglassPurchase.js', '!./common/script/ops/openMysteryItem.js', '!./common/script/ops/readCard.js', '!./common/script/ops/rebirth.js', diff --git a/test/api/v3/integration/user/POST-user_purchase_hourglass.test.js b/test/api/v3/integration/user/POST-user_purchase_hourglass.test.js new file mode 100644 index 0000000000..cd43334d00 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_purchase_hourglass.test.js @@ -0,0 +1,25 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/purchase-hourglass/:type/:key', () => { + let user; + + beforeEach(async () => { + user = await generateUser({ + 'purchased.plan.consecutive.trinkets': 2, + }); + }); + + // More tests in common code unit tests + + it('buys a hourglass pet', async () => { + let response = await user.post('/user/purchase-hourglass/pets/MantisShrimp-Base'); + await user.sync(); + + expect(response.message).to.eql(t('hourglassPurchase')); + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); + expect(user.items.pets).to.eql({'MantisShrimp-Base': 5}); + }); +}); diff --git a/test/common/ops/hourglassPurchase.js b/test/common/ops/hourglassPurchase.js new file mode 100644 index 0000000000..258e0c6f01 --- /dev/null +++ b/test/common/ops/hourglassPurchase.js @@ -0,0 +1,145 @@ +import hourglassPurchase from '../../../common/script/ops/hourglassPurchase'; +import { + BadRequest, + NotAuthorized, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; +import content from '../../../common/script/content/index'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('user.ops.hourglassPurchase', () => { + let user; + + beforeEach(() => { + user = generateUser(); + }); + + context('failure conditions', () => { + it('return error when key is not provided', (done) => { + try { + hourglassPurchase(user, {params: {}}); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.eql(i18n.t('missingKeyParam')); + done(); + } + }); + + it('returns error when type is not provided', (done) => { + try { + hourglassPurchase(user, {params: {key: 'Base'}}); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.eql(i18n.t('missingTypeParam')); + done(); + } + }); + + it('returns error when inccorect type is provided', (done) => { + try { + hourglassPurchase(user, {params: {type: 'notAType', key: 'MantisShrimp-Base'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.eql(i18n.t('typeNotAllowedHourglass', {allowedTypes: _.keys(content.timeTravelStable).toString()})); + done(); + } + }); + + it('does not grant to pets without Mystic Hourglasses', (done) => { + try { + hourglassPurchase(user, {params: {type: 'pets', key: 'MantisShrimp-Base'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.eql(i18n.t('notEnoughHourglasses')); + done(); + } + }); + + it('does not grant to mounts without Mystic Hourglasses', (done) => { + try { + hourglassPurchase(user, {params: {type: 'mounts', key: 'MantisShrimp-Base'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.eql(i18n.t('notEnoughHourglasses')); + done(); + } + }); + + it('does not grant pet that is not part of the Time Travel Stable', (done) => { + user.purchased.plan.consecutive.trinkets = 1; + + try { + hourglassPurchase(user, {params: {type: 'pets', key: 'Wolf-Veteran'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.eql(i18n.t('notAllowedHourglass')); + done(); + } + }); + + it('does not grant mount that is not part of the Time Travel Stable', (done) => { + user.purchased.plan.consecutive.trinkets = 1; + + try { + hourglassPurchase(user, {params: {type: 'mounts', key: 'Orca-Base'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.eql(i18n.t('notAllowedHourglass')); + done(); + } + }); + + it('does not grant pet that has already been purchased', (done) => { + user.purchased.plan.consecutive.trinkets = 1; + user.items.pets = { + 'MantisShrimp-Base': true, + }; + + try { + hourglassPurchase(user, {params: {type: 'pets', key: 'MantisShrimp-Base'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.eql(i18n.t('petsAlreadyOwned')); + done(); + } + }); + + it('does not grant mount that has already been purchased', (done) => { + user.purchased.plan.consecutive.trinkets = 1; + user.items.mounts = { + 'MantisShrimp-Base': true, + }; + + try { + hourglassPurchase(user, {params: {type: 'mounts', key: 'MantisShrimp-Base'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.eql(i18n.t('mountsAlreadyOwned')); + done(); + } + }); + }); + + context('successful purchases', () => { + it('buys a pet', () => { + user.purchased.plan.consecutive.trinkets = 2; + + let response = hourglassPurchase(user, {params: {type: 'pets', key: 'MantisShrimp-Base'}}); + + expect(response.message).to.eql(i18n.t('hourglassPurchase')); + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); + expect(user.items.pets).to.eql({'MantisShrimp-Base': 5}); + }); + + it('buys a mount', () => { + user.purchased.plan.consecutive.trinkets = 2; + + let response = hourglassPurchase(user, {params: {type: 'mounts', key: 'MantisShrimp-Base'}}); + expect(response.message).to.eql(i18n.t('hourglassPurchase')); + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); + expect(user.items.mounts).to.eql({'MantisShrimp-Base': true}); + }); + }); +}); diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index 8dcf87cad9..904fcf34a2 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -699,4 +699,27 @@ api.purchase = { }, }; +/** +* @api {post} /user/purchase-hourglass/:type/:key Purchase Hourglass. +* @apiVersion 3.0.0 +* @apiName UserPurchaseHourglass +* @apiGroup User +* +* @apiParam {string} type {pets|mounts}. The type of item to purchase +* @apiParam {string} key Ex: {MantisShrimp-Base}. The key for the mount/pet +* +* @apiSuccess {Object} data `items purchased.plan.consecutive` +*/ +api.userPurchaseHourglass = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/purchase-hourglass/:type/:key', + async handler (req, res) { + let user = res.locals.user; + let purchaseHourglassResponse = common.ops.purchaseHourglass(user, req, res.analytics); + await user.save(); + res.respond(200, purchaseHourglassResponse); + }, +}; + module.exports = api;