mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 21:57:22 +01:00
Purchase API Refactoring: Gems [Gold] (#10271)
* remove `keyRequired` - change to `missingKeyParam` - i18n-string * extract & convert buyGemsOperation * fix lint
This commit is contained in:
@@ -74,6 +74,10 @@ export class AbstractBuyOperation {
|
||||
return resultObj;
|
||||
}
|
||||
|
||||
analyticsLabel () {
|
||||
return 'acquire item';
|
||||
}
|
||||
|
||||
sendToAnalytics (additionalData = {}) {
|
||||
// spread-operator produces an "unexpected token" error
|
||||
let analyticsData = _merge(additionalData, {
|
||||
@@ -87,7 +91,7 @@ export class AbstractBuyOperation {
|
||||
analyticsData.quantityPurchased = this.quantity;
|
||||
}
|
||||
|
||||
this.analytics.track('acquire item', analyticsData);
|
||||
this.analytics.track(this.analyticsLabel(), analyticsData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +104,10 @@ export class AbstractGoldItemOperation extends AbstractBuyOperation {
|
||||
return item.value;
|
||||
}
|
||||
|
||||
getIemKey (item) {
|
||||
return item.key;
|
||||
}
|
||||
|
||||
canUserPurchase (user, item) {
|
||||
this.item = item;
|
||||
let itemValue = this.getItemValue(item);
|
||||
@@ -110,20 +118,20 @@ export class AbstractGoldItemOperation extends AbstractBuyOperation {
|
||||
throw new NotAuthorized(this.i18n('messageNotEnoughGold'));
|
||||
}
|
||||
|
||||
if (item.canOwn && !item.canOwn(user)) {
|
||||
if (item && item.canOwn && !item.canOwn(user)) {
|
||||
throw new NotAuthorized(this.i18n('cannotBuyItem'));
|
||||
}
|
||||
}
|
||||
|
||||
subtractCurrency (user, item, quantity = 1) {
|
||||
subtractCurrency (user, item) {
|
||||
let itemValue = this.getItemValue(item);
|
||||
|
||||
user.stats.gp -= itemValue * quantity;
|
||||
user.stats.gp -= itemValue * this.quantity;
|
||||
}
|
||||
|
||||
analyticsData () {
|
||||
return {
|
||||
itemKey: this.item.key,
|
||||
itemKey: this.getIemKey(this.item),
|
||||
itemType: 'Market',
|
||||
acquireMethod: 'Gold',
|
||||
goldCost: this.getItemValue(this.item),
|
||||
|
||||
@@ -11,6 +11,7 @@ import {BuyQuestWithGoldOperation} from './buyQuest';
|
||||
import buySpecialSpell from './buySpecialSpell';
|
||||
import purchaseOp from './purchase';
|
||||
import hourglassPurchase from './hourglassPurchase';
|
||||
import {BuyGemOperation} from './buyGem';
|
||||
|
||||
// @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
|
||||
@@ -45,13 +46,18 @@ module.exports = function buy (user, req = {}, analytics) {
|
||||
buyRes = buyOp.purchase();
|
||||
break;
|
||||
}
|
||||
case 'gems': {
|
||||
const buyOp = new BuyGemOperation(user, req, analytics);
|
||||
|
||||
buyRes = buyOp.purchase();
|
||||
break;
|
||||
}
|
||||
case 'eggs':
|
||||
case 'hatchingPotions':
|
||||
case 'food':
|
||||
case 'quests':
|
||||
case 'gear':
|
||||
case 'bundles':
|
||||
case 'gems':
|
||||
buyRes = purchaseOp(user, req, analytics);
|
||||
break;
|
||||
case 'pets':
|
||||
|
||||
81
website/common/script/ops/buy/buyGem.js
Normal file
81
website/common/script/ops/buy/buyGem.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import pick from 'lodash/pick';
|
||||
import splitWhitespace from '../../libs/splitWhitespace';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../libs/errors';
|
||||
import {AbstractGoldItemOperation} from './abstractBuyOperation';
|
||||
import get from 'lodash/get';
|
||||
import planGemLimits from '../../libs/planGemLimits';
|
||||
|
||||
export class BuyGemOperation extends AbstractGoldItemOperation {
|
||||
constructor (user, req, analytics) {
|
||||
super(user, req, analytics);
|
||||
}
|
||||
|
||||
multiplePurchaseAllowed () {
|
||||
return true;
|
||||
}
|
||||
|
||||
getItemValue () {
|
||||
return planGemLimits.convRate;
|
||||
}
|
||||
|
||||
getIemKey () {
|
||||
return 'gem';
|
||||
}
|
||||
|
||||
extractAndValidateParams (user, req) {
|
||||
let key = this.key = get(req, 'params.key');
|
||||
if (!key) throw new BadRequest(this.i18n('missingKeyParam'));
|
||||
|
||||
let convCap = planGemLimits.convCap;
|
||||
convCap += user.purchased.plan.consecutive.gemCapExtra;
|
||||
|
||||
// todo better name?
|
||||
this.convCap = convCap;
|
||||
|
||||
this.canUserPurchase(user);
|
||||
}
|
||||
|
||||
canUserPurchase (user, item) {
|
||||
if (!user.purchased || !user.purchased.plan || !user.purchased.plan.customerId) {
|
||||
throw new NotAuthorized(this.i18n('mustSubscribeToPurchaseGems'));
|
||||
}
|
||||
|
||||
super.canUserPurchase(user, item);
|
||||
|
||||
if (user.purchased.plan.gemsBought >= this.convCap) {
|
||||
throw new NotAuthorized(this.i18n('reachedGoldToGemCap', {convCap: this.convCap}));
|
||||
}
|
||||
|
||||
if (user.purchased.plan.gemsBought + this.quantity >= this.convCap) {
|
||||
throw new NotAuthorized(this.i18n('reachedGoldToGemCapQuantity', {
|
||||
convCap: this.convCap,
|
||||
quantity: this.quantity,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
executeChanges (user, item) {
|
||||
user.balance += 0.25 * this.quantity;
|
||||
user.purchased.plan.gemsBought += this.quantity;
|
||||
|
||||
this.subtractCurrency(user, item);
|
||||
|
||||
return [
|
||||
pick(user, splitWhitespace('stats balance')),
|
||||
this.i18n('plusGem', {count: this.quantity}),
|
||||
];
|
||||
}
|
||||
|
||||
analyticsLabel () {
|
||||
return 'purchase gems';
|
||||
}
|
||||
|
||||
analyticsData () {
|
||||
let data = super.analyticsData();
|
||||
data.itemKey = 'gem';
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import get from 'lodash/get';
|
||||
import pick from 'lodash/pick';
|
||||
import forEach from 'lodash/forEach';
|
||||
import splitWhitespace from '../../libs/splitWhitespace';
|
||||
import planGemLimits from '../../libs/planGemLimits';
|
||||
import {
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
@@ -14,48 +13,6 @@ import {
|
||||
import { removeItemByPath } from '../pinnedGearUtils';
|
||||
import getItemInfo from '../../libs/getItemInfo';
|
||||
|
||||
function buyGems (user, analytics, req, key) {
|
||||
let convRate = planGemLimits.convRate;
|
||||
let convCap = planGemLimits.convCap;
|
||||
convCap += user.purchased.plan.consecutive.gemCapExtra;
|
||||
|
||||
// Some groups limit their members ability to obtain gems
|
||||
// The check is async so it's done on the server (in server/controllers/api-v3/user#purchase)
|
||||
// only and not on the client,
|
||||
// resulting in a purchase that will seem successful until the request hit the server.
|
||||
if (!user.purchased || !user.purchased.plan || !user.purchased.plan.customerId) {
|
||||
throw new NotAuthorized(i18n.t('mustSubscribeToPurchaseGems', req.language));
|
||||
}
|
||||
|
||||
if (user.stats.gp < convRate) {
|
||||
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
|
||||
}
|
||||
|
||||
if (user.purchased.plan.gemsBought >= convCap) {
|
||||
throw new NotAuthorized(i18n.t('reachedGoldToGemCap', {convCap}, req.language));
|
||||
}
|
||||
|
||||
user.balance += 0.25;
|
||||
user.purchased.plan.gemsBought++;
|
||||
user.stats.gp -= convRate;
|
||||
|
||||
if (analytics) {
|
||||
analytics.track('purchase gems', {
|
||||
uuid: user._id,
|
||||
itemKey: key,
|
||||
acquireMethod: 'Gold',
|
||||
goldCost: convRate,
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
pick(user, splitWhitespace('stats balance')),
|
||||
i18n.t('plusOneGem', req.language),
|
||||
];
|
||||
}
|
||||
|
||||
function getItemAndPrice (user, type, key, req) {
|
||||
let item;
|
||||
let price;
|
||||
@@ -120,15 +77,7 @@ module.exports = function purchase (user, req = {}, analytics) {
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new BadRequest(i18n.t('keyRequired', req.language));
|
||||
}
|
||||
|
||||
if (type === 'gems' && key === 'gem') {
|
||||
let gemResponse;
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
gemResponse = buyGems(user, analytics, req, key);
|
||||
}
|
||||
return gemResponse;
|
||||
throw new BadRequest(i18n.t('missingKeyParam', req.language));
|
||||
}
|
||||
|
||||
if (!acceptedTypes.includes(type)) {
|
||||
|
||||
@@ -26,7 +26,7 @@ module.exports = function sell (user, req = {}) {
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new BadRequest(i18n.t('keyRequired', req.language));
|
||||
throw new BadRequest(i18n.t('missingKeyParam', req.language));
|
||||
}
|
||||
|
||||
if (ACCEPTEDTYPES.indexOf(type) === -1) {
|
||||
|
||||
Reference in New Issue
Block a user