AbstractGemItemOperation - BuyQuestWithGemOperation (#10476)

This commit is contained in:
negue
2018-06-28 12:37:21 +02:00
committed by Matteo Pagliazzi
parent 487523f64b
commit d9b573b430
7 changed files with 212 additions and 21 deletions

View File

@@ -0,0 +1,94 @@
import pinnedGearUtils from '../../../../website/common/script/ops/pinnedGearUtils';
import {
NotAuthorized,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import {
generateUser,
} from '../../../helpers/common.helper';
import {BuyQuestWithGemOperation} from '../../../../website/common/script/ops/buy/buyQuestGem';
describe('shared.ops.buyQuestGems', () => {
let user;
let goldPoints = 40;
let analytics = {track () {}};
function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGemOperation(_user, _req, _analytics);
return buyOp.purchase();
}
before(() => {
user = generateUser({'stats.class': 'rogue'});
});
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
});
context('successful purchase', () => {
let userGemAmount = 10;
before(() => {
user.balance = userGemAmount;
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = 0;
user.purchased.plan.customerId = 'customer-id';
user.pinnedItems.push({type: 'quests', key: 'gryphon'});
});
it('purchases quests', () => {
let key = 'gryphon';
buyQuest(user, {params: {key}});
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
});
context('bulk purchase', () => {
let userGemAmount = 10;
beforeEach(() => {
user.balance = userGemAmount;
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = 0;
user.purchased.plan.customerId = 'customer-id';
});
it('errors when user does not have enough gems', (done) => {
user.balance = 1;
let key = 'gryphon';
try {
buyQuest(user, {
params: {key},
quantity: 2,
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notEnoughGems'));
done();
}
});
it('makes bulk purchases of quests', () => {
let key = 'gryphon';
buyQuest(user, {
params: {key},
quantity: 3,
});
expect(user.items.quests[key]).to.equal(4);
});
});
});

View File

@@ -121,7 +121,6 @@ describe('shared.ops.purchase', () => {
user.pinnedItems.push({type: 'eggs', key: 'Wolf'});
user.pinnedItems.push({type: 'hatchingPotions', key: 'Base'});
user.pinnedItems.push({type: 'food', key: SEASONAL_FOOD});
user.pinnedItems.push({type: 'quests', key: 'gryphon'});
user.pinnedItems.push({type: 'gear', key: 'headAccessory_special_tigerEars'});
user.pinnedItems.push({type: 'bundles', key: 'featheredFriends'});
});
@@ -157,16 +156,6 @@ describe('shared.ops.purchase', () => {
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases quests', () => {
let type = 'quests';
let key = 'gryphon';
purchase(user, {params: {type, key}});
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases gear', () => {
let type = 'gear';
let key = 'headAccessory_special_tigerEars';

View File

@@ -98,6 +98,7 @@
"guildQuestsNotSupported": "Guilds cannot be invited on quests.",
"questNotOwned": "You don't own that quest scroll.",
"questNotGoldPurchasable": "Quest \"<%= key %>\" is not a Gold-purchasable quest.",
"questNotGemPurchasable": "Quest \"<%= key %>\" is not a Gem-purchasable quest.",
"questLevelTooHigh": "You must be level <%= level %> to begin this quest.",
"questAlreadyUnderway": "Your party is already on a quest. Try again when the current quest has ended.",
"questAlreadyAccepted": "You already accepted the quest invitation.",

View File

@@ -24,6 +24,24 @@ export class AbstractBuyOperation {
if (isNaN(this.quantity)) throw new BadRequest(this.i18n('invalidQuantity'));
}
/**
* Returns the item value
* @param item
* @returns {number}
*/
getItemValue (item) {
return item.value;
}
/**
* Returns the item key
* @param item
* @returns {String}
*/
getIemKey (item) {
return item.key;
}
/**
* Shortcut to get the translated string without passing `req.language`
* @param {String} key - translation key
@@ -100,14 +118,6 @@ export class AbstractGoldItemOperation extends AbstractBuyOperation {
super(user, req, analytics);
}
getItemValue (item) {
return item.value;
}
getIemKey (item) {
return item.key;
}
canUserPurchase (user, item) {
this.item = item;
let itemValue = this.getItemValue(item);
@@ -138,3 +148,37 @@ export class AbstractGoldItemOperation extends AbstractBuyOperation {
};
}
}
export class AbstractGemItemOperation extends AbstractBuyOperation {
constructor (user, req, analytics) {
super(user, req, analytics);
}
canUserPurchase (user, item) {
this.item = item;
let itemValue = this.getItemValue(item);
if (!item.canBuy(user)) {
throw new NotAuthorized(this.i18n('messageNotAvailable'));
}
if (!user.balance || user.balance < itemValue * this.quantity) {
throw new NotAuthorized(this.i18n('notEnoughGems'));
}
}
subtractCurrency (user, item) {
let itemValue = this.getItemValue(item);
user.balance -= itemValue * this.quantity;
}
analyticsData () {
return {
itemKey: this.getIemKey(this.item),
itemType: 'Market',
acquireMethod: 'Gems',
gemCost: this.getItemValue(this.item) * 4,
};
}
}

View File

@@ -12,6 +12,7 @@ import purchaseOp from './purchase';
import hourglassPurchase from './hourglassPurchase';
import errorMessage from '../../libs/errorMessage';
import {BuyGemOperation} from './buyGem';
import {BuyQuestWithGemOperation} from './buyQuestGem';
// @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
@@ -52,10 +53,15 @@ module.exports = function buy (user, req = {}, analytics) {
buyRes = buyOp.purchase();
break;
}
case 'quests': {
const buyOp = new BuyQuestWithGemOperation(user, req, analytics);
buyRes = buyOp.purchase();
break;
}
case 'eggs':
case 'hatchingPotions':
case 'food':
case 'quests':
case 'gear':
case 'bundles':
buyRes = purchaseOp(user, req, analytics);

View File

@@ -0,0 +1,57 @@
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../../libs/errors';
import content from '../../content/index';
import get from 'lodash/get';
import errorMessage from '../../libs/errorMessage';
import {AbstractGemItemOperation} from './abstractBuyOperation';
export class BuyQuestWithGemOperation extends AbstractGemItemOperation {
constructor (user, req, analytics) {
super(user, req, analytics);
}
multiplePurchaseAllowed () {
return true;
}
getItemKey () {
return this.key;
}
getItemValue (item) {
return item.value / 4;
}
extractAndValidateParams (user, req) {
let key = this.key = get(req, 'params.key');
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
let item = content.quests[key];
if (!item) throw new NotFound(errorMessage('questNotFound', {key}));
if (item.category === 'gold') {
throw new NotAuthorized(this.i18n('questNotGemPurchasable', {key}));
}
this.canUserPurchase(user, item);
}
executeChanges (user, item, req) {
user.items.quests[item.key] = user.items.quests[item.key] || 0;
user.items.quests[item.key] += this.quantity;
this.subtractCurrency(user, item, this.quantity);
return [
user.items.quests,
this.i18n('messageBought', {
itemText: item.text(req.language),
}),
];
}
}

View File

@@ -63,7 +63,7 @@ function purchaseItem (user, item, price, type, key) {
}
}
const acceptedTypes = ['eggs', 'hatchingPotions', 'food', 'quests', 'gear', 'bundles'];
const acceptedTypes = ['eggs', 'hatchingPotions', 'food', 'gear', 'bundles'];
const singlePurchaseTypes = ['gear'];
module.exports = function purchase (user, req = {}, analytics) {
let type = get(req.params, 'type');