mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 06:07:21 +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: 'eggs', key: 'Wolf'});
|
||||||
user.pinnedItems.push({type: 'hatchingPotions', key: 'Base'});
|
user.pinnedItems.push({type: 'hatchingPotions', key: 'Base'});
|
||||||
user.pinnedItems.push({type: 'food', key: SEASONAL_FOOD});
|
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: 'gear', key: 'headAccessory_special_tigerEars'});
|
||||||
user.pinnedItems.push({type: 'bundles', key: 'featheredFriends'});
|
user.pinnedItems.push({type: 'bundles', key: 'featheredFriends'});
|
||||||
});
|
});
|
||||||
@@ -157,16 +156,6 @@ describe('shared.ops.purchase', () => {
|
|||||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
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', () => {
|
it('purchases gear', () => {
|
||||||
let type = 'gear';
|
let type = 'gear';
|
||||||
let key = 'headAccessory_special_tigerEars';
|
let key = 'headAccessory_special_tigerEars';
|
||||||
|
|||||||
@@ -98,6 +98,7 @@
|
|||||||
"guildQuestsNotSupported": "Guilds cannot be invited on quests.",
|
"guildQuestsNotSupported": "Guilds cannot be invited on quests.",
|
||||||
"questNotOwned": "You don't own that quest scroll.",
|
"questNotOwned": "You don't own that quest scroll.",
|
||||||
"questNotGoldPurchasable": "Quest \"<%= key %>\" is not a Gold-purchasable quest.",
|
"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.",
|
"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.",
|
"questAlreadyUnderway": "Your party is already on a quest. Try again when the current quest has ended.",
|
||||||
"questAlreadyAccepted": "You already accepted the quest invitation.",
|
"questAlreadyAccepted": "You already accepted the quest invitation.",
|
||||||
|
|||||||
@@ -24,6 +24,24 @@ export class AbstractBuyOperation {
|
|||||||
if (isNaN(this.quantity)) throw new BadRequest(this.i18n('invalidQuantity'));
|
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`
|
* Shortcut to get the translated string without passing `req.language`
|
||||||
* @param {String} key - translation key
|
* @param {String} key - translation key
|
||||||
@@ -100,14 +118,6 @@ export class AbstractGoldItemOperation extends AbstractBuyOperation {
|
|||||||
super(user, req, analytics);
|
super(user, req, analytics);
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemValue (item) {
|
|
||||||
return item.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
getIemKey (item) {
|
|
||||||
return item.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
canUserPurchase (user, item) {
|
canUserPurchase (user, item) {
|
||||||
this.item = item;
|
this.item = item;
|
||||||
let itemValue = this.getItemValue(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 hourglassPurchase from './hourglassPurchase';
|
||||||
import errorMessage from '../../libs/errorMessage';
|
import errorMessage from '../../libs/errorMessage';
|
||||||
import {BuyGemOperation} from './buyGem';
|
import {BuyGemOperation} from './buyGem';
|
||||||
|
import {BuyQuestWithGemOperation} from './buyQuestGem';
|
||||||
|
|
||||||
// @TODO: remove the req option style. Dependency on express structure is an anti-pattern
|
// @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
|
// 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();
|
buyRes = buyOp.purchase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'quests': {
|
||||||
|
const buyOp = new BuyQuestWithGemOperation(user, req, analytics);
|
||||||
|
|
||||||
|
buyRes = buyOp.purchase();
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'eggs':
|
case 'eggs':
|
||||||
case 'hatchingPotions':
|
case 'hatchingPotions':
|
||||||
case 'food':
|
case 'food':
|
||||||
case 'quests':
|
|
||||||
case 'gear':
|
case 'gear':
|
||||||
case 'bundles':
|
case 'bundles':
|
||||||
buyRes = purchaseOp(user, req, analytics);
|
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'];
|
const singlePurchaseTypes = ['gear'];
|
||||||
module.exports = function purchase (user, req = {}, analytics) {
|
module.exports = function purchase (user, req = {}, analytics) {
|
||||||
let type = get(req.params, 'type');
|
let type = get(req.params, 'type');
|
||||||
|
|||||||
Reference in New Issue
Block a user