mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 14:17:22 +01:00
AbstractGemItemOperation - BuyQuestWithGemOperation (#10476)
This commit is contained in:
94
test/common/ops/buy/buyQuestGems.js
Normal file
94
test/common/ops/buy/buyQuestGems.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
57
website/common/script/ops/buy/buyQuestGem.js
Normal file
57
website/common/script/ops/buy/buyQuestGem.js
Normal 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),
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user