improve transactions logs + split createSubscription (#14289)

* improve transactions logs + split createSubscription
This commit is contained in:
negue 2022-10-27 08:39:06 +02:00 committed by GitHub
parent 90250d1a25
commit f7a03d2eb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 128 additions and 44 deletions

View file

@ -11,10 +11,13 @@ import {
generateGroup,
} from '../../../../helpers/api-unit.helper';
import * as worldState from '../../../../../website/server/libs/worldState';
import { TransactionModel } from '../../../../../website/server/models/transaction';
describe('payments/index', () => {
let user; let group; let data; let
plan;
let user;
let group;
let data;
let plan;
beforeEach(async () => {
user = new User();
@ -104,6 +107,23 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.extraMonths).to.eql(3);
});
it('add a transaction entry to the recipient', async () => {
recipient.purchased.plan = plan;
expect(recipient.purchased.plan.extraMonths).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.extraMonths).to.eql(3);
const transactions = await TransactionModel
.find({ userId: recipient._id })
.sort({ createdAt: -1 })
.exec();
expect(transactions).to.have.lengthOf(1);
});
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
const dateTerminated = moment().subtract(2, 'months').toDate();
recipient.purchased.plan.dateTerminated = dateTerminated;

View file

@ -279,11 +279,16 @@ api.updateHero = {
}
if (updateData.purchased.plan.consecutive) {
if (updateData.purchased.plan.consecutive.trinkets) {
await hero.updateHourglasses(
updateData.purchased.plan.consecutive.trinkets
- hero.purchased.plan.consecutive.trinkets,
'admin_update_hourglasses', '', 'Updated by Habitica staff',
);
const changedHourglassTrinkets = updateData.purchased.plan.consecutive.trinkets
- hero.purchased.plan.consecutive.trinkets;
if (changedHourglassTrinkets !== 0) {
await hero.updateHourglasses(
changedHourglassTrinkets,
'admin_update_hourglasses', '', 'Updated by Habitica staff',
);
}
hero.purchased.plan.consecutive.trinkets = updateData.purchased.plan.consecutive.trinkets;
}
if (updateData.purchased.plan.consecutive.gemCapExtra) {

View file

@ -0,0 +1,7 @@
export const paymentConstants = {
UNLIMITED_CUSTOMER_ID: 'habitrpg', // Users with the customerId have an unlimted free subscription
GROUP_PLAN_CUSTOMER_ID: 'group-plan',
GROUP_PLAN_PAYMENT_METHOD: 'Group Plan',
GOOGLE_PAYMENT_METHOD: 'Google',
IOS_PAYMENT_METHOD: 'Apple',
};

View file

@ -1,3 +1,5 @@
// TODO these files need to refactored.
import nconf from 'nconf';
import _ from 'lodash';
import moment from 'moment';
@ -12,6 +14,8 @@ import { // eslint-disable-line import/no-cycle
getUserInfo,
sendTxn as txnEmail,
} from '../email';
import { paymentConstants } from './constants';
import { cancelSubscription, createSubscription } from './subscriptions'; // eslint-disable-line import/no-cycle
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL');
const JOINED_GROUP_PLAN = 'joined group plan';
@ -37,7 +41,8 @@ async function addSubscriptionToGroupUsers (group) {
members = await User.find({ 'party._id': group._id }).select('_id purchased items auth profile.name notifications').exec();
}
const promises = members.map(member => this.addSubToGroupUser(member, group));
// eslint-disable-next-line no-use-before-define
const promises = members.map(member => addSubToGroupUser(member, group));
await Promise.all(promises);
}
@ -63,12 +68,12 @@ async function addSubToGroupUser (member, group) {
// When changing customerIdsToIgnore or paymentMethodsToIgnore, the code blocks below for
// the `group-member-join` email template will probably need to be changed.
const customerIdsToIgnore = [
this.constants.GROUP_PLAN_CUSTOMER_ID,
this.constants.UNLIMITED_CUSTOMER_ID,
paymentConstants.GROUP_PLAN_CUSTOMER_ID,
paymentConstants.UNLIMITED_CUSTOMER_ID,
];
const paymentMethodsToIgnore = [
this.constants.GOOGLE_PAYMENT_METHOD,
this.constants.IOS_PAYMENT_METHOD,
paymentConstants.GOOGLE_PAYMENT_METHOD,
paymentConstants.IOS_PAYMENT_METHOD,
];
let previousSubscriptionType = EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE;
const leader = await User.findById(group.leader).exec();
@ -104,7 +109,7 @@ async function addSubToGroupUser (member, group) {
const memberPlan = member.purchased.plan;
if (member.isSubscribed()) {
const customerHasCancelledGroupPlan = (
memberPlan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID
memberPlan.customerId === paymentConstants.GROUP_PLAN_CUSTOMER_ID
&& !member.hasNotCancelled()
);
const ignorePaymentPlan = paymentMethodsToIgnore.indexOf(memberPlan.paymentMethod) !== -1;
@ -127,13 +132,13 @@ async function addSubToGroupUser (member, group) {
if ((ignorePaymentPlan || ignoreCustomerId) && !customerHasCancelledGroupPlan) {
// member has been added to group plan but their subscription will not be changed
// automatically so they need a special message in the email
if (memberPlan.paymentMethod === this.constants.GOOGLE_PAYMENT_METHOD) {
if (memberPlan.paymentMethod === paymentConstants.GOOGLE_PAYMENT_METHOD) {
previousSubscriptionType = EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE;
} else if (memberPlan.paymentMethod === this.constants.IOS_PAYMENT_METHOD) {
} else if (memberPlan.paymentMethod === paymentConstants.IOS_PAYMENT_METHOD) {
previousSubscriptionType = EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS;
} else if (memberPlan.customerId === this.constants.UNLIMITED_CUSTOMER_ID) {
} else if (memberPlan.customerId === paymentConstants.UNLIMITED_CUSTOMER_ID) {
previousSubscriptionType = EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_LIFETIME_FREE;
} else if (memberPlan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID) {
} else if (memberPlan.customerId === paymentConstants.GROUP_PLAN_CUSTOMER_ID) {
previousSubscriptionType = EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GROUP_PLAN;
} else {
// this triggers a generic message in the email template in case we forget
@ -183,7 +188,7 @@ async function addSubToGroupUser (member, group) {
member.markModified('items.mounts');
data.user = member;
await this.createSubscription(data);
await createSubscription(data);
txnEmail(data.user, 'group-member-join', [
{ name: 'LEADER', content: leader.profile.name },
@ -207,13 +212,14 @@ async function cancelGroupUsersSubscription (group) {
members = await User.find({ 'party._id': group._id }).select('_id guilds purchased').exec();
}
const promises = members.map(member => this.cancelGroupSubscriptionForUser(member, group));
// eslint-disable-next-line no-use-before-define
const promises = members.map(member => cancelGroupSubscriptionForUser(member, group));
await Promise.all(promises);
}
async function cancelGroupSubscriptionForUser (user, group, userWasRemoved = false) {
if (user.purchased.plan.customerId !== this.constants.GROUP_PLAN_CUSTOMER_ID) return;
if (user.purchased.plan.customerId !== paymentConstants.GROUP_PLAN_CUSTOMER_ID) return;
const userGroups = user.guilds.toObject();
if (user.party._id) userGroups.push(user.party._id);
@ -241,7 +247,7 @@ async function cancelGroupSubscriptionForUser (user, group, userWasRemoved = fal
{ name: 'LEADER', content: leader.profile.name },
{ name: 'GROUP_NAME', content: group.name },
]);
await this.cancelSubscription({ user });
await cancelSubscription({ user });
}
}

View file

@ -11,16 +11,11 @@ import { // eslint-disable-line import/no-cycle
import { // eslint-disable-line import/no-cycle
buyGems,
} from './gems';
import { paymentConstants } from './constants';
const api = {};
api.constants = {
UNLIMITED_CUSTOMER_ID: 'habitrpg', // Users with the customerId have an unlimted free subscription
GROUP_PLAN_CUSTOMER_ID: 'group-plan',
GROUP_PLAN_PAYMENT_METHOD: 'Group Plan',
GOOGLE_PAYMENT_METHOD: 'Google',
IOS_PAYMENT_METHOD: 'Apple',
};
api.constants = paymentConstants;
api.addSubscriptionToGroupUsers = addSubscriptionToGroupUsers;

View file

@ -1,3 +1,5 @@
// TODO these files need to refactored.
import _ from 'lodash';
import moment from 'moment';
@ -20,6 +22,8 @@ import shared from '../../../common';
import { sendNotification as sendPushNotification } from '../pushNotifications'; // eslint-disable-line import/no-cycle
import calculateSubscriptionTerminationDate from './calculateSubscriptionTerminationDate';
import { getCurrentEventList } from '../worldState'; // eslint-disable-line import/no-cycle
import { paymentConstants } from './constants';
import { addSubscriptionToGroupUsers, cancelGroupUsersSubscription } from './groupPayments'; // eslint-disable-line import/no-cycle
// @TODO: Abstract to shared/constant
const JOINED_GROUP_PLAN = 'joined group plan';
@ -65,7 +69,7 @@ function _dateDiff (earlyDate, lateDate) {
return moment(lateDate).diff(earlyDate, 'months', true);
}
async function createSubscription (data) {
async function prepareSubscriptionValues (data) {
let recipient = data.gift ? data.gift.member : data.user;
const block = shared.content.subscriptionBlocks[data.gift
? data.gift.subscription.key
@ -105,7 +109,7 @@ async function createSubscription (data) {
if (group) {
analytics.track(
this.groupID,
data.groupID,
data.demographics,
);
}
@ -130,7 +134,7 @@ async function createSubscription (data) {
groupId = group._id;
recipient.purchased.plan.quantity = data.sub.quantity;
await this.addSubscriptionToGroupUsers(group);
await addSubscriptionToGroupUsers(group);
}
const { plan } = recipient.purchased;
@ -139,7 +143,10 @@ async function createSubscription (data) {
if (plan.customerId && !plan.dateTerminated) { // User has active plan
plan.extraMonths += months;
} else {
if (!recipientIsSubscribed || !plan.dateUpdated) plan.dateUpdated = today;
if (!recipientIsSubscribed || !plan.dateUpdated) {
plan.dateUpdated = today;
}
if (moment(plan.dateTerminated).isAfter()) {
plan.dateTerminated = moment(plan.dateTerminated).add({ months }).toDate();
} else {
@ -148,9 +155,15 @@ async function createSubscription (data) {
}
}
if (!plan.customerId) plan.customerId = 'Gift'; // don't override existing customer, but all sub need a customerId
if (!plan.customerId) {
plan.customerId = 'Gift';
}
// don't override existing customer, but all sub need a customerId
} else {
if (!plan.dateTerminated) plan.dateTerminated = today;
if (!plan.dateTerminated) {
plan.dateTerminated = today;
}
Object.assign(plan, { // override plan with new values
planId: block.key,
@ -170,22 +183,58 @@ async function createSubscription (data) {
});
// allow non-override if a plan was previously used
if (!plan.gemsBought) plan.gemsBought = 0;
if (!plan.dateCreated) plan.dateCreated = today;
if (!plan.mysteryItems) plan.mysteryItems = [];
if (!plan.gemsBought) {
plan.gemsBought = 0;
}
if (!plan.dateCreated) {
plan.dateCreated = today;
}
if (!plan.mysteryItems) {
plan.mysteryItems = [];
}
if (data.subscriptionId) {
plan.subscriptionId = data.subscriptionId;
}
}
return {
block,
months,
plan,
recipient,
autoRenews,
group,
groupId,
itemPurchased,
purchaseType,
emailType,
};
}
async function createSubscription (data) {
const {
block,
months,
plan,
recipient,
autoRenews,
group,
groupId,
itemPurchased,
purchaseType,
emailType,
} = 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(data.user._id, perks, 'subscription_perks'); // one Hourglass every 3 months
await plan.updateHourglasses(recipient._id, perks, 'subscription_perks'); // one Hourglass every 3 months
}
if (recipient !== group) {
@ -195,7 +244,7 @@ async function createSubscription (data) {
}
// @TODO: Create a factory pattern for use cases
if (!data.gift && data.customerId !== this.constants.GROUP_PLAN_CUSTOMER_ID) {
if (!data.gift && data.customerId !== paymentConstants.GROUP_PLAN_CUSTOMER_ID) {
txnEmail(data.user, emailType);
}
@ -284,7 +333,7 @@ async function createSubscription (data) {
promo: 'Winter',
promoUsername: data.gift.member.auth.local.username,
};
await this.createSubscription(promoData);
await createSubscription(promoData);
}
if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) {
@ -365,7 +414,7 @@ async function cancelSubscription (data) {
emailType = 'group-cancel-subscription';
emailMergeData.push({ name: 'GROUP_NAME', content: group.name });
await this.cancelGroupUsersSubscription(group);
await cancelGroupUsersSubscription(group);
} else {
// cancelling a user subscription
plan = data.user.purchased.plan;
@ -375,12 +424,12 @@ async function cancelSubscription (data) {
if (data.cancellationReason && data.cancellationReason === JOINED_GROUP_PLAN) sendEmail = false;
}
if (plan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID) {
if (plan.customerId === paymentConstants.GROUP_PLAN_CUSTOMER_ID) {
sendEmail = false; // because group-member-cancel email has already been sent
}
plan.dateTerminated = calculateSubscriptionTerminationDate(
data.nextBill, plan, this.constants.GROUP_PLAN_CUSTOMER_ID,
data.nextBill, plan, paymentConstants.GROUP_PLAN_CUSTOMER_ID,
);
// clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
@ -392,7 +441,9 @@ async function cancelSubscription (data) {
await data.user.save();
}
if (sendEmail) txnEmail(data.user, emailType, emailMergeData);
if (sendEmail) {
txnEmail(data.user, emailType, emailMergeData);
}
if (group) {
cancelType = 'group-unsubscribe';