mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-07-13 17:52:22 +00:00
improve transactions logs + split createSubscription (#14289)
* improve transactions logs + split createSubscription
This commit is contained in:
parent
90250d1a25
commit
f7a03d2eb5
6 changed files with 128 additions and 44 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
7
website/server/libs/payments/constants.js
Normal file
7
website/server/libs/payments/constants.js
Normal 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',
|
||||
};
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue