mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 15:48:04 +01:00
Merge branch 'develop' into phillip/chat-skill-merge
This commit is contained in:
@@ -37,7 +37,15 @@ export default function baseModel (schema, options = {}) {
|
||||
});
|
||||
|
||||
schema.pre('update', function preUpdateModel () {
|
||||
this.update({}, { $set: { updatedAt: new Date() } });
|
||||
this.set({}, { $set: { updatedAt: new Date() } });
|
||||
});
|
||||
|
||||
schema.pre('updateOne', function preUpdateModel () {
|
||||
this.set({}, { $set: { updatedAt: new Date() } });
|
||||
});
|
||||
|
||||
schema.pre('updateMany', function preUpdateModel () {
|
||||
this.set({}, { $set: { updatedAt: new Date() } });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -63,9 +63,6 @@ const CLEAR_BUFFS = {
|
||||
};
|
||||
|
||||
async function grantEndOfTheMonthPerks (user, now) {
|
||||
// multi-month subscriptions are for multiples of 3 months
|
||||
const SUBSCRIPTION_BASIC_BLOCK_LENGTH = 3;
|
||||
|
||||
const { plan, elapsedMonths } = getPlanContext(user, now);
|
||||
|
||||
if (elapsedMonths > 0) {
|
||||
@@ -106,32 +103,17 @@ async function grantEndOfTheMonthPerks (user, now) {
|
||||
planMonthsLength = getPlanMonths(plan);
|
||||
}
|
||||
|
||||
// every 3 months you get one set of perks - this variable records how many sets you need
|
||||
let perkAmountNeeded = 0;
|
||||
if (planMonthsLength === 1) {
|
||||
// User has a single-month recurring subscription and are due for perks
|
||||
// IF they've been subscribed for a multiple of 3 months.
|
||||
if (plan.consecutive.count % SUBSCRIPTION_BASIC_BLOCK_LENGTH === 0) { // every 3 months
|
||||
perkAmountNeeded = 1;
|
||||
}
|
||||
plan.consecutive.offset = 0; // allow the same logic to be run next month
|
||||
} else {
|
||||
// User has a multi-month recurring subscription
|
||||
// and it renewed in the previous calendar month.
|
||||
|
||||
// e.g., for a 6-month subscription, give two sets of perks
|
||||
perkAmountNeeded = planMonthsLength / SUBSCRIPTION_BASIC_BLOCK_LENGTH;
|
||||
// don't need to check for perks again for this many months
|
||||
// (subtract 1 because we should have run this when the payment was taken last month)
|
||||
plan.consecutive.offset = planMonthsLength - 1;
|
||||
}
|
||||
if (perkAmountNeeded > 0) {
|
||||
// one Hourglass every 3 months
|
||||
await plan.updateHourglasses(user._id, perkAmountNeeded, 'subscription_perks'); // eslint-disable-line no-await-in-loop
|
||||
plan.consecutive.gemCapExtra += 5 * perkAmountNeeded; // 5 extra Gems every 3 months
|
||||
// cap it at 50 (hard 25 limit + extra 25)
|
||||
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25;
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await plan.incrementPerkCounterAndReward(user._id, planMonthsLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,6 +279,8 @@ export async function cron (options = {}) {
|
||||
|
||||
if (user.isSubscribed()) {
|
||||
await grantEndOfTheMonthPerks(user, now);
|
||||
} if (!user.isSubscribed() && user.purchased.plan.perkMonthCount > 0) {
|
||||
user.purchased.plan.perkMonthCount = 0;
|
||||
}
|
||||
|
||||
const { plan } = user.purchased;
|
||||
|
||||
@@ -21,6 +21,8 @@ export default {
|
||||
setup: util.promisify(iap.setup.bind(iap)),
|
||||
validate: util.promisify(iap.validate.bind(iap)),
|
||||
isValidated: iap.isValidated,
|
||||
isCanceled: iap.isCanceled,
|
||||
isExpired: iap.isExpired,
|
||||
getPurchaseData: iap.getPurchaseData,
|
||||
GOOGLE: iap.GOOGLE,
|
||||
APPLE: iap.APPLE,
|
||||
|
||||
@@ -87,10 +87,9 @@ async function inviteUserToParty (userToInvite, group, inviter, res) {
|
||||
}
|
||||
|
||||
if (userToInvite.party._id) {
|
||||
const userParty = await Group.getGroup({ user: userToInvite, groupId: 'party', fields: 'memberCount' });
|
||||
const userParty = await Group.getGroup({ user: userToInvite, groupId: 'party', fields: '_id' });
|
||||
|
||||
// Allow user to be invited to a new party when they're partying solo
|
||||
if (userParty && userParty.memberCount !== 1) throw new NotAuthorized(res.t('userAlreadyInAParty', { userId: uuid, username: userToInvite.profile.name }));
|
||||
if (userParty) throw new NotAuthorized(res.t('userAlreadyInAParty', { userId: uuid, username: userToInvite.profile.name }));
|
||||
}
|
||||
|
||||
const partyInvite = { id: group._id, name: group.name, inviter: inviter._id };
|
||||
@@ -142,6 +141,22 @@ async function inviteByUUID (uuid, group, inviter, req, res) {
|
||||
));
|
||||
}
|
||||
|
||||
const analyticsObject = {
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
uuid: inviter._id,
|
||||
invitee: uuid,
|
||||
groupId: group._id,
|
||||
groupType: group.type,
|
||||
headers: req.headers,
|
||||
};
|
||||
|
||||
if (group.type === 'party') {
|
||||
analyticsObject.seekingParty = Boolean(userToInvite.party.seeking);
|
||||
}
|
||||
|
||||
res.analytics.track('group invite', analyticsObject);
|
||||
|
||||
return addInvitationToUser(userToInvite, group, inviter, res);
|
||||
}
|
||||
|
||||
@@ -189,6 +204,18 @@ async function inviteByEmail (invite, group, inviter, req, res) {
|
||||
const userIsUnsubscribed = await EmailUnsubscription.findOne({ email: invite.email }).exec();
|
||||
const groupLabel = group.type === 'guild' ? '-guild' : '';
|
||||
if (!userIsUnsubscribed) sendTxnEmail(invite, `invite-friend${groupLabel}`, variables);
|
||||
|
||||
const analyticsObject = {
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
uuid: inviter._id,
|
||||
invitee: 'email',
|
||||
groupId: group._id,
|
||||
groupType: group.type,
|
||||
headers: req.headers,
|
||||
};
|
||||
|
||||
res.analytics.track('group invite', analyticsObject);
|
||||
}
|
||||
|
||||
return userReturnInfo;
|
||||
@@ -214,6 +241,23 @@ async function inviteByUserName (username, group, inviter, req, res) {
|
||||
{ userId: userToInvite._id, username: userToInvite.profile.name },
|
||||
));
|
||||
}
|
||||
|
||||
const analyticsObject = {
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
uuid: inviter._id,
|
||||
invitee: userToInvite._id,
|
||||
groupId: group._id,
|
||||
groupType: group.type,
|
||||
headers: req.headers,
|
||||
};
|
||||
|
||||
if (group.type === 'party') {
|
||||
analyticsObject.seekingParty = Boolean(userToInvite.party.seeking);
|
||||
}
|
||||
|
||||
res.analytics.track('group invite', analyticsObject);
|
||||
|
||||
return addInvitationToUser(userToInvite, group, inviter, res);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ api.constants = {
|
||||
GIFT_TYPE_SUBSCRIPTION: 'subscription',
|
||||
|
||||
METHOD_BUY_GEMS: 'buyGems',
|
||||
METHOD_BUY_SKU_ITEM: 'buySkuItem',
|
||||
METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
|
||||
PAYMENT_METHOD: 'Amazon Payments',
|
||||
PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
|
||||
@@ -110,7 +111,7 @@ api.authorize = function authorize (inputSet) {
|
||||
*/
|
||||
api.checkout = async function checkout (options = {}) {
|
||||
const {
|
||||
gift, user, orderReferenceId, headers, gemsBlock: gemsBlockKey,
|
||||
gift, user, orderReferenceId, headers, gemsBlock: gemsBlockKey, sku,
|
||||
} = options;
|
||||
let amount;
|
||||
let gemsBlock;
|
||||
@@ -127,6 +128,12 @@ api.checkout = async function checkout (options = {}) {
|
||||
} else if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) {
|
||||
amount = common.content.subscriptionBlocks[gift.subscription.key].price;
|
||||
}
|
||||
} else if (sku) {
|
||||
if (sku === 'Pet-Gryphatrice-Jubilant') {
|
||||
amount = 9.99;
|
||||
} else {
|
||||
throw new NotFound('SKU not found.');
|
||||
}
|
||||
} else {
|
||||
gemsBlock = getGemsBlock(gemsBlockKey);
|
||||
amount = gemsBlock.price / 100;
|
||||
@@ -171,12 +178,16 @@ api.checkout = async function checkout (options = {}) {
|
||||
|
||||
// execute payment
|
||||
let method = this.constants.METHOD_BUY_GEMS;
|
||||
if (sku) {
|
||||
method = this.constants.METHOD_BUY_SKU_ITEM;
|
||||
}
|
||||
|
||||
const data = {
|
||||
user,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
gemsBlock,
|
||||
sku,
|
||||
};
|
||||
|
||||
if (gift) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import moment from 'moment';
|
||||
import shared from '../../../common';
|
||||
import iap from '../inAppPurchases';
|
||||
import payments from './payments';
|
||||
import { getGemsBlock, validateGiftMessage } from './gems';
|
||||
import { validateGiftMessage } from './gems';
|
||||
import {
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
@@ -22,7 +22,7 @@ api.constants = {
|
||||
RESPONSE_NO_ITEM_PURCHASED: 'NO_ITEM_PURCHASED',
|
||||
};
|
||||
|
||||
api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
api.verifyPurchase = async function verifyPurchase (options) {
|
||||
const {
|
||||
gift, user, receipt, headers,
|
||||
} = options;
|
||||
@@ -44,7 +44,6 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
if (purchaseDataList.length === 0) {
|
||||
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
||||
}
|
||||
let correctReceipt = false;
|
||||
|
||||
// Purchasing one item at a time (processing of await(s) below is sequential not parallel)
|
||||
for (const purchaseData of purchaseDataList) {
|
||||
@@ -62,58 +61,45 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
userId: user._id,
|
||||
});
|
||||
|
||||
let gemsBlockKey;
|
||||
switch (purchaseData.productId) { // eslint-disable-line default-case
|
||||
case 'com.habitrpg.ios.Habitica.4gems':
|
||||
gemsBlockKey = '4gems';
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.20gems':
|
||||
case 'com.habitrpg.ios.Habitica.21gems':
|
||||
gemsBlockKey = '21gems';
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.42gems':
|
||||
gemsBlockKey = '42gems';
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.84gems':
|
||||
gemsBlockKey = '84gems';
|
||||
break;
|
||||
}
|
||||
if (!gemsBlockKey) throw new NotAuthorized(api.constants.RESPONSE_INVALID_ITEM);
|
||||
const gemsBlock = getGemsBlock(gemsBlockKey);
|
||||
|
||||
if (gift) {
|
||||
gift.type = 'gems';
|
||||
if (!gift.gems) gift.gems = {};
|
||||
gift.gems.amount = shared.content.gems[gemsBlock.key].gems;
|
||||
}
|
||||
|
||||
if (gemsBlock) {
|
||||
correctReceipt = true;
|
||||
await payments.buyGems({ // eslint-disable-line no-await-in-loop
|
||||
user,
|
||||
gift,
|
||||
paymentMethod: api.constants.PAYMENT_METHOD_APPLE,
|
||||
gemsBlock,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
await payments.buySkuItem({ // eslint-disable-line no-await-in-loop
|
||||
user,
|
||||
gift,
|
||||
paymentMethod: api.constants.PAYMENT_METHOD_APPLE,
|
||||
sku: purchaseData.productId,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!correctReceipt) throw new NotAuthorized(api.constants.RESPONSE_INVALID_ITEM);
|
||||
|
||||
return appleRes;
|
||||
};
|
||||
|
||||
api.subscribe = async function subscribe (sku, user, receipt, headers, nextPaymentProcessing) {
|
||||
if (user && user.isSubscribed()) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
api.subscribe = async function subscribe (user, receipt, headers, nextPaymentProcessing) {
|
||||
await iap.setup();
|
||||
|
||||
const appleRes = await iap.validate(iap.APPLE, receipt);
|
||||
const isValidated = iap.isValidated(appleRes);
|
||||
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
||||
|
||||
const purchaseDataList = iap.getPurchaseData(appleRes);
|
||||
if (purchaseDataList.length === 0) {
|
||||
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
||||
}
|
||||
|
||||
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
|
||||
let purchase;
|
||||
let newestDate;
|
||||
|
||||
for (const purchaseData of purchaseDataList) {
|
||||
const datePurchased = new Date(Number(purchaseData.purchaseDate));
|
||||
const dateTerminated = new Date(Number(purchaseData.expirationDate));
|
||||
if ((!newestDate || datePurchased > newestDate) && dateTerminated > new Date()) {
|
||||
purchase = purchaseData;
|
||||
newestDate = datePurchased;
|
||||
}
|
||||
}
|
||||
|
||||
let subCode;
|
||||
switch (sku) { // eslint-disable-line default-case
|
||||
switch (purchase.productId) { // eslint-disable-line default-case
|
||||
case 'subscription1month':
|
||||
subCode = 'basic_earned';
|
||||
break;
|
||||
@@ -128,45 +114,56 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme
|
||||
break;
|
||||
}
|
||||
const sub = subCode ? shared.content.subscriptionBlocks[subCode] : false;
|
||||
if (!sub) throw new NotAuthorized(this.constants.RESPONSE_INVALID_ITEM);
|
||||
await iap.setup();
|
||||
|
||||
const appleRes = await iap.validate(iap.APPLE, receipt);
|
||||
const isValidated = iap.isValidated(appleRes);
|
||||
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
||||
|
||||
const purchaseDataList = iap.getPurchaseData(appleRes);
|
||||
if (purchaseDataList.length === 0) {
|
||||
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
||||
}
|
||||
|
||||
let transactionId;
|
||||
|
||||
for (const purchaseData of purchaseDataList) {
|
||||
const dateTerminated = new Date(Number(purchaseData.expirationDate));
|
||||
if (purchaseData.productId === sku && dateTerminated > new Date()) {
|
||||
transactionId = purchaseData.transactionId;
|
||||
break;
|
||||
if (purchase.originalTransactionId) {
|
||||
let existingSub;
|
||||
if (user && user.isSubscribed()) {
|
||||
if (user.purchased.plan.customerId !== purchase.originalTransactionId) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
}
|
||||
existingSub = shared.content.subscriptionBlocks[user.purchased.plan.planId];
|
||||
if (existingSub === sub) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (transactionId) {
|
||||
const existingUser = await User.findOne({
|
||||
'purchased.plan.customerId': transactionId,
|
||||
const existingUsers = await User.find({
|
||||
$or: [
|
||||
{ 'purchased.plan.customerId': purchase.originalTransactionId },
|
||||
{ 'purchased.plan.customerId': purchase.transactionId },
|
||||
],
|
||||
}).exec();
|
||||
if (existingUser) throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
if (existingUsers.length > 0) {
|
||||
if (purchase.originalTransactionId === purchase.transactionId) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
}
|
||||
for (const existingUser of existingUsers) {
|
||||
if (existingUser._id !== user._id && !existingUser.purchased.plan.dateTerminated) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextPaymentProcessing = nextPaymentProcessing || moment.utc().add({ days: 2 }); // eslint-disable-line max-len, no-param-reassign
|
||||
const terminationDate = moment(Number(purchase.expirationDate));
|
||||
if (nextPaymentProcessing > terminationDate) {
|
||||
// For test subscriptions that have a significantly shorter expiration period, this is better
|
||||
nextPaymentProcessing = terminationDate; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
await payments.createSubscription({
|
||||
const data = {
|
||||
user,
|
||||
customerId: transactionId,
|
||||
customerId: purchase.originalTransactionId,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
||||
sub,
|
||||
headers,
|
||||
nextPaymentProcessing,
|
||||
additionalData: receipt,
|
||||
});
|
||||
};
|
||||
if (existingSub) {
|
||||
data.updatedFrom = existingSub;
|
||||
data.updatedFrom.logic = 'refundAndRepay';
|
||||
}
|
||||
await payments.createSubscription(data);
|
||||
} else {
|
||||
throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
||||
}
|
||||
@@ -258,8 +255,6 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
||||
|
||||
await iap.setup();
|
||||
|
||||
let dateTerminated;
|
||||
|
||||
try {
|
||||
const appleRes = await iap.validate(iap.APPLE, plan.additionalData);
|
||||
|
||||
@@ -268,10 +263,27 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
||||
|
||||
const purchases = iap.getPurchaseData(appleRes);
|
||||
if (purchases.length === 0) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
||||
const subscriptionData = purchases[0];
|
||||
let newestDate;
|
||||
let newestPurchase;
|
||||
|
||||
dateTerminated = new Date(Number(subscriptionData.expirationDate));
|
||||
if (dateTerminated > new Date()) throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||
for (const purchaseData of purchases) {
|
||||
const datePurchased = new Date(Number(purchaseData.purchaseDate));
|
||||
if (!newestDate || datePurchased > newestDate) {
|
||||
newestDate = datePurchased;
|
||||
newestPurchase = purchaseData;
|
||||
}
|
||||
}
|
||||
|
||||
if (!iap.isCanceled(newestPurchase) && !iap.isExpired(newestPurchase)) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||
}
|
||||
|
||||
await payments.cancelSubscription({
|
||||
user,
|
||||
nextBill: new Date(Number(newestPurchase.expirationDate)),
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
||||
headers,
|
||||
});
|
||||
} catch (err) {
|
||||
// If we have an invalid receipt, cancel anyway
|
||||
if (
|
||||
@@ -281,13 +293,6 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await payments.cancelSubscription({
|
||||
user,
|
||||
nextBill: dateTerminated,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '../errors';
|
||||
import { model as IapPurchaseReceipt } from '../../models/iapPurchaseReceipt';
|
||||
import { model as User } from '../../models/user';
|
||||
import { getGemsBlock, validateGiftMessage } from './gems';
|
||||
import { validateGiftMessage } from './gems';
|
||||
|
||||
const api = {};
|
||||
|
||||
@@ -21,7 +21,7 @@ api.constants = {
|
||||
RESPONSE_STILL_VALID: 'SUBSCRIPTION_STILL_VALID',
|
||||
};
|
||||
|
||||
api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
api.verifyPurchase = async function verifyPurchase (options) {
|
||||
const {
|
||||
gift, user, receipt, signature, headers,
|
||||
} = options;
|
||||
@@ -61,39 +61,11 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
userId: user._id,
|
||||
});
|
||||
|
||||
let gemsBlockKey;
|
||||
|
||||
switch (receiptObj.productId) { // eslint-disable-line default-case
|
||||
case 'com.habitrpg.android.habitica.iap.4gems':
|
||||
gemsBlockKey = '4gems';
|
||||
break;
|
||||
case 'com.habitrpg.android.habitica.iap.20gems':
|
||||
case 'com.habitrpg.android.habitica.iap.21gems':
|
||||
gemsBlockKey = '21gems';
|
||||
break;
|
||||
case 'com.habitrpg.android.habitica.iap.42gems':
|
||||
gemsBlockKey = '42gems';
|
||||
break;
|
||||
case 'com.habitrpg.android.habitica.iap.84gems':
|
||||
gemsBlockKey = '84gems';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!gemsBlockKey) throw new NotAuthorized(this.constants.RESPONSE_INVALID_ITEM);
|
||||
|
||||
const gemsBlock = getGemsBlock(gemsBlockKey);
|
||||
|
||||
if (gift) {
|
||||
gift.type = 'gems';
|
||||
if (!gift.gems) gift.gems = {};
|
||||
gift.gems.amount = shared.content.gems[gemsBlock.key].gems;
|
||||
}
|
||||
|
||||
await payments.buyGems({
|
||||
await payments.buySkuItem({ // eslint-disable-line no-await-in-loop
|
||||
user,
|
||||
gift,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_GOOGLE,
|
||||
gemsBlock,
|
||||
paymentMethod: api.constants.PAYMENT_METHOD_GOOGLE,
|
||||
sku: googleRes.productId,
|
||||
headers,
|
||||
});
|
||||
|
||||
|
||||
@@ -180,6 +180,7 @@ async function addSubToGroupUser (member, group) {
|
||||
}
|
||||
|
||||
// save unused hourglass and mystery items
|
||||
plan.perkMonthCount = memberPlan.perkMonthCount;
|
||||
plan.consecutive.trinkets = memberPlan.consecutive.trinkets;
|
||||
plan.mysteryItems = memberPlan.mysteryItems;
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ import { // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
buyGems,
|
||||
} from './gems';
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
buySkuItem,
|
||||
} from './skuItem';
|
||||
import { paymentConstants } from './constants';
|
||||
|
||||
const api = {};
|
||||
@@ -31,4 +34,6 @@ api.cancelSubscription = cancelSubscription;
|
||||
|
||||
api.buyGems = buyGems;
|
||||
|
||||
api.buySkuItem = buySkuItem;
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -77,7 +77,9 @@ api.paypalBillingAgreementCancel = util
|
||||
api.ipnVerifyAsync = util.promisify(paypalIpn.verify.bind(paypalIpn));
|
||||
|
||||
api.checkout = async function checkout (options = {}) {
|
||||
const { gift, user, gemsBlock: gemsBlockKey } = options;
|
||||
const {
|
||||
gift, gemsBlock: gemsBlockKey, sku, user,
|
||||
} = options;
|
||||
|
||||
let amount;
|
||||
let gemsBlock;
|
||||
@@ -99,12 +101,17 @@ api.checkout = async function checkout (options = {}) {
|
||||
amount = Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2);
|
||||
description = 'mo. Habitica Subscription (Gift)';
|
||||
}
|
||||
} else if (sku) {
|
||||
if (sku === 'Pet-Gryphatrice-Jubilant') {
|
||||
description = 'Jubilant Gryphatrice';
|
||||
amount = 9.99;
|
||||
}
|
||||
} else {
|
||||
gemsBlock = getGemsBlock(gemsBlockKey);
|
||||
amount = gemsBlock.price / 100;
|
||||
}
|
||||
|
||||
if (!gift || gift.type === 'gems') {
|
||||
if (gemsBlock || (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));
|
||||
@@ -146,10 +153,10 @@ api.checkout = async function checkout (options = {}) {
|
||||
|
||||
api.checkoutSuccess = async function checkoutSuccess (options = {}) {
|
||||
const {
|
||||
user, gift, gemsBlock: gemsBlockKey, paymentId, customerId,
|
||||
user, gift, gemsBlock: gemsBlockKey, paymentId, customerId, sku,
|
||||
} = options;
|
||||
|
||||
let method = 'buyGems';
|
||||
let method = sku ? 'buySkuItem' : 'buyGems';
|
||||
const data = {
|
||||
user,
|
||||
customerId,
|
||||
@@ -164,6 +171,8 @@ api.checkoutSuccess = async function checkoutSuccess (options = {}) {
|
||||
|
||||
data.paymentMethod = 'PayPal (Gift)';
|
||||
data.gift = gift;
|
||||
} else if (sku) {
|
||||
data.sku = sku;
|
||||
} else {
|
||||
data.gemsBlock = getGemsBlock(gemsBlockKey);
|
||||
}
|
||||
|
||||
110
website/server/libs/payments/skuItem.js
Normal file
110
website/server/libs/payments/skuItem.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
BadRequest,
|
||||
} from '../errors';
|
||||
import shared from '../../../common';
|
||||
import { getAnalyticsServiceByEnvironment } from '../analyticsService';
|
||||
import { getGemsBlock, buyGems } from './gems'; // eslint-disable-line import/no-cycle
|
||||
|
||||
const analytics = getAnalyticsServiceByEnvironment();
|
||||
|
||||
const RESPONSE_INVALID_ITEM = 'INVALID_ITEM_PURCHASED';
|
||||
|
||||
const EVENTS = {
|
||||
birthday10: {
|
||||
start: '2023-01-30T08:00-05:00',
|
||||
end: '2023-02-08T23:59-05:00',
|
||||
},
|
||||
};
|
||||
|
||||
function canBuyGryphatrice (user) {
|
||||
if (!moment().isBetween(EVENTS.birthday10.start, EVENTS.birthday10.end)) return false;
|
||||
if (user.items.pets['Gryphatrice-Jubilant']) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function buyGryphatrice (data) {
|
||||
// Double check it's available
|
||||
if (!canBuyGryphatrice(data.user)) throw new BadRequest();
|
||||
const key = 'Gryphatrice-Jubilant';
|
||||
data.user.items.pets[key] = 5;
|
||||
data.user.purchased.txnCount += 1;
|
||||
|
||||
analytics.trackPurchase({
|
||||
uuid: data.user._id,
|
||||
itemPurchased: 'Gryphatrice',
|
||||
sku: `${data.paymentMethod.toLowerCase()}-checkout`,
|
||||
purchaseType: 'checkout',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: Boolean(data.gift),
|
||||
purchaseValue: 10,
|
||||
headers: data.headers,
|
||||
firstPurchase: data.user.purchased.txnCount === 1,
|
||||
});
|
||||
if (data.user.markModified) data.user.markModified('items.pets');
|
||||
await data.user.save();
|
||||
}
|
||||
|
||||
export function canBuySkuItem (sku, user) {
|
||||
switch (sku) {
|
||||
case 'com.habitrpg.android.habitica.iap.pets.gryphatrice_jubilant':
|
||||
case 'com.habitrpg.ios.Habitica.pets.Gryphatrice_Jubilant':
|
||||
case 'Pet-Gryphatrice-Jubilant':
|
||||
case 'price_0MPZ6iZCD0RifGXlLah2furv':
|
||||
return canBuyGryphatrice(user);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function buySkuItem (data) {
|
||||
let gemsBlockKey;
|
||||
|
||||
switch (data.sku) { // eslint-disable-line default-case
|
||||
case 'com.habitrpg.android.habitica.iap.4gems':
|
||||
case 'com.habitrpg.ios.Habitica.4gems':
|
||||
gemsBlockKey = '4gems';
|
||||
break;
|
||||
case 'com.habitrpg.android.habitica.iap.20gems':
|
||||
case 'com.habitrpg.android.habitica.iap.21gems':
|
||||
case 'com.habitrpg.ios.Habitica.20gems':
|
||||
case 'com.habitrpg.ios.Habitica.21gems':
|
||||
gemsBlockKey = '21gems';
|
||||
break;
|
||||
case 'com.habitrpg.android.habitica.iap.42gems':
|
||||
case 'com.habitrpg.ios.Habitica.42gems':
|
||||
gemsBlockKey = '42gems';
|
||||
break;
|
||||
case 'com.habitrpg.android.habitica.iap.84gems':
|
||||
case 'com.habitrpg.ios.Habitica.84gems':
|
||||
gemsBlockKey = '84gems';
|
||||
break;
|
||||
case 'com.habitrpg.android.habitica.iap.pets.gryphatrice_jubilant':
|
||||
case 'com.habitrpg.ios.Habitica.pets.Gryphatrice_Jubilant':
|
||||
case 'Pet-Gryphatrice-Jubilant':
|
||||
case 'price_0MPZ6iZCD0RifGXlLah2furv':
|
||||
buyGryphatrice(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (gemsBlockKey) {
|
||||
const gemsBlock = getGemsBlock(gemsBlockKey);
|
||||
|
||||
if (data.gift) {
|
||||
data.gift.type = 'gems';
|
||||
if (!data.gift.gems) data.gift.gems = {};
|
||||
data.gift.gems.amount = shared.content.gems[gemsBlock.key].gems;
|
||||
}
|
||||
|
||||
await buyGems({
|
||||
user: data.user,
|
||||
gift: data.gift,
|
||||
paymentMethod: data.paymentMethod,
|
||||
gemsBlock,
|
||||
headers: data.headers,
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw new BadRequest(RESPONSE_INVALID_ITEM);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export async function createCheckoutSession (options, stripeInc) {
|
||||
sub,
|
||||
groupId,
|
||||
coupon,
|
||||
sku,
|
||||
} = options;
|
||||
|
||||
// @TODO: We need to mock this, but curently we don't have correct
|
||||
@@ -37,6 +38,8 @@ export async function createCheckoutSession (options, stripeInc) {
|
||||
validateGiftMessage(gift, user);
|
||||
} else if (sub) {
|
||||
type = 'subscription';
|
||||
} else if (sku) {
|
||||
type = 'sku';
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
@@ -71,6 +74,12 @@ export async function createCheckoutSession (options, stripeInc) {
|
||||
price: sub.key,
|
||||
quantity,
|
||||
}];
|
||||
} else if (type === 'sku') {
|
||||
metadata.sku = sku;
|
||||
lineItems = [{
|
||||
price: sku,
|
||||
quantity: 1,
|
||||
}];
|
||||
} else {
|
||||
const {
|
||||
amount,
|
||||
|
||||
@@ -22,6 +22,20 @@ function getGiftAmount (gift) {
|
||||
return `${(gift.gems.amount / 4) * 100}`;
|
||||
}
|
||||
|
||||
export async function applySku (session) {
|
||||
const { metadata } = session;
|
||||
const { userId, sku } = metadata;
|
||||
const user = await User.findById(metadata.userId).exec();
|
||||
if (!user) throw new NotFound(shared.i18n.t('userWithIDNotFound', { userId }));
|
||||
if (sku === 'price_0MPZ6iZCD0RifGXlLah2furv') {
|
||||
await payments.buySkuItem({
|
||||
sku, user, paymentMethod: stripeConstants.PAYMENT_METHOD,
|
||||
});
|
||||
} else {
|
||||
throw new NotFound('SKU not found.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOneTimePaymentInfo (gemsBlockKey, gift, user) {
|
||||
let receiver = user;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { // eslint-disable-line import/no-cycle
|
||||
basicFields as basicGroupFields,
|
||||
} from '../../../models/group';
|
||||
import shared from '../../../../common';
|
||||
import { applyGemPayment } from './oneTimePayments'; // eslint-disable-line import/no-cycle
|
||||
import { applyGemPayment, applySku } from './oneTimePayments'; // eslint-disable-line import/no-cycle
|
||||
import { applySubscription, handlePaymentMethodChange } from './subscriptions'; // eslint-disable-line import/no-cycle
|
||||
|
||||
const endpointSecret = nconf.get('STRIPE_WEBHOOKS_ENDPOINT_SECRET');
|
||||
@@ -69,10 +69,12 @@ export async function handleWebhooks (options, stripeInc) {
|
||||
|
||||
if (metadata.type === 'edit-card-group' || metadata.type === 'edit-card-user') {
|
||||
await handlePaymentMethodChange(session);
|
||||
} else if (metadata.type !== 'subscription') {
|
||||
await applyGemPayment(session);
|
||||
} else {
|
||||
} else if (metadata.type === 'subscription') {
|
||||
await applySubscription(session);
|
||||
} else if (metadata.type === 'sku') {
|
||||
await applySku(session);
|
||||
} else {
|
||||
await applyGemPayment(session);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -74,7 +74,39 @@ async function prepareSubscriptionValues (data) {
|
||||
? data.gift.subscription.key
|
||||
: data.sub.key];
|
||||
const autoRenews = data.autoRenews !== undefined ? data.autoRenews : true;
|
||||
const months = Number(block.months);
|
||||
const updatedFrom = data.updatedFrom
|
||||
? shared.content.subscriptionBlocks[data.updatedFrom.key]
|
||||
: undefined;
|
||||
let months;
|
||||
if (updatedFrom && Number(updatedFrom.months) !== 1) {
|
||||
if (Number(updatedFrom.months) > Number(block.months)) {
|
||||
months = 0;
|
||||
} else if (data.updatedFrom.logic === 'payDifference') {
|
||||
months = Math.max(0, Number(block.months) - Number(updatedFrom.months));
|
||||
} else if (data.updatedFrom.logic === 'payFull') {
|
||||
months = Number(block.months);
|
||||
} else if (data.updatedFrom.logic === 'refundAndRepay') {
|
||||
const originalMonths = Number(updatedFrom.months);
|
||||
let currentCycleBegin = moment(recipient.purchased.plan.dateCurrentTypeCreated);
|
||||
const today = moment();
|
||||
while (currentCycleBegin.isBefore()) {
|
||||
currentCycleBegin = currentCycleBegin.add({ months: originalMonths });
|
||||
}
|
||||
// Subtract last iteration again, because we overshot
|
||||
currentCycleBegin = currentCycleBegin.subtract({ months: originalMonths });
|
||||
// For simplicity we round every month to 30 days since moment can not add half months
|
||||
if (currentCycleBegin.add({ days: (originalMonths * 30) / 2.0 }).isBefore(today)) {
|
||||
// user is in second half of their subscription cycle. Give them full benefits.
|
||||
months = Number(block.months);
|
||||
} else {
|
||||
// user is in first half of their subscription cycle. Give them the difference.
|
||||
months = Math.max(0, Number(block.months) - Number(updatedFrom.months));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (months === undefined) {
|
||||
months = Number(block.months);
|
||||
}
|
||||
const today = new Date();
|
||||
let group;
|
||||
let groupId;
|
||||
@@ -82,6 +114,7 @@ async function prepareSubscriptionValues (data) {
|
||||
let purchaseType = 'subscribe';
|
||||
let emailType = 'subscription-begins';
|
||||
let recipientIsSubscribed = recipient.isSubscribed();
|
||||
const isNewSubscription = !recipientIsSubscribed;
|
||||
|
||||
// If we are buying a group subscription
|
||||
if (data.groupId) {
|
||||
@@ -122,6 +155,10 @@ async function prepareSubscriptionValues (data) {
|
||||
|
||||
const { plan } = recipient.purchased;
|
||||
|
||||
if (isNewSubscription) {
|
||||
plan.perkMonthCount = 0;
|
||||
}
|
||||
|
||||
if (data.gift || !autoRenews) {
|
||||
if (plan.customerId && !plan.dateTerminated) { // User has active plan
|
||||
plan.extraMonths += months;
|
||||
@@ -136,6 +173,7 @@ async function prepareSubscriptionValues (data) {
|
||||
plan.dateTerminated = moment().add({ months }).toDate();
|
||||
plan.dateCreated = today;
|
||||
}
|
||||
plan.dateCurrentTypeCreated = today;
|
||||
}
|
||||
|
||||
if (!plan.customerId) {
|
||||
@@ -152,6 +190,7 @@ async function prepareSubscriptionValues (data) {
|
||||
planId: block.key,
|
||||
customerId: data.customerId,
|
||||
dateUpdated: today,
|
||||
dateCurrentTypeCreated: today,
|
||||
paymentMethod: data.paymentMethod,
|
||||
extraMonths: Number(plan.extraMonths) + _dateDiff(today, plan.dateTerminated),
|
||||
dateTerminated: null,
|
||||
@@ -194,6 +233,7 @@ async function prepareSubscriptionValues (data) {
|
||||
itemPurchased,
|
||||
purchaseType,
|
||||
emailType,
|
||||
isNewSubscription,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -209,15 +249,22 @@ async function createSubscription (data) {
|
||||
itemPurchased,
|
||||
purchaseType,
|
||||
emailType,
|
||||
isNewSubscription,
|
||||
} = await prepareSubscriptionValues(data);
|
||||
|
||||
// Block sub perks
|
||||
const perks = Math.floor(months / 3);
|
||||
if (perks) {
|
||||
plan.consecutive.offset += months;
|
||||
plan.consecutive.gemCapExtra += perks * 5;
|
||||
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25;
|
||||
await plan.updateHourglasses(recipient._id, perks, 'subscription_perks'); // one Hourglass every 3 months
|
||||
if (months > 1 && (!data.gift || !isNewSubscription)) {
|
||||
if (!data.gift && !groupId) {
|
||||
plan.consecutive.offset = block.months;
|
||||
}
|
||||
} else if (months === 1) {
|
||||
plan.consecutive.offset = 0;
|
||||
}
|
||||
if (months > 1 || data.gift) {
|
||||
await plan.incrementPerkCounterAndReward(recipient._id, months);
|
||||
} else {
|
||||
// Make sure the perkMonthCount field is initialized.
|
||||
await plan.incrementPerkCounterAndReward(recipient._id, 0);
|
||||
}
|
||||
|
||||
if (recipient !== group) {
|
||||
|
||||
@@ -21,7 +21,7 @@ const apnProvider = APN_ENABLED ? new apn.Provider({
|
||||
}) : undefined;
|
||||
|
||||
function removePushDevice (user, pushDevice) {
|
||||
return User.update({ _id: user._id }, {
|
||||
return User.updateOne({ _id: user._id }, {
|
||||
$pull: { pushDevices: { regId: pushDevice.regId } },
|
||||
}).exec().catch(err => {
|
||||
logger.error(err, `Error removing pushDevice ${pushDevice.regId} for user ${user._id}`);
|
||||
|
||||
@@ -24,7 +24,7 @@ export function readController (router, controller, overrides = []) {
|
||||
|
||||
// If an authentication middleware is used run getUserLanguage after it, otherwise before
|
||||
// for cron instead use it only if an authentication middleware is present
|
||||
const authMiddlewareIndex = _.findIndex(middlewares, middleware => {
|
||||
let authMiddlewareIndex = _.findIndex(middlewares, middleware => {
|
||||
if (middleware.name.indexOf('authWith') === 0) { // authWith{Headers|Session|Url|...}
|
||||
return true;
|
||||
}
|
||||
@@ -36,6 +36,7 @@ export function readController (router, controller, overrides = []) {
|
||||
// disable caching for all routes with mandatory or optional authentication
|
||||
if (authMiddlewareIndex !== -1) {
|
||||
middlewares.unshift(disableCache);
|
||||
authMiddlewareIndex += 1;
|
||||
}
|
||||
|
||||
if (action.noLanguage !== true) { // unless getting the language is explictly disabled
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '../models/group';
|
||||
import apiError from './apiError';
|
||||
|
||||
const partyMembersFields = 'profile.name stats achievements items.special notifications flags pinnedItems';
|
||||
const partyMembersFields = 'profile.name stats achievements items.special pinnedItems notifications flags';
|
||||
// Excluding notifications and flags from the list of public fields to return.
|
||||
const partyMembersPublicFields = 'profile.name stats achievements items.special';
|
||||
|
||||
@@ -75,12 +75,13 @@ async function castSelfSpell (req, user, spell, quantity = 1) {
|
||||
await user.save();
|
||||
}
|
||||
|
||||
async function castPartySpell (req, party, partyMembers, user, spell, quantity = 1) {
|
||||
async function getPartyMembers (user, party) {
|
||||
let partyMembers;
|
||||
if (!party) {
|
||||
// Act as solo party
|
||||
partyMembers = [user]; // eslint-disable-line no-param-reassign
|
||||
partyMembers = [user];
|
||||
} else {
|
||||
partyMembers = await User // eslint-disable-line no-param-reassign
|
||||
partyMembers = await User
|
||||
.find({
|
||||
'party._id': party._id,
|
||||
_id: { $ne: user._id }, // add separately
|
||||
@@ -90,22 +91,40 @@ async function castPartySpell (req, party, partyMembers, user, spell, quantity =
|
||||
|
||||
partyMembers.unshift(user);
|
||||
}
|
||||
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
spell.cast(user, partyMembers, req);
|
||||
}
|
||||
await Promise.all(partyMembers.map(m => m.save()));
|
||||
|
||||
return partyMembers;
|
||||
}
|
||||
|
||||
async function castUserSpell (res, req, party, partyMembers, targetId, user, spell, quantity = 1) {
|
||||
async function castPartySpell (req, party, user, spell, quantity = 1) {
|
||||
let partyMembers;
|
||||
if (spell.bulk) {
|
||||
const data = { };
|
||||
if (party) {
|
||||
data.query = { 'party._id': party._id };
|
||||
} else {
|
||||
data.query = { _id: user._id };
|
||||
}
|
||||
spell.cast(user, data);
|
||||
await User.updateMany(data.query, data.update);
|
||||
await user.save();
|
||||
partyMembers = await getPartyMembers(user, party);
|
||||
} else {
|
||||
partyMembers = await getPartyMembers(user, party);
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
spell.cast(user, partyMembers, req);
|
||||
}
|
||||
await Promise.all(partyMembers.map(m => m.save()));
|
||||
}
|
||||
return partyMembers;
|
||||
}
|
||||
|
||||
async function castUserSpell (res, req, party, targetId, user, spell, quantity = 1) {
|
||||
let partyMembers;
|
||||
if (!party && (!targetId || user._id === targetId)) {
|
||||
partyMembers = user; // eslint-disable-line no-param-reassign
|
||||
partyMembers = user;
|
||||
} else {
|
||||
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
|
||||
if (!party) throw new NotFound(res.t('partyNotFound'));
|
||||
partyMembers = await User // eslint-disable-line no-param-reassign
|
||||
partyMembers = await User
|
||||
.findOne({ _id: targetId, 'party._id': party._id })
|
||||
.select(partyMembersFields)
|
||||
.exec();
|
||||
@@ -196,10 +215,10 @@ async function castSpell (req, res, { isV3 = false }) {
|
||||
let partyMembers;
|
||||
|
||||
if (targetType === 'party') {
|
||||
partyMembers = await castPartySpell(req, party, partyMembers, user, spell, quantity);
|
||||
partyMembers = await castPartySpell(req, party, user, spell, quantity);
|
||||
} else {
|
||||
partyMembers = await castUserSpell(
|
||||
res, req, party, partyMembers,
|
||||
res, req, party,
|
||||
targetId, user, spell, quantity,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ async function createTasks (req, res, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
await owner.update(taskOrderUpdateQuery).exec();
|
||||
await owner.updateOne(taskOrderUpdateQuery).exec();
|
||||
|
||||
// tasks with aliases need to be validated asynchronously
|
||||
await validateTaskAlias(toSave, res);
|
||||
|
||||
@@ -51,6 +51,7 @@ const updatablePaths = [
|
||||
'party.orderAscending',
|
||||
'party.quest.completed',
|
||||
'party.quest.RSVPNeeded',
|
||||
'party.seeking',
|
||||
|
||||
'preferences',
|
||||
'profile',
|
||||
@@ -97,7 +98,9 @@ function checkPreferencePurchase (user, path, item) {
|
||||
const itemPath = `${path}.${item}`;
|
||||
const appearance = _.get(common.content.appearances, itemPath);
|
||||
if (!appearance) return false;
|
||||
if (appearance.price === 0) return true;
|
||||
if (appearance.price === 0 && path !== 'background') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _.get(user.purchased, itemPath);
|
||||
}
|
||||
@@ -120,6 +123,17 @@ export async function update (req, res, { isV3 = false }) {
|
||||
|
||||
let promisesForTagsRemoval = [];
|
||||
|
||||
if (req.body['party.seeking'] !== undefined && req.body['party.seeking'] !== null) {
|
||||
user.invitations.party = {};
|
||||
user.invitations.parties = [];
|
||||
res.analytics.track('Starts Looking for Party', {
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body['profile.name'] !== undefined) {
|
||||
const newName = req.body['profile.name'];
|
||||
if (newName === null) throw new BadRequest(res.t('invalidReqParams'));
|
||||
@@ -168,7 +182,15 @@ export async function update (req, res, { isV3 = false }) {
|
||||
throw new NotAuthorized(res.t('mustPurchaseToSet', { val, key }));
|
||||
}
|
||||
|
||||
if (key === 'tags') {
|
||||
if (key === 'party.seeking' && val === null) {
|
||||
user.party.seeking = undefined;
|
||||
res.analytics.track('Leaves Looking for Party', {
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
} else if (key === 'tags') {
|
||||
if (!Array.isArray(val)) throw new BadRequest('Tag list must be an array.');
|
||||
|
||||
const removedTagsIds = [];
|
||||
@@ -198,13 +220,13 @@ export async function update (req, res, { isV3 = false }) {
|
||||
// Remove from all the tasks
|
||||
// NOTE each tag to remove requires a query
|
||||
|
||||
promisesForTagsRemoval = removedTagsIds.map(tagId => Tasks.Task.update({
|
||||
promisesForTagsRemoval = removedTagsIds.map(tagId => Tasks.Task.updateMany({
|
||||
userId: user._id,
|
||||
}, {
|
||||
$pull: {
|
||||
tags: tagId,
|
||||
},
|
||||
}, { multi: true }).exec());
|
||||
}).exec());
|
||||
} else if (key === 'flags.newStuff' && val === false) {
|
||||
// flags.newStuff was removed from the user schema and is only returned for compatibility
|
||||
// reasons but we're keeping the ability to set it in API v3
|
||||
@@ -254,6 +276,7 @@ export async function reset (req, res, { isV3 = false }) {
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
res.respond(200, ...resetRes);
|
||||
|
||||
@@ -61,7 +61,7 @@ function sendWebhook (webhook, body, user) {
|
||||
};
|
||||
}
|
||||
|
||||
return User.update({
|
||||
return User.updateOne({
|
||||
_id: user._id,
|
||||
'webhooks.id': webhook.id,
|
||||
}, update).exec();
|
||||
|
||||
Reference in New Issue
Block a user