mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 13:47:33 +01:00
* add possibility for group to block members from getting gems * fixes * fix tests * adds some tests * unit tests * finish unit tests * remove old code
348 lines
12 KiB
JavaScript
348 lines
12 KiB
JavaScript
import amazonPayments from 'amazon-payments';
|
|
import nconf from 'nconf';
|
|
import Bluebird from 'bluebird';
|
|
import moment from 'moment';
|
|
import cc from 'coupon-code';
|
|
import uuid from 'uuid';
|
|
|
|
import common from '../../common';
|
|
import {
|
|
BadRequest,
|
|
NotAuthorized,
|
|
NotFound,
|
|
} from './errors';
|
|
import payments from './payments';
|
|
import { model as User } from '../models/user';
|
|
import {
|
|
model as Group,
|
|
basicFields as basicGroupFields,
|
|
} from '../models/group';
|
|
import { model as Coupon } from '../models/coupon';
|
|
|
|
// TODO better handling of errors
|
|
|
|
const i18n = common.i18n;
|
|
const IS_PROD = nconf.get('NODE_ENV') === 'production';
|
|
|
|
let amzPayment = amazonPayments.connect({
|
|
environment: amazonPayments.Environment[IS_PROD ? 'Production' : 'Sandbox'],
|
|
sellerId: nconf.get('AMAZON_PAYMENTS:SELLER_ID'),
|
|
mwsAccessKey: nconf.get('AMAZON_PAYMENTS:MWS_KEY'),
|
|
mwsSecretKey: nconf.get('AMAZON_PAYMENTS:MWS_SECRET'),
|
|
clientId: nconf.get('AMAZON_PAYMENTS:CLIENT_ID'),
|
|
});
|
|
|
|
let api = {};
|
|
|
|
api.constants = {
|
|
CURRENCY_CODE: 'USD',
|
|
SELLER_NOTE: 'Habitica Payment',
|
|
SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription',
|
|
SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment',
|
|
SELLER_NOTE_GROUP_NEW_MEMBER: 'Habitica Group Plan New Member',
|
|
STORE_NAME: 'Habitica',
|
|
|
|
GIFT_TYPE_GEMS: 'gems',
|
|
GIFT_TYPE_SUBSCRIPTION: 'subscription',
|
|
|
|
METHOD_BUY_GEMS: 'buyGems',
|
|
METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
|
|
PAYMENT_METHOD: 'Amazon Payments',
|
|
PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
|
|
};
|
|
|
|
api.getTokenInfo = Bluebird.promisify(amzPayment.api.getTokenInfo, {context: amzPayment.api});
|
|
api.createOrderReferenceId = Bluebird.promisify(amzPayment.offAmazonPayments.createOrderReferenceForId, {context: amzPayment.offAmazonPayments});
|
|
api.setOrderReferenceDetails = Bluebird.promisify(amzPayment.offAmazonPayments.setOrderReferenceDetails, {context: amzPayment.offAmazonPayments});
|
|
api.confirmOrderReference = Bluebird.promisify(amzPayment.offAmazonPayments.confirmOrderReference, {context: amzPayment.offAmazonPayments});
|
|
api.closeOrderReference = Bluebird.promisify(amzPayment.offAmazonPayments.closeOrderReference, {context: amzPayment.offAmazonPayments});
|
|
api.setBillingAgreementDetails = Bluebird.promisify(amzPayment.offAmazonPayments.setBillingAgreementDetails, {context: amzPayment.offAmazonPayments});
|
|
api.getBillingAgreementDetails = Bluebird.promisify(amzPayment.offAmazonPayments.getBillingAgreementDetails, {context: amzPayment.offAmazonPayments});
|
|
api.confirmBillingAgreement = Bluebird.promisify(amzPayment.offAmazonPayments.confirmBillingAgreement, {context: amzPayment.offAmazonPayments});
|
|
api.closeBillingAgreement = Bluebird.promisify(amzPayment.offAmazonPayments.closeBillingAgreement, {context: amzPayment.offAmazonPayments});
|
|
|
|
api.authorizeOnBillingAgreement = function authorizeOnBillingAgreement (inputSet) {
|
|
return new Promise((resolve, reject) => {
|
|
amzPayment.offAmazonPayments.authorizeOnBillingAgreement(inputSet, (err, response) => {
|
|
if (err) return reject(err);
|
|
if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(new BadRequest(i18n.t('paymentNotSuccessful')));
|
|
return resolve(response);
|
|
});
|
|
});
|
|
};
|
|
|
|
api.authorize = function authorize (inputSet) {
|
|
return new Promise((resolve, reject) => {
|
|
amzPayment.offAmazonPayments.authorize(inputSet, (err, response) => {
|
|
if (err) return reject(err);
|
|
if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(new BadRequest(i18n.t('paymentNotSuccessful')));
|
|
return resolve(response);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Makes a purchase using Amazon Payment Lib
|
|
*
|
|
* @param options
|
|
* @param options.user The user object who is purchasing
|
|
* @param options.gift The gift details if any
|
|
* @param options.orderReferenceId The amazon orderReferenceId generated on the front end
|
|
* @param options.headers The request headers
|
|
*
|
|
* @return undefined
|
|
*/
|
|
api.checkout = async function checkout (options = {}) {
|
|
let {gift, user, orderReferenceId, headers} = 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'));
|
|
}
|
|
amount = gift.gems.amount / 4;
|
|
} else if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) {
|
|
amount = common.content.subscriptionBlocks[gift.subscription.key].price;
|
|
}
|
|
}
|
|
|
|
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: {
|
|
OrderTotal: {
|
|
CurrencyCode: this.constants.CURRENCY_CODE,
|
|
Amount: amount,
|
|
},
|
|
SellerNote: this.constants.SELLER_NOTE,
|
|
SellerOrderAttributes: {
|
|
SellerOrderId: common.uuid(),
|
|
StoreName: this.constants.STORE_NAME,
|
|
},
|
|
},
|
|
});
|
|
|
|
await this.confirmOrderReference({ AmazonOrderReferenceId: orderReferenceId });
|
|
|
|
await this.authorize({
|
|
AmazonOrderReferenceId: orderReferenceId,
|
|
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
|
AuthorizationAmount: {
|
|
CurrencyCode: this.constants.CURRENCY_CODE,
|
|
Amount: amount,
|
|
},
|
|
SellerAuthorizationNote: this.constants.SELLER_NOTE,
|
|
TransactionTimeout: 0,
|
|
CaptureNow: true,
|
|
});
|
|
|
|
await this.closeOrderReference({ AmazonOrderReferenceId: orderReferenceId });
|
|
|
|
// execute payment
|
|
let method = this.constants.METHOD_BUY_GEMS;
|
|
|
|
let data = {
|
|
user,
|
|
paymentMethod: this.constants.PAYMENT_METHOD,
|
|
headers,
|
|
};
|
|
|
|
if (gift) {
|
|
if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) method = this.constants.METHOD_CREATE_SUBSCRIPTION;
|
|
gift.member = await User.findById(gift ? gift.uuid : undefined).exec();
|
|
data.gift = gift;
|
|
data.paymentMethod = this.constants.PAYMENT_METHOD_GIFT;
|
|
}
|
|
|
|
await payments[method](data);
|
|
};
|
|
|
|
/**
|
|
* Cancel an Amazon Subscription
|
|
*
|
|
* @param options
|
|
* @param options.user The user object who is canceling
|
|
* @param options.groupId The id of the group that is canceling
|
|
* @param options.headers The request headers
|
|
* @param options.cancellationReason A text string to control sending an email
|
|
*
|
|
* @return undefined
|
|
*/
|
|
api.cancelSubscription = async function cancelSubscription (options = {}) {
|
|
let {user, groupId, headers, cancellationReason} = options;
|
|
|
|
let billingAgreementId;
|
|
let planId;
|
|
let lastBillingDate;
|
|
|
|
if (groupId) {
|
|
let groupFields = basicGroupFields.concat(' purchased');
|
|
let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
|
|
|
if (!group) {
|
|
throw new NotFound(i18n.t('groupNotFound'));
|
|
}
|
|
|
|
if (group.leader !== user._id) {
|
|
throw new NotAuthorized(i18n.t('onlyGroupLeaderCanManageSubscription'));
|
|
}
|
|
|
|
billingAgreementId = group.purchased.plan.customerId;
|
|
planId = group.purchased.plan.planId;
|
|
lastBillingDate = group.purchased.plan.lastBillingDate;
|
|
} else {
|
|
billingAgreementId = user.purchased.plan.customerId;
|
|
planId = user.purchased.plan.planId;
|
|
lastBillingDate = user.purchased.plan.lastBillingDate;
|
|
}
|
|
|
|
if (!billingAgreementId) throw new NotAuthorized(i18n.t('missingSubscription'));
|
|
|
|
let details = await this.getBillingAgreementDetails({
|
|
AmazonBillingAgreementId: billingAgreementId,
|
|
}).catch(function errorCatch (err) {
|
|
return err;
|
|
});
|
|
|
|
let badBAStates = ['Canceled', 'Closed', 'Suspended'];
|
|
if (details && details.BillingAgreementDetails && details.BillingAgreementDetails.BillingAgreementStatus &&
|
|
badBAStates.indexOf(details.BillingAgreementDetails.BillingAgreementStatus.State) === -1) {
|
|
await this.closeBillingAgreement({
|
|
AmazonBillingAgreementId: billingAgreementId,
|
|
});
|
|
}
|
|
|
|
|
|
let subscriptionBlock = common.content.subscriptionBlocks[planId];
|
|
let subscriptionLength = subscriptionBlock.months * 30;
|
|
|
|
await payments.cancelSubscription({
|
|
user,
|
|
groupId,
|
|
nextBill: moment(lastBillingDate).add({ days: subscriptionLength }),
|
|
paymentMethod: this.constants.PAYMENT_METHOD,
|
|
headers,
|
|
cancellationReason,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Allows for purchasing a user subscription or group subscription with Amazon
|
|
*
|
|
* @param options
|
|
* @param options.billingAgreementId The Amazon billingAgreementId generated on the front end
|
|
* @param options.user The user object who is purchasing
|
|
* @param options.sub The subscription data to purchase
|
|
* @param options.coupon The coupon to discount the sub
|
|
* @param options.groupId The id of the group purchasing a subscription
|
|
* @param options.headers The request headers to store on analytics
|
|
* @return undefined
|
|
*/
|
|
api.subscribe = async function subscribe (options) {
|
|
let {
|
|
billingAgreementId,
|
|
sub,
|
|
coupon,
|
|
user,
|
|
groupId,
|
|
headers,
|
|
} = options;
|
|
|
|
if (!sub) throw new BadRequest(i18n.t('missingSubscriptionCode'));
|
|
if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId');
|
|
|
|
if (sub.discount) { // apply discount
|
|
if (!coupon) throw new BadRequest(i18n.t('couponCodeRequired'));
|
|
let result = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec();
|
|
if (!result) throw new NotAuthorized(i18n.t('invalidCoupon'));
|
|
}
|
|
|
|
let amount = sub.price;
|
|
let leaderCount = 1;
|
|
let priceOfSingleMember = 3;
|
|
|
|
if (groupId) {
|
|
let groupFields = basicGroupFields.concat(' purchased');
|
|
let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
|
|
|
amount = sub.price + (group.memberCount - leaderCount) * priceOfSingleMember;
|
|
}
|
|
|
|
await this.setBillingAgreementDetails({
|
|
AmazonBillingAgreementId: billingAgreementId,
|
|
BillingAgreementAttributes: {
|
|
SellerNote: this.constants.SELLER_NOTE_SUBSCRIPTION,
|
|
SellerBillingAgreementAttributes: {
|
|
SellerBillingAgreementId: common.uuid(),
|
|
StoreName: this.constants.STORE_NAME,
|
|
CustomInformation: this.constants.SELLER_NOTE_SUBSCRIPTION,
|
|
},
|
|
},
|
|
});
|
|
|
|
await this.confirmBillingAgreement({
|
|
AmazonBillingAgreementId: billingAgreementId,
|
|
});
|
|
|
|
await this.authorizeOnBillingAgreement({
|
|
AmazonBillingAgreementId: billingAgreementId,
|
|
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
|
AuthorizationAmount: {
|
|
CurrencyCode: this.constants.CURRENCY_CODE,
|
|
Amount: amount,
|
|
},
|
|
SellerAuthorizationNote: this.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
|
TransactionTimeout: 0,
|
|
CaptureNow: true,
|
|
SellerNote: this.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
|
SellerOrderAttributes: {
|
|
SellerOrderId: common.uuid(),
|
|
StoreName: this.constants.STORE_NAME,
|
|
},
|
|
});
|
|
|
|
await payments.createSubscription({
|
|
user,
|
|
customerId: billingAgreementId,
|
|
paymentMethod: this.constants.PAYMENT_METHOD,
|
|
sub,
|
|
headers,
|
|
groupId,
|
|
});
|
|
};
|
|
|
|
|
|
api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) {
|
|
// @TODO: Can we get this from the content plan?
|
|
let priceForNewMember = 3;
|
|
|
|
// @TODO: Prorate?
|
|
|
|
return this.authorizeOnBillingAgreement({
|
|
AmazonBillingAgreementId: group.purchased.plan.customerId,
|
|
AuthorizationReferenceId: uuid.v4().substring(0, 32),
|
|
AuthorizationAmount: {
|
|
CurrencyCode: this.constants.CURRENCY_CODE,
|
|
Amount: priceForNewMember,
|
|
},
|
|
SellerAuthorizationNote: this.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
|
TransactionTimeout: 0,
|
|
CaptureNow: true,
|
|
SellerNote: this.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
|
SellerOrderAttributes: {
|
|
SellerOrderId: uuid.v4(),
|
|
StoreName: this.constants.STORE_NAME,
|
|
},
|
|
});
|
|
};
|
|
|
|
module.exports = api;
|