diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 7af8dc9af0..2f3ea4052a 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -160,7 +160,8 @@ "sold": "You sold a <%= key %> <%= type %>", "pathRequired": "Path string is required", "unlocked": "Items have been unlocked", - "alreadyUnlocked": "Item already unlocked", + "alreadyUnlocked": "Full set already unlocked.", + "alreadyUnlockedPart": "Full set already partially unlocked.", "cannotRevive": "Cannot revive if not dead", "rebirthComplete": "You have been reborn!", "petNotOwned": "You do not own this pet.", diff --git a/common/script/ops/unlock.js b/common/script/ops/unlock.js index 08ec175dcc..e7907f898b 100644 --- a/common/script/ops/unlock.js +++ b/common/script/ops/unlock.js @@ -6,6 +6,8 @@ import { BadRequest, } from '../libs/errors'; +// If item is already purchased -> equip it +// Otherwise unlock it module.exports = function unlock (user, req = {}, analytics) { let path = _.get(req.query, 'path'); @@ -14,9 +16,9 @@ module.exports = function unlock (user, req = {}, analytics) { } let isFullSet = path.indexOf(',') !== -1; - let cost; let isBackground = path.indexOf('background.') !== -1; + let cost; if (isBackground && isFullSet) { cost = 3.75; } else if (isBackground) { @@ -27,21 +29,39 @@ module.exports = function unlock (user, req = {}, analytics) { cost = 0.5; } - let alreadyOwns = !isFullSet && _.get(user, `purchased.${path}`) === true; + let setPaths; + let alreadyOwns; + + if (isFullSet) { + setPaths = path.split(','); + let alreadyOwnedItems = 0; + + _.each(setPaths, singlePath => { + if (_.get(user, `purchased.${singlePath}`) === true) { + alreadyOwnedItems++; + } + }); + + if (alreadyOwnedItems === setPaths.length) { + throw new NotAuthorized(i18n.t('alreadyUnlocked', req.language)); + } else if (alreadyOwnedItems > 0) { + throw new NotAuthorized(i18n.t('alreadyUnlockedPart', req.language)); + } + } else { + alreadyOwns = _.get(user, `purchased.${path}`) === true; + } if ((!user.balance || user.balance < cost) && !alreadyOwns) { throw new NotAuthorized(i18n.t('notEnoughGems', req.language)); } if (isFullSet) { - _.each(path.split(','), function markItemsAsPurchased (pathPart) { + _.each(setPaths, function markItemsAsPurchased (pathPart) { if (path.indexOf('gear.') !== -1) { _.set(user, pathPart, true); - return true; } _.set(user, `purchased.${pathPart}`, true); - return true; }); } else { if (alreadyOwns) { @@ -51,35 +71,38 @@ module.exports = function unlock (user, req = {}, analytics) { if (key === 'background' && value === user.preferences.background) { value = ''; } + _.set(user, `preferences.${key}`, value); - - throw new NotAuthorized(i18n.t('alreadyUnlocked', req.language)); + } else { + _.set(user, `purchased.${path}`, true); } - _.set(user, `purchased.${path}`, true); } - if (path.indexOf('gear.') === -1) { - user.markModified('purchased'); - } + if (!alreadyOwns) { + if (path.indexOf('gear.') === -1) { + user.markModified('purchased'); + } - user.balance -= cost; + user.balance -= cost; - if (analytics) { - analytics.track('acquire item', { - uuid: user._id, - itemKey: path, - itemType: 'customization', - acquireMethod: 'Gems', - gemCost: cost / 0.25, - category: 'behavior', - }); + if (analytics) { + analytics.track('acquire item', { + uuid: user._id, + itemKey: path, + itemType: 'customization', + acquireMethod: 'Gems', + gemCost: cost / 0.25, + category: 'behavior', + }); + } } let response = { data: _.pick(user, splitWhitespace('purchased preferences items')), - message: i18n.t('unlocked'), }; + if (!alreadyOwns) response.message = i18n.t('unlocked', req.language); + if (req.v2 === true) { return response.data; } else { diff --git a/test/common/ops/unlock.js b/test/common/ops/unlock.js index b7d7bda370..2f9f7d3aea 100644 --- a/test/common/ops/unlock.js +++ b/test/common/ops/unlock.js @@ -43,10 +43,10 @@ describe('shared.ops.unlock', () => { } }); - it('returns an error when user already owns an item', (done) => { + it('returns an error when user already owns a full set', (done) => { try { - unlock(user, {query: {path: backgroundUnlockPath}}); - unlock(user, {query: {path: backgroundUnlockPath}}); + unlock(user, {query: {path: unlockPath}}); + unlock(user, {query: {path: unlockPath}}); } catch (err) { expect(err).to.be.an.instanceof(NotAuthorized); expect(err.message).to.equal(i18n.t('alreadyUnlocked')); @@ -54,6 +54,42 @@ describe('shared.ops.unlock', () => { } }); + it('returns an error when user already owns items in a full set', (done) => { + try { + unlock(user, {query: {path: unlockPath}}); + unlock(user, {query: {path: unlockPath}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('alreadyUnlocked')); + done(); + } + }); + + it('equips an item already owned', () => { + expect(user.purchased.background.giant_florals).to.not.exists; + + unlock(user, {query: {path: backgroundUnlockPath}}); + let afterBalance = user.balance; + let response = unlock(user, {query: {path: backgroundUnlockPath}}); + expect(user.balance).to.equal(afterBalance); // do not bill twice + + expect(response.message).to.not.exists; + expect(user.preferences.background).to.equal('giant_florals'); + }); + + it('un-equips an item already equipped', () => { + expect(user.purchased.background.giant_florals).to.not.exists; + + unlock(user, {query: {path: backgroundUnlockPath}}); // unlock + let afterBalance = user.balance; + unlock(user, {query: {path: backgroundUnlockPath}}); // equip + let response = unlock(user, {query: {path: backgroundUnlockPath}}); + expect(user.balance).to.equal(afterBalance); // do not bill twice + + expect(response.message).to.not.exists; + expect(user.preferences.background).to.equal(''); + }); + it('unlocks a full set', () => { let response = unlock(user, {query: {path: unlockPath}});