feat(amazon-payments): working subscriptions, put donations and gifts on hold

This commit is contained in:
Matteo Pagliazzi
2015-06-30 17:55:25 +02:00
parent 8747cf4cce
commit 65f4aac23c
8 changed files with 222 additions and 64 deletions

View File

@@ -65,6 +65,7 @@ function($rootScope, User, $http, Content) {
console.error(error); console.error(error);
console.log(error.getErrorMessage(), error.getErrorCode()); console.log(error.getErrorMessage(), error.getErrorCode());
alert(error.getErrorMessage()); alert(error.getErrorMessage());
Payments.amazonPayments.reset();
}; };
Payments.amazonPayments = {}; Payments.amazonPayments = {};
@@ -79,24 +80,30 @@ function($rootScope, User, $http, Content) {
Payments.amazonPayments.type = null; Payments.amazonPayments.type = null;
Payments.amazonPayments.loggedIn = false; Payments.amazonPayments.loggedIn = false;
Payments.amazonPayments.gift = null; Payments.amazonPayments.gift = null;
Payments.amazonPayments.orderReferenceId = null;
Payments.amazonPayments.billingAgreementId = null; Payments.amazonPayments.billingAgreementId = null;
Payments.amazonPayments.paymentSelected = false; Payments.amazonPayments.paymentSelected = false;
Payments.amazonPayments.recurringConsent = false; Payments.amazonPayments.recurringConsent = false;
Payments.amazonPayments.subscription = null;
Payments.amazonPayments.coupon = null;
}; };
// Needs to be called everytime the modal/router is accessed // Needs to be called everytime the modal/router is accessed
Payments.amazonPayments.init = function(type, gift, giftedTo){ Payments.amazonPayments.init = function(data){
if(gift){ if(!isAmazonReady) return;
if(gift.gems && gift.gems.amount && gift.gems.amount <= 0) return; if(data.type !== 'donation' && data.type !== 'subscription') return;
gift.uuid = giftedTo;
if(data.gift){
if(data.gift.gems && data.gift.gems.amount && data.gift.gems.amount <= 0) return;
gift.uuid = data.giftedTo;
} }
if(!isAmazonReady) return; if(data.subscription){
if(type !== 'donation' && type !== 'subscription') return; Payments.amazonPayments.subscription = data.subscription;
Payments.amazonPayments.coupon = data.coupon;
}
Payments.amazonPayments.gift = gift; Payments.amazonPayments.gift = data.gift;
Payments.amazonPayments.type = type; Payments.amazonPayments.type = data.type;
var modal = Payments.amazonPayments.modal = $rootScope.openModal('amazonPayments', { var modal = Payments.amazonPayments.modal = $rootScope.openModal('amazonPayments', {
// Allow the modal to be closed only by pressing cancel // Allow the modal to be closed only by pressing cancel
@@ -153,24 +160,12 @@ function($rootScope, User, $http, Content) {
design: { design: {
designMode: 'responsive' designMode: 'responsive'
}, },
agreementType: 'BillingAgreement',
onPaymentSelect: function(orderReference) { onReady: function(billingAgreement){
$rootScope.$apply(function(){
Payments.amazonPayments.paymentSelected = true;
});
},
onError: amazonOnError
};
if(Payments.amazonPayments.type === 'donation'){
walletParams.onOrderReferenceCreate = function(orderReference) {
Payments.amazonPayments.orderReferenceId = orderReference.getAmazonOrderReferenceId();
}
}else if(Payments.amazonPayments.type === 'subscription'){
walletParams.onReady = function(billingAgreement) {
Payments.amazonPayments.billingAgreementId = billingAgreement.getAmazonBillingAgreementId(); Payments.amazonPayments.billingAgreementId = billingAgreement.getAmazonBillingAgreementId();
if(Payments.amazonPayments.type === 'subscription'){
new OffAmazonPayments.Widgets.Consent({ new OffAmazonPayments.Widgets.Consent({
sellerId: window.env.AMAZON_PAYMENTS.SELLER_ID, sellerId: window.env.AMAZON_PAYMENTS.SELLER_ID,
amazonBillingAgreementId: Payments.amazonPayments.billingAgreementId, amazonBillingAgreementId: Payments.amazonPayments.billingAgreementId,
@@ -180,7 +175,8 @@ function($rootScope, User, $http, Content) {
onReady: function(consent){ onReady: function(consent){
$rootScope.$apply(function(){ $rootScope.$apply(function(){
Payments.amazonPayments.recurringConsent = consent.getConsentStatus(); var getConsent = consent.getConsentStatus
Payments.amazonPayments.recurringConsent = getConsent ? getConsent() : false;
}); });
}, },
@@ -192,10 +188,17 @@ function($rootScope, User, $http, Content) {
onError: amazonOnError onError: amazonOnError
}).bind('AmazonPayRecurring'); }).bind('AmazonPayRecurring');
};
walletParams.agreementType = 'BillingAgreement';
} }
},
onPaymentSelect: function() {
$rootScope.$apply(function(){
Payments.amazonPayments.paymentSelected = true;
});
},
onError: amazonOnError
};
new OffAmazonPayments.Widgets.Wallet(walletParams).bind('AmazonPayWallet'); new OffAmazonPayments.Widgets.Wallet(walletParams).bind('AmazonPayWallet');
} }
@@ -204,7 +207,7 @@ function($rootScope, User, $http, Content) {
if(Payments.amazonPayments.type === 'donation'){ if(Payments.amazonPayments.type === 'donation'){
var url = '/amazon/checkout' var url = '/amazon/checkout'
$http.post(url, { $http.post(url, {
orderReferenceId: Payments.amazonPayments.orderReferenceId, billingAgreementId: Payments.amazonPayments.billingAgreementId,
gift: Payments.amazonPayments.gift gift: Payments.amazonPayments.gift
}).success(function(){ }).success(function(){
Payments.amazonPayments.reset(); Payments.amazonPayments.reset();
@@ -214,13 +217,33 @@ function($rootScope, User, $http, Content) {
Payments.amazonPayments.reset(); Payments.amazonPayments.reset();
}); });
}else if(Payments.amazonPayments.type === 'subscription'){ }else if(Payments.amazonPayments.type === 'subscription'){
return false var url = '/amazon/subscribe';
$http.post(url, {
billingAgreementId: Payments.amazonPayments.billingAgreementId,
subscription: Payments.amazonPayments.subscription,
coupon: Payments.amazonPayments.coupon
}).success(function(){
Payments.amazonPayments.reset();
window.location.reload(true);
}).error(function(res){
alert(res.err);
Payments.amazonPayments.reset();
});
} }
} }
Payments.cancelSubscription = function(){ Payments.cancelSubscription = function(){
if (!confirm(window.env.t('sureCancelSub'))) return; if (!confirm(window.env.t('sureCancelSub'))) return;
window.location.href = '/' + User.user.purchased.plan.paymentMethod.toLowerCase() + '/subscribe/cancel?_id=' + User.user._id + '&apiToken=' + User.user.apiToken; var paymentMethod = User.user.purchased.plan.paymentMethod;
if(paymentMethod === 'Amazon Payments'){
paymentMethod = 'amazon';
}else{
paymentMethod = paymentMethod.toLowerCase();
}
window.location.href = '/' + paymentMethod + '/subscribe/cancel?_id=' + User.user._id + '&apiToken=' + User.user.apiToken;
} }
Payments.encodeGift = function(uuid, gift){ Payments.encodeGift = function(uuid, gift){

View File

@@ -1,9 +1,12 @@
var amazonPayments = require('amazon-payments'); var amazonPayments = require('amazon-payments');
var mongoose = require('mongoose');
var moment = require('moment');
var nconf = require('nconf'); var nconf = require('nconf');
var async = require('async'); var async = require('async');
var User = require('mongoose').model('User'); var User = require('mongoose').model('User');
var shared = require('../../../../common'); var shared = require('../../../../common');
var payments = require('./index'); var payments = require('./index');
var cc = require('coupon-code');
var isProd = nconf.get("NODE_ENV") === 'production'; var isProd = nconf.get("NODE_ENV") === 'production';
var amzPayment = amazonPayments.connect({ var amzPayment = amazonPayments.connect({
@@ -27,24 +30,49 @@ exports.verifyAccessToken = function(req, res, next){
}; };
exports.checkout = function(req, res, next){ exports.checkout = function(req, res, next){
if(!req.body || !req.body['orderReferenceId']){ if(!req.body || !req.body['billingAgreementId']){
return res.json(400, {err: 'Order Reference Id not supplied.'}); return res.json(400, {err: 'Billing Agreement Id not supplied.'});
} }
var gift = req.body.gift; var gift = req.body.gift;
var user = res.locals.user; var user = res.locals.user;
var orderReferenceId = req.body.orderReferenceId; var billingAgreementId = req.body.billingAgreementId;
var orderReferenceId;
var amount = 5;
if(gift){
if(gift.type === 'gems'){
amount = gift.gems.amount/4;
}else if(gift.type === 'subscription'){
amount = shared.content.subscriptionBlocks[gift.subscription.key].price;
}
}
async.series({ async.series({
createOrderReferenceForId: function(cb){
amzPayment.offAmazonPayments.createOrderReferenceForId({
Id: billingAgreementId,
IdType: 'BillingAgreement'
}, function(err, response){
if(err) return cb(err);
if(!response.OrderReferenceDetails || !response.OrderReferenceDetails.AmazonOrderReferenceId){
return cb('Missing attributes in Amazon response.');
}
orderReferenceId = response.OrderReferenceDetails.AmazonOrderReferenceId;
return cb();
});
},
setOrderReferenceDetails: function(cb){ setOrderReferenceDetails: function(cb){
amzPayment.offAmazonPayments.setOrderReferenceDetails({ amzPayment.offAmazonPayments.setOrderReferenceDetails({
AmazonOrderReferenceId: orderReferenceId, AmazonOrderReferenceId: orderReferenceId,
OrderReferenceAttributes: { OrderReferenceAttributes: {
OrderTotal: { OrderTotal: {
CurrencyCode: 'USD', CurrencyCode: 'USD',
Amount: gift ? (gift.gems.amount/4) : 5 Amount: amount
}, },
SellerNote: 'HabitRPG Gems', SellerNote: 'HabitRPG Payment',
SellerOrderAttributes: { SellerOrderAttributes: {
SellerOrderId: shared.uuid(), SellerOrderId: shared.uuid(),
StoreName: 'HabitRPG' StoreName: 'HabitRPG'
@@ -65,9 +93,9 @@ exports.checkout = function(req, res, next){
AuthorizationReferenceId: shared.uuid().substring(0, 32), AuthorizationReferenceId: shared.uuid().substring(0, 32),
AuthorizationAmount: { AuthorizationAmount: {
CurrencyCode: 'USD', CurrencyCode: 'USD',
Amount: gift ? (gift.gems.amount/4) : 5 Amount: amount
}, },
SellerAuthorizationNote: 'HabitRPG Donation', SellerAuthorizationNote: 'HabitRPG Payment',
TransactionTimeout: 0, TransactionTimeout: 0,
CaptureNow: true CaptureNow: true
}, cb); }, cb);
@@ -87,12 +115,13 @@ exports.checkout = function(req, res, next){
var method = 'buyGems'; var method = 'buyGems';
if (gift){ if (gift){
if (gift.type == 'subscription') method = 'createSubscription';
gift.member = member; gift.member = member;
data.gift = gift; data.gift = gift;
data.paymentMethod = 'Gift'; data.paymentMethod = 'Gift';
} }
payments.buyGems(data, cb2); payments[method](data, cb2);
} }
], cb); ], cb);
} }
@@ -104,8 +133,112 @@ exports.checkout = function(req, res, next){
}; };
exports.setupSubscription = function(req, res, next){ exports.subscribe = function(req, res, next){
if(!req.body || !req.body['orderReferenceId']){ if(!req.body || !req.body['billingAgreementId']){
return res.json(400, {err: 'Billing Agreement Id not supplied.'}); return res.json(400, {err: 'Billing Agreement Id not supplied.'});
} }
var billingAgreementId = req.body.billingAgreementId
var sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false;
var coupon = req.body.coupon;
var user = res.locals.user;
if(!sub){
return res.json(400, {err: 'Subscription plan not found.'});
}
async.series({
applyDiscount: function(cb){
if (!sub.discount) return cb();
if (!coupon) return cb(new Error('Please provide a coupon code for this plan.'));
mongoose.model('Coupon').findOne({_id:cc.validate(coupon), event:sub.key}, function(err, coupon){
if(err) return cb(err);
if(!coupon) return cb(new Error('Coupon code not found.'));
cb()
});
},
setBillingAgreementDetails: function(cb){
amzPayment.offAmazonPayments.setBillingAgreementDetails({
AmazonBillingAgreementId: billingAgreementId,
BillingAgreementAttributes: {
SellerNote: 'HabitRPG Subscription',
SellerBillingAgreementAttributes: {
SellerBillingAgreementId: shared.uuid(),
StoreName: 'HabitRPG',
CustomInformation: 'HabitRPG Subscription'
}
}
}, cb);
},
confirmBillingAgreement: function(cb){
amzPayment.offAmazonPayments.confirmBillingAgreement({
AmazonBillingAgreementId: billingAgreementId
}, cb);
},
authorizeOnBillingAgreeement: function(cb){
amzPayment.offAmazonPayments.authorizeOnBillingAgreement({
AmazonBillingAgreementId: billingAgreementId,
AuthorizationReferenceId: shared.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: 'USD',
Amount: sub.price
},
SellerAuthorizationNote: 'HabitRPG Subscription Payment',
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: 'HabitRPG Subscription Payment',
SellerOrderAttributes: {
SellerOrderId: shared.uuid(),
StoreName: 'HabitRPG'
}
}, cb);
},
createSubscription: function(cb){
payments.createSubscription({
user: user,
customerId: billingAgreementId,
paymentMethod: 'Amazon Payments',
sub: sub
}, cb);
}
}, function(err, results){
if(err) return next(err);
res.send(200);
});
};
exports.subscribeCancel = function(req, res, next){
var user = res.locals.user;
if (!user.purchased.plan.customerId)
return res.json(401, {err: "User does not have a plan subscription"});
var billingAgreementId = user.purchased.plan.customerId;
async.series({
closeBillingAgreement: function(cb){
amzPayment.offAmazonPayments.closeBillingAgreement({
AmazonBillingAgreementId: billingAgreementId
}, cb);
},
cancelSubscription: function(cb){
var data = {
user: user,
// Date of next bill, dateUpdated can be used because it's only updated when the user is billed
nextBill: moment(user.purchased.plan.dateUpdated).add({days: 30}),
paymentMethod: 'Amazon Payments'
};
payments.cancelSubscription(data, cb);
}
}, function(err, results){
if (err) return next(err); // don't json this, let toString() handle errors
res.redirect('/');
user = null;
});
}; };

View File

@@ -172,6 +172,8 @@ exports.paypalIPN = paypal.ipn;
exports.amazonVerifyAccessToken = amazon.verifyAccessToken; exports.amazonVerifyAccessToken = amazon.verifyAccessToken;
exports.amazonCheckout = amazon.checkout; exports.amazonCheckout = amazon.checkout;
exports.amazonSubscribe = amazon.subscribe;
exports.amazonSubscribeCancel = amazon.subscribeCancel;
exports.iapAndroidVerify = iap.androidVerify; exports.iapAndroidVerify = iap.androidVerify;
exports.iapIosVerify = iap.iosVerify; exports.iapIosVerify = iap.iosVerify;

View File

@@ -101,8 +101,8 @@ var UserSchema = new Schema({
mobileChat: Boolean, mobileChat: Boolean,
plan: { plan: {
planId: String, planId: String,
paymentMethod: String, //enum: ['Paypal','Stripe', 'Gift', '']} paymentMethod: String, //enum: ['Paypal','Stripe', 'Gift', 'Amazon Payments', '']}
customerId: String, customerId: String, // Billing Agreement Id in case of Amazon Payments
dateCreated: Date, dateCreated: Date,
dateTerminated: Date, dateTerminated: Date,
dateUpdated: Date, dateUpdated: Date,

View File

@@ -19,6 +19,8 @@ router.get("/stripe/subscribe/cancel", auth.authWithUrl, i18n.getUserLanguage, p
router.post('/amazon/verifyAccessToken', auth.auth, i18n.getUserLanguage, payments.amazonVerifyAccessToken); router.post('/amazon/verifyAccessToken', auth.auth, i18n.getUserLanguage, payments.amazonVerifyAccessToken);
router.post('/amazon/checkout', auth.auth, i18n.getUserLanguage, payments.amazonCheckout); router.post('/amazon/checkout', auth.auth, i18n.getUserLanguage, payments.amazonCheckout);
router.post('/amazon/subscribe', auth.auth, i18n.getUserLanguage, payments.amazonSubscribe);
router.get('/amazon/subscribe/cancel', auth.authWithUrl, i18n.getUserLanguage, payments.amazonSubscribeCancel);
router.post("/iap/android/verify", auth.authWithUrl, /*i18n.getUserLanguage, */payments.iapAndroidVerify); router.post("/iap/android/verify", auth.authWithUrl, /*i18n.getUserLanguage, */payments.iapAndroidVerify);
router.post("/iap/ios/verify", auth.auth, /*i18n.getUserLanguage, */ payments.iapIosVerify); router.post("/iap/ios/verify", auth.auth, /*i18n.getUserLanguage, */ payments.iapIosVerify);

View File

@@ -180,8 +180,6 @@ script(type='text/ng-template', id='partials/options.settings.promo.html')
button.btn.btn-primary(type='submit') Generate button.btn.btn-primary(type='submit') Generate
a.btn.btn-default(href='/api/v2/coupons?_id={{user._id}}&apiToken={{user.apiToken}}') Get Codes a.btn.btn-default(href='/api/v2/coupons?_id={{user._id}}&apiToken={{user.apiToken}}') Get Codes
script(type='text/ng-template', id='partials/options.settings.api.html') script(type='text/ng-template', id='partials/options.settings.api.html')
.container-fluid .container-fluid
.row .row
@@ -339,7 +337,7 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
h3(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')= env.t("resubscribe") h3(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')= env.t("resubscribe")
a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key')= env.t('card') a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key')= env.t('card')
a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key') PayPal a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key') PayPal
.btn.btn-default(ng-click="Payments.amazonPayments.init('subscription')") Amazon Payments a.btn.btn-default(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})") Amazon Payments
div(ng-if='user.purchased.plan.customerId') div(ng-if='user.purchased.plan.customerId')
.btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard') .btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard')
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub') .btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')

View File

@@ -22,7 +22,7 @@ script(id='modals/buyGems.html', type='text/ng-template')
small.muted=env.t('paymentMethods') small.muted=env.t('paymentMethods')
.btn.btn-primary(ng-click='Payments.showStripe({})')=env.t('card') .btn.btn-primary(ng-click='Payments.showStripe({})')=env.t('card')
a.btn.btn-warning(href='/paypal/checkout?_id={{user._id}}&apiToken={{user.apiToken}}') PayPal a.btn.btn-warning(href='/paypal/checkout?_id={{user._id}}&apiToken={{user.apiToken}}') PayPal
.btn.btn-default(ng-click="Payments.amazonPayments.init('donation')") Amazon Payments //.btn.btn-default(ng-click="Payments.amazonPayments.init({type: 'donation'})") Amazon Payments
div(ng-include="'partials/options.settings.subscription.html'") div(ng-include="'partials/options.settings.subscription.html'")

View File

@@ -97,7 +97,7 @@ script(type='text/ng-template', id='modals/send-gift.html')
button.btn.btn-primary(ng-show=fromBal, ng-click='sendGift(profile._id, gift)')=env.t("send") button.btn.btn-primary(ng-show=fromBal, ng-click='sendGift(profile._id, gift)')=env.t("send")
a.btn.btn-primary(ng-hide=fromBal, ng-click='Payments.showStripe({gift:gift, uuid:profile._id})')=env.t('card') a.btn.btn-primary(ng-hide=fromBal, ng-click='Payments.showStripe({gift:gift, uuid:profile._id})')=env.t('card')
a.btn.btn-warning(ng-hide=fromBal, href='/paypal/checkout?_id={{::user._id}}&apiToken={{::user.apiToken}}&gift={{Payments.encodeGift(profile._id, gift)}}') PayPal a.btn.btn-warning(ng-hide=fromBal, href='/paypal/checkout?_id={{::user._id}}&apiToken={{::user.apiToken}}&gift={{Payments.encodeGift(profile._id, gift)}}') PayPal
.btn.btn-default(ng-hide=fromBal, ng-click="Payments.amazonPayments.init(gift.type == 'gems' ? 'donation' : 'subscription', gift, profile._id)") Amazon Payments .btn.btn-default(ng-hide="true", ng-click="Payments.amazonPayments.init({type: gift.type == 'gems' ? 'donation' : 'subscription', gift: gift, giftedTo: profile._id})") Amazon Payments
button.btn.btn-default(ng-click='$close()')=env.t('cancel') button.btn.btn-default(ng-click='$close()')=env.t('cancel')
script(type='text/ng-template', id='modals/abuse-flag.html') script(type='text/ng-template', id='modals/abuse-flag.html')