diff --git a/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js b/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js index c7c38a4c67..f25a6c669f 100644 --- a/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js +++ b/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js @@ -114,6 +114,26 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => { await expect(winningUser.sync()).to.eventually.have.property('balance', oldBalance + challenge.prize / 4); }); + it('doesn\'t gives winner gems if group policy prevents it', async () => { + let oldBalance = winningUser.balance; + let oldLeaderBalance = (await groupLeader.sync()).balance; + + await winningUser.update({ + 'purchased.plan.customerId': 'group-plan', + }); + await group.update({ + 'leaderOnly.getGems': true, + 'purchased.plan.customerId': 123, + }); + + await groupLeader.post(`/challenges/${challenge._id}/selectWinner/${winningUser._id}`); + + await sleep(0.5); + + await expect(winningUser.sync()).to.eventually.have.property('balance', oldBalance); + await expect(groupLeader.sync()).to.eventually.have.property('balance', oldLeaderBalance + challenge.prize / 4); + }); + it('doesn\'t refund gems to group leader', async () => { let oldBalance = (await groupLeader.sync()).balance; diff --git a/test/api/v3/integration/user/POST-user_purchase.test.js b/test/api/v3/integration/user/POST-user_purchase.test.js index dff6d59c48..7db7600f60 100644 --- a/test/api/v3/integration/user/POST-user_purchase.test.js +++ b/test/api/v3/integration/user/POST-user_purchase.test.js @@ -1,5 +1,6 @@ import { generateUser, + createAndPopulateGroup, translate as t, } from '../../../../helpers/api-integration/v3'; @@ -31,4 +32,70 @@ describe('POST /user/purchase/:type/:key', () => { expect(user.items[type][key]).to.equal(1); }); + + it('can convert gold to gems if subscribed', async () => { + let oldBalance = user.balance; + await user.update({ + 'purchased.plan.customerId': 'group-plan', + 'stats.gp': 1000, + }); + await user.post('/user/purchase/gems/gem'); + await user.sync(); + expect(user.balance).to.equal(oldBalance + 0.25); + }); + + it('leader can convert gold to gems even if the group plan prevents it', async () => { + let { group, groupLeader } = await createAndPopulateGroup({ + groupDetails: { + name: 'test', + type: 'guild', + privacy: 'private', + }, + }); + await group.update({ + 'leaderOnly.getGems': true, + 'purchased.plan.customerId': 123, + }); + await groupLeader.sync(); + let oldBalance = groupLeader.balance; + + await groupLeader.update({ + 'purchased.plan.customerId': 'group-plan', + 'stats.gp': 1000, + }); + await groupLeader.post('/user/purchase/gems/gem'); + + await groupLeader.sync(); + expect(groupLeader.balance).to.equal(oldBalance + 0.25); + }); + + it('cannot convert gold to gems if the group plan prevents it', async () => { + let { group, members } = await createAndPopulateGroup({ + groupDetails: { + name: 'test', + type: 'guild', + privacy: 'private', + }, + members: 1, + }); + await group.update({ + 'leaderOnly.getGems': true, + 'purchased.plan.customerId': 123, + }); + let oldBalance = members[0].balance; + + await members[0].update({ + 'purchased.plan.customerId': 'group-plan', + 'stats.gp': 1000, + }); + await expect(members[0].post('/user/purchase/gems/gem')) + .to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('groupPolicyCannotGetGems'), + }); + + await members[0].sync(); + expect(members[0].balance).to.equal(oldBalance); + }); }); diff --git a/test/api/v3/unit/libs/amazonPayments.test.js b/test/api/v3/unit/libs/amazonPayments.test.js index 005b445e4d..757511121a 100644 --- a/test/api/v3/unit/libs/amazonPayments.test.js +++ b/test/api/v3/unit/libs/amazonPayments.test.js @@ -102,6 +102,7 @@ describe('Amazon Payments', () => { }); it('should purchase gems', async () => { + sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); await amzLib.checkout({user, orderReferenceId, headers}); expect(paymentBuyGemsStub).to.be.calledOnce; @@ -111,6 +112,8 @@ describe('Amazon Payments', () => { headers, }); expectAmazonStubs(); + expect(user.canGetGems).to.be.calledOnce; + user.canGetGems.restore(); }); it('should error if gem amount is too low', async () => { @@ -132,20 +135,29 @@ describe('Amazon Payments', () => { }); }); + it('should error if user cannot get gems gems', async () => { + sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); + await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + message: i18n.t('groupPolicyCannotGetGems'), + name: 'NotAuthorized', + }); + user.canGetGems.restore(); + }); + it('should gift gems', async () => { let receivingUser = new User(); - receivingUser.save(); + await receivingUser.save(); let gift = { type: 'gems', + uuid: receivingUser._id, gems: { amount: 16, - uuid: receivingUser._id, }, }; amount = 16 / 4; await amzLib.checkout({gift, user, orderReferenceId, headers}); - gift.member = receivingUser; expect(paymentBuyGemsStub).to.be.calledOnce; expect(paymentBuyGemsStub).to.be.calledWith({ user, diff --git a/test/api/v3/unit/libs/applePayments.test.js b/test/api/v3/unit/libs/applePayments.test.js index 778672f4c8..db199bd565 100644 --- a/test/api/v3/unit/libs/applePayments.test.js +++ b/test/api/v3/unit/libs/applePayments.test.js @@ -57,7 +57,20 @@ describe('Apple Payments', () => { }); }); + it('errors if the user cannot purchase gems', async () => { + sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); + await expect(applePayments.verifyGemPurchase(user, receipt, headers)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('groupPolicyCannotGetGems'), + }); + + user.canGetGems.restore(); + }); + it('purchases gems', async () => { + sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); await applePayments.verifyGemPurchase(user, receipt, headers); expect(iapSetupStub).to.be.calledOnce; @@ -74,6 +87,8 @@ describe('Apple Payments', () => { amount: 5.25, headers, }); + expect(user.canGetGems).to.be.calledOnce; + user.canGetGems.restore(); }); }); diff --git a/test/api/v3/unit/libs/googlePayments.test.js b/test/api/v3/unit/libs/googlePayments.test.js index 35b9ff5366..76ba779fb4 100644 --- a/test/api/v3/unit/libs/googlePayments.test.js +++ b/test/api/v3/unit/libs/googlePayments.test.js @@ -63,7 +63,21 @@ describe('Google Payments', () => { }); }); + it('should throw an error if user cannot purchase gems', async () => { + sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); + + await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('groupPolicyCannotGetGems'), + }); + + user.canGetGems.restore(); + }); + it('purchases gems', async () => { + sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); await googlePayments.verifyGemPurchase(user, receipt, signature, headers); expect(iapSetupStub).to.be.calledOnce; @@ -82,6 +96,8 @@ describe('Google Payments', () => { amount: 5.25, headers, }); + expect(user.canGetGems).to.be.calledOnce; + user.canGetGems.restore(); }); }); diff --git a/test/api/v3/unit/libs/paypalPayments.test.js b/test/api/v3/unit/libs/paypalPayments.test.js index bcad82996a..ae67cf3ede 100644 --- a/test/api/v3/unit/libs/paypalPayments.test.js +++ b/test/api/v3/unit/libs/paypalPayments.test.js @@ -61,7 +61,7 @@ describe('Paypal Payments', () => { }); it('creates a link for gem purchases', async () => { - let link = await paypalPayments.checkout(); + let link = await paypalPayments.checkout({user: new User()}); expect(paypalPaymentCreateStub).to.be.calledOnce; expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00)); @@ -87,13 +87,25 @@ describe('Paypal Payments', () => { }); }); + it('should error if the user cannot get gems', async () => { + let user = new User(); + sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); + + await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + message: i18n.t('groupPolicyCannotGetGems'), + name: 'NotAuthorized', + }); + }); + it('creates a link for gifting gems', async () => { let receivingUser = new User(); + await receivingUser.save(); let gift = { type: 'gems', + uuid: receivingUser._id, gems: { amount: 16, - uuid: receivingUser._id, }, }; diff --git a/test/api/v3/unit/libs/stripePayments.test.js b/test/api/v3/unit/libs/stripePayments.test.js index 98988e37b2..75aaffafcb 100644 --- a/test/api/v3/unit/libs/stripePayments.test.js +++ b/test/api/v3/unit/libs/stripePayments.test.js @@ -75,8 +75,29 @@ describe('Stripe Payments', () => { }); }); + + it('should error if user cannot get gems', async () => { + gift = undefined; + sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); + + await expect(stripePayments.checkout({ + token, + user, + gift, + groupId, + email, + headers, + coupon, + }, stripe)).to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + message: i18n.t('groupPolicyCannotGetGems'), + name: 'NotAuthorized', + }); + }); + it('should purchase gems', async () => { gift = undefined; + sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); await stripePayments.checkout({ token, @@ -102,16 +123,18 @@ describe('Stripe Payments', () => { paymentMethod: 'Stripe', gift, }); + expect(user.canGetGems).to.be.calledOnce; + user.canGetGems.restore(); }); it('should gift gems', async () => { let receivingUser = new User(); - receivingUser.save(); + await receivingUser.save(); gift = { type: 'gems', + uuid: receivingUser._id, gems: { amount: 16, - uuid: receivingUser._id, }, }; @@ -125,7 +148,6 @@ describe('Stripe Payments', () => { coupon, }, stripe); - gift.member = receivingUser; expect(stripeChargeStub).to.be.calledOnce; expect(stripeChargeStub).to.be.calledWith({ amount: '400', diff --git a/test/api/v3/unit/models/user.test.js b/test/api/v3/unit/models/user.test.js index 0f128cd296..98abc11a4c 100644 --- a/test/api/v3/unit/models/user.test.js +++ b/test/api/v3/unit/models/user.test.js @@ -1,6 +1,7 @@ import Bluebird from 'bluebird'; import moment from 'moment'; import { model as User } from '../../../../../website/server/models/user'; +import { model as Group } from '../../../../../website/server/models/group'; import common from '../../../../../website/common'; describe('User Model', () => { @@ -179,6 +180,75 @@ describe('User Model', () => { }); }); + context('canGetGems', () => { + let user; + let group; + beforeEach(() => { + user = new User(); + let leader = new User(); + group = new Group({ + name: 'test', + type: 'guild', + privacy: 'private', + leader: leader._id, + }); + }); + + it('returns true if user is not subscribed', async () => { + expect(await user.canGetGems()).to.equal(true); + }); + + it('returns true if user is not subscribed with a group plan', async () => { + user.purchased.plan.customerId = 123; + expect(await user.canGetGems()).to.equal(true); + }); + + it('returns true if user is subscribed with a group plan', async () => { + user.purchased.plan.customerId = 'group-plan'; + expect(await user.canGetGems()).to.equal(true); + }); + + it('returns true if user is part of a group', async () => { + user.guilds.push(group._id); + expect(await user.canGetGems()).to.equal(true); + }); + + it('returns true if user is part of a group with a subscription', async () => { + user.guilds.push(group._id); + user.purchased.plan.customerId = 'group-plan'; + group.purchased.plan.customerId = 123; + await group.save(); + expect(await user.canGetGems()).to.equal(true); + }); + + it('returns true if leader is part of a group with a subscription and canGetGems: false', async () => { + user.guilds.push(group._id); + user.purchased.plan.customerId = 'group-plan'; + group.purchased.plan.customerId = 123; + group.leader = user._id; + group.leaderOnly.getGems = true; + await group.save(); + expect(await user.canGetGems()).to.equal(true); + }); + + it('returns true if user is part of a group with no subscription but canGetGems: false', async () => { + user.guilds.push(group._id); + user.purchased.plan.customerId = 'group-plan'; + group.leaderOnly.getGems = true; + await group.save(); + expect(await user.canGetGems()).to.equal(true); + }); + + it('returns false if user is part of a group with a subscription and canGetGems: false', async () => { + user.guilds.push(group._id); + user.purchased.plan.customerId = 'group-plan'; + group.purchased.plan.customerId = 123; + group.leaderOnly.getGems = true; + await group.save(); + expect(await user.canGetGems()).to.equal(false); + }); + }); + context('hasNotCancelled', () => { let user; beforeEach(() => { diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index a2d15c698b..4ea8eb2c1e 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -298,5 +298,6 @@ "managerMarker": " - Manager", "joinedGuild": "Joined a Guild", "joinedGuildText": "Ventured into the social side of Habitica by joining a Guild!", - "badAmountOfGemsToPurchase": "Amount must be at least 1." + "badAmountOfGemsToPurchase": "Amount must be at least 1.", + "groupPolicyCannotGetGems": "The policy of one group you're part of prevents its members from obtaining gems." } diff --git a/website/common/script/ops/purchase.js b/website/common/script/ops/purchase.js index 96324281b5..a3efc076d1 100644 --- a/website/common/script/ops/purchase.js +++ b/website/common/script/ops/purchase.js @@ -30,6 +30,11 @@ module.exports = function purchase (user, req = {}, analytics) { 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)); } diff --git a/website/server/controllers/api-v3/user.js b/website/server/controllers/api-v3/user.js index bbe8ad5d24..bbc892ce52 100644 --- a/website/server/controllers/api-v3/user.js +++ b/website/server/controllers/api-v3/user.js @@ -19,6 +19,7 @@ import { sendTxn as txnEmail, } from '../../libs/email'; import nconf from 'nconf'; +import get from 'lodash/get'; const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL'); @@ -1227,7 +1228,18 @@ api.purchase = { url: '/user/purchase/:type/:key', async handler (req, res) { let user = res.locals.user; - let purchaseRes = req.params.type === 'spells' ? common.ops.buySpecialSpell(user, req) : common.ops.purchase(user, req, res.analytics); + const type = get(req.params, 'type'); + const key = get(req.params, 'key'); + + // Some groups limit their members ability to obtain gems + // The check is async so it's done on the server only and not on the client, + // resulting in a purchase that will seem successful until the request hit the server. + if (type === 'gems' && key === 'gem') { + const canGetGems = await user.canGetGems(); + if (!canGetGems) throw new NotAuthorized(res.t('groupPolicyCannotGetGems')); + } + + let purchaseRes = type === 'spells' ? common.ops.buySpecialSpell(user, req) : common.ops.purchase(user, req, res.analytics); await user.save(); res.respond(200, ...purchaseRes); }, diff --git a/website/server/controllers/top-level/payments/paypal.js b/website/server/controllers/top-level/payments/paypal.js index 149f90d3b3..32d103ad02 100644 --- a/website/server/controllers/top-level/payments/paypal.js +++ b/website/server/controllers/top-level/payments/paypal.js @@ -27,7 +27,7 @@ api.checkout = { let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined; req.session.gift = req.query.gift; - let link = await paypalPayments.checkout({gift}); + let link = await paypalPayments.checkout({gift, user: res.locals.user}); if (req.query.noRedirect) { res.respond(200); diff --git a/website/server/libs/amazonPayments.js b/website/server/libs/amazonPayments.js index d723cd976b..49b4045b75 100644 --- a/website/server/libs/amazonPayments.js +++ b/website/server/libs/amazonPayments.js @@ -97,6 +97,8 @@ api.checkout = async function checkout (options = {}) { let amount = 5; if (gift) { + gift.member = await User.findById(gift.uuid).exec(); + if (gift.type === this.constants.GIFT_TYPE_GEMS) { if (gift.gems.amount <= 0) { throw new BadRequest(i18n.t('badAmountOfGemsToPurchase')); @@ -107,6 +109,12 @@ api.checkout = async function checkout (options = {}) { } } + if (!gift || gift.type === this.constants.GIFT_TYPE_GEMS) { + const receiver = gift ? gift.member : user; + const receiverCanGetGems = await receiver.canGetGems(); + if (!receiverCanGetGems) throw new NotAuthorized(i18n.t('groupPolicyCannotGetGems', receiver.preferences.language)); + } + await this.setOrderReferenceDetails({ AmazonOrderReferenceId: orderReferenceId, OrderReferenceAttributes: { diff --git a/website/server/libs/applePayments.js b/website/server/libs/applePayments.js index 881c7ef152..e77f452a78 100644 --- a/website/server/libs/applePayments.js +++ b/website/server/libs/applePayments.js @@ -21,6 +21,9 @@ api.constants = { }; api.verifyGemPurchase = async function verifyGemPurchase (user, receipt, headers) { + const userCanGetGems = await user.canGetGems(); + if (!userCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', user.preferences.language)); + await iap.setup(); let appleRes = await iap.validate(iap.APPLE, receipt); let isValidated = iap.isValidated(appleRes); diff --git a/website/server/libs/googlePayments.js b/website/server/libs/googlePayments.js index 5b38e9ae27..6214da97a8 100644 --- a/website/server/libs/googlePayments.js +++ b/website/server/libs/googlePayments.js @@ -20,6 +20,9 @@ api.constants = { }; api.verifyGemPurchase = async function verifyGemPurchase (user, receipt, signature, headers) { + const userCanGetGems = await user.canGetGems(); + if (!userCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', user.preferences.language)); + await iap.setup(); let testObj = { diff --git a/website/server/libs/paypalPayments.js b/website/server/libs/paypalPayments.js index 6c0ccadb66..19bd2f2676 100644 --- a/website/server/libs/paypalPayments.js +++ b/website/server/libs/paypalPayments.js @@ -70,11 +70,15 @@ api.paypalBillingAgreementCancel = Bluebird.promisify(paypal.billingAgreement.ca api.ipnVerifyAsync = Bluebird.promisify(ipn.verify, {context: ipn}); api.checkout = async function checkout (options = {}) { - let {gift} = options; + let {gift, user} = options; let amount = 5.00; let description = 'Habitica Gems'; + if (gift) { + const member = await User.findById(gift.uuid).exec(); + gift.member = member; + if (gift.type === 'gems') { if (gift.gems.amount <= 0) { throw new BadRequest(i18n.t('badAmountOfGemsToPurchase')); @@ -87,6 +91,14 @@ api.checkout = async function checkout (options = {}) { } } + + if (!gift || gift.type === 'gems') { + const receiver = gift ? gift.member : user; + const receiverCanGetGems = await receiver.canGetGems(); + if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language)); + } + + let createPayment = { intent: 'sale', payer: { payment_method: this.constants.PAYMENT_METHOD }, diff --git a/website/server/libs/stripePayments.js b/website/server/libs/stripePayments.js index f6cfae820b..63b2041ca3 100644 --- a/website/server/libs/stripePayments.js +++ b/website/server/libs/stripePayments.js @@ -76,6 +76,11 @@ api.checkout = async function checkout (options, stripeInc) { if (!token) throw new BadRequest('Missing req.body.id'); + if (gift) { + const member = await User.findById(gift.uuid).exec(); + gift.member = member; + } + if (sub) { if (sub.discount) { if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired')); @@ -114,6 +119,12 @@ api.checkout = async function checkout (options, stripeInc) { } } + if (!gift || gift.type === 'gems') { + const receiver = gift ? gift.member : user; + const receiverCanGetGems = await receiver.canGetGems(); + if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language)); + } + response = await stripeApi.charges.create({ amount, currency: 'usd', @@ -141,8 +152,6 @@ api.checkout = async function checkout (options, stripeInc) { }; if (gift) { - let member = await User.findById(gift.uuid).exec(); - gift.member = member; if (gift.type === 'subscription') method = 'createSubscription'; data.paymentMethod = 'Gift'; } diff --git a/website/server/models/challenge.js b/website/server/models/challenge.js index bcb28e69a9..270f5ad5b4 100644 --- a/website/server/models/challenge.js +++ b/website/server/models/challenge.js @@ -283,7 +283,15 @@ schema.methods.closeChal = async function closeChal (broken = {}) { // Award prize to winner and notify if (winner) { winner.achievements.challenges.push(challenge.name); - winner.balance += challenge.prize / 4; + + // If the winner cannot get gems (because of a group policy) + // reimburse the leader + const winnerCanGetGems = await winner.canGetGems(); + if (!winnerCanGetGems) { + await User.update({_id: challenge.leader}, {$inc: {balance: challenge.prize / 4}}).exec(); + } else { + winner.balance += challenge.prize / 4; + } winner.addNotification('WON_CHALLENGE'); diff --git a/website/server/models/group.js b/website/server/models/group.js index 569a65830e..e60afdbbfb 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -76,6 +76,8 @@ export let schema = new Schema({ leaderOnly: { // restrict group actions to leader (members can't do them) challenges: {type: Boolean, default: false, required: true}, // invites: {type: Boolean, default: false, required: true}, + // Some group plans prevent members from getting gems + getGems: {type: Boolean, default: false}, }, memberCount: {type: Number, default: 1}, challengeCount: {type: Number, default: 0}, diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index b9336134e8..6aa32a3a54 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -4,6 +4,7 @@ import Bluebird from 'bluebird'; import { chatDefaults, TAVERN_ID, + model as Group, } from '../group'; import { defaults, map, flatten, flow, compact, uniq, partialRight } from 'lodash'; import { model as UserNotification } from '../userNotification'; @@ -190,7 +191,7 @@ schema.methods.cancelSubscription = async function cancelSubscription (options = return await payments.cancelSubscription(options); }; -schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) { +schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) { // If the user's timezone has changed (due to travel or daylight savings), // cron can be triggered twice in one day, so we check for that and use // both timezones to work out if cron should run. @@ -271,3 +272,28 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) { return {daysMissed, timezoneOffsetFromUserPrefs}; }; + +// Determine if the user can get gems: some groups restrict their members ability to obtain them. +// User is allowed to buy gems if no group has `leaderOnly.getGems` === true or if +// its the group leader +schema.methods.canGetGems = async function canObtainGems () { + const user = this; + const plan = user.purchased.plan; + + if (!user.isSubscribed() || plan.customerId !== payments.constants.GROUP_PLAN_CUSTOMER_ID) { + return true; + } + + const userGroups = user.getGroups(); + + const groups = await Group + .find({ + _id: {$in: userGroups}, + }) + .select('leaderOnly leader purchased') + .exec(); + + return groups.every(g => { + return !g.isSubscribed() || g.leader === user._id || g.leaderOnly.getGems !== true; + }); +};