website/server/libs/payments/amazon.js
import amazonPayments from 'amazon-payments';
import nconf from 'nconf';
import moment from 'moment';
import cc from 'coupon-code';
import util from 'util';
import common from '../../../common';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../errors';
import payments from './payments'; // eslint-disable-line import/no-cycle
import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
import { // eslint-disable-line import/no-cycle
model as Group,
basicFields as basicGroupFields,
} from '../../models/group';
import { model as Coupon } from '../../models/coupon';
import { getGemsBlock, validateGiftMessage } from './gems'; // eslint-disable-line import/no-cycle
// TODO better handling of errors
const { i18n } = common;
const IS_SANDBOX = nconf.get('AMAZON_PAYMENTS_MODE') === 'sandbox';
const amzPayment = amazonPayments.connect({
environment: amazonPayments.Environment[IS_SANDBOX ? 'Sandbox' : 'Production'],
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'),
});
const 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_BUY_SKU_ITEM: 'buySkuItem',
METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
PAYMENT_METHOD: 'Amazon Payments',
PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
};
api.getTokenInfo = util.promisify(amzPayment.api.getTokenInfo).bind(amzPayment.api);
api.createOrderReferenceId = util
.promisify(amzPayment.offAmazonPayments.createOrderReferenceForId)
.bind(amzPayment.offAmazonPayments);
api.setOrderReferenceDetails = util
.promisify(amzPayment.offAmazonPayments.setOrderReferenceDetails)
.bind(amzPayment.offAmazonPayments);
api.confirmOrderReference = util
.promisify(amzPayment.offAmazonPayments.confirmOrderReference)
.bind(amzPayment.offAmazonPayments);
api.closeOrderReference = util
.promisify(amzPayment.offAmazonPayments.closeOrderReference)
.bind(amzPayment.offAmazonPayments);
api.setBillingAgreementDetails = util
.promisify(amzPayment.offAmazonPayments.setBillingAgreementDetails)
.bind(amzPayment.offAmazonPayments);
api.getBillingAgreementDetails = util
.promisify(amzPayment.offAmazonPayments.getBillingAgreementDetails)
.bind(amzPayment.offAmazonPayments);
api.confirmBillingAgreement = util
.promisify(amzPayment.offAmazonPayments.confirmBillingAgreement)
.bind(amzPayment.offAmazonPayments);
api.closeBillingAgreement = util
.promisify(amzPayment.offAmazonPayments.closeBillingAgreement)
.bind(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 = {}) {
const {
gift, user, orderReferenceId, headers, gemsBlock: gemsBlockKey, sku,
} = options;
let amount;
let gemsBlock;
if (gift) {
gift.member = await User.findById(gift.uuid).exec();
validateGiftMessage(gift, user);
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;
}
} 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;
}
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;
if (sku) {
method = this.constants.METHOD_BUY_SKU_ITEM;
}
const data = {
user,
paymentMethod: this.constants.PAYMENT_METHOD,
headers,
gemsBlock,
sku,
};
if (gift) {
if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) {
method = this.constants.METHOD_CREATE_SUBSCRIPTION;
}
gift.member = await User.findById(gift.uuid).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 = {}) {
const {
user, groupId, headers, cancellationReason,
} = options;
let billingAgreementId;
let planId;
let lastBillingDate;
if (groupId) {
const groupFields = basicGroupFields.concat(' purchased');
const 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'));
const details = await this.getBillingAgreementDetails({
AmazonBillingAgreementId: billingAgreementId,
}).catch(err => err);
const badBAStates = ['Canceled', 'Closed', 'Suspended'];
if (
details
&& details.BillingAgreementDetails
&& details.BillingAgreementDetails.BillingAgreementStatus
&& badBAStates.indexOf(details.BillingAgreementDetails.BillingAgreementStatus.State) === -1
) {
await this.closeBillingAgreement({
AmazonBillingAgreementId: billingAgreementId,
});
}
const subscriptionBlock = common.content.subscriptionBlocks[planId];
const 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) {
const {
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'));
const result = await Coupon.findOne({ _id: cc.validate(coupon), event: sub.key }).exec();
if (!result) throw new NotAuthorized(i18n.t('invalidCoupon'));
}
let amount = sub.price;
const leaderCount = 1;
const priceOfSingleMember = 3;
if (groupId) {
const groupFields = basicGroupFields.concat(' purchased');
const group = await Group.getGroup({
user, groupId, populateLeader: false, groupFields,
});
const membersCount = await group.getMemberCount();
amount = sub.price + (membersCount - 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?
const priceForNewMember = 3;
// @TODO: Prorate?
return this.authorizeOnBillingAgreement({
AmazonBillingAgreementId: group.purchased.plan.customerId,
AuthorizationReferenceId: common.uuid().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: common.uuid(),
StoreName: this.constants.STORE_NAME,
},
});
};
export default api;