Skip to content

Commit

Permalink
Fix Braintree discounts and refunds (#1265)
Browse files Browse the repository at this point in the history
* add Braintree Payment error to en.json

* enable discounts for Braintree payments

Braintree was not capturing discounts, instead using the amount in the
original authorization for the capturing. This update changes the
amount to the paymentMethod.amount, which includes discounts.

* removed else statement after an if containing a return

Since the return inside the IF statement will effectively kill the
process if it’s hit, there is no need for the else.

* added testing for Braintree refunds

This code still needs some love, just wanted to get it up for others to
take a look at.

* remove no longer needed commented code

* remove no longer needed commented code

* added field to expected response for testing

* moved braintree/refund/list methods into BraintreeApi wrapper

Creating a wrapper for Braintree in order to more easily perform testing

* move braintree methods into new file

* reconfigure all braintree payment code to no longer use ValidateMethod

* fixed lint issues

* removed code used to skip over 24 hour braintree delay (for testing)

* Rename braintreeapi.js to braintreeApi.js

* display absolute number of adjustedTotal

due to various instances of rounding numbers for display purposes, the
adjustedTotal would sometimes display -0.00, as the adjusted total was
technically -.00000000000000000000001, even though we display 0.00 when
we round it. This update just shows the absolute number, as this is
simple a display number and does not have any affect on what is being
sent to and from the payment provider.

* updated schema to match supported payment methods

The current Schema had a 16 number minimum for credit cards, however
braintree supports cards which have number lengths ranging from 12-19

* min -> max

* removed comments

* test testing

* update braintree test

* update exports of braintreeApi functions

* fixed callback error when action has no callback

* Updated error message to make more sense to a human user

* linter fixes

* updated 'Logger.info' to Logger.debug

* removed unused test

* removed commented callback

* don't log full order details on transaction error
  • Loading branch information
kieckhafer authored and Aaron Judd committed Aug 16, 2016
1 parent cb9e65c commit f7c56a5
Show file tree
Hide file tree
Showing 10 changed files with 469 additions and 244 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ Object.assign(Alerts, {
...options
}).then((isConfirm) => {
if (isConfirm === true) {
callback(isConfirm);
if (callback) {
callback(isConfirm);
}
}
});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,8 @@ Template.coreOrderShippingInvoice.helpers({
_.each(refunds, function (item) {
refundTotal += parseFloat(item.amount);
});
return paymentMethod.amount - refundTotal;

return Math.abs(paymentMethod.amount - refundTotal);
},

refundSubmitDisabled() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ handleBraintreeSubmitError = function (error) {
if (serverError) {
return paymentAlert("Server Error " + serverError);
} else if (error) {
return paymentAlert("Oops " + error);
return paymentAlert("Oops! Credit card is invalid. Please check your information and try again.");
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export const BraintreePayment = new SimpleSchema({
},
cardNumber: {
type: String,
min: 16,
min: 12,
max: 19,
label: "Card number"
},
expireMonth: {
Expand Down
244 changes: 5 additions & 239 deletions imports/plugins/included/braintree/server/methods/braintree.js
Original file line number Diff line number Diff line change
@@ -1,243 +1,9 @@
import moment from "moment";
import * as BraintreeMethods from "./braintreeMethods";
import { Meteor } from "meteor/meteor";
import Future from "fibers/future";
import Braintree from "braintree";
import { Reaction, Logger } from "/server/api";
import { Packages } from "/lib/collections";
import { PaymentMethod } from "/lib/collections/schemas";

function getSettings(settings, ref, valueName) {
if (settings !== null) {
return settings[valueName];
} else if (ref !== null) {
return ref[valueName];
}
return undefined;
}


function getAccountOptions() {
let environment;
let settings = Packages.findOne({
name: "reaction-braintree",
shopId: Reaction.getShopId(),
enabled: true
}).settings;
if (typeof settings !== "undefined" && settings !== null ? settings.mode : undefined === true) {
environment = "production";
} else {
environment = "sandbox";
}

let ref = Meteor.settings.braintree;
let options = {
environment: environment,
merchantId: getSettings(settings, ref, "merchant_id"),
publicKey: getSettings(settings, ref, "public_key"),
privateKey: getSettings(settings, ref, "private_key")
};
if (!options.merchantId) {
throw new Meteor.Error("invalid-credentials", "Invalid Braintree Credentials");
}
return options;
}


function getGateway() {
let accountOptions = getAccountOptions();
if (accountOptions.environment === "production") {
accountOptions.environment = Braintree.Environment.Production;
} else {
accountOptions.environment = Braintree.Environment.Sandbox;
}
let gateway = Braintree.connect(accountOptions);
return gateway;
}

function getPaymentObj() {
return {
amount: "",
options: {submitForSettlement: true}
};
}

function parseCardData(data) {
return {
cardholderName: data.name,
number: data.number,
expirationMonth: data.expirationMonth,
expirationYear: data.expirationYear,
cvv: data.cvv
};
}

Meteor.methods({
/**
* braintreeSubmit
* Authorize, or authorize and capture payments from Brinatree
* https://developers.braintreepayments.com/reference/request/transaction/sale/node
* @param {String} transactionType - either authorize or capture
* @param {Object} cardData - Object containing everything about the Credit card to be submitted
* @param {Object} paymentData - Object containing everything about the transaction to be settled
* @return {Object} results - Object containing the results of the transaction
*/
"braintreeSubmit": function (transactionType, cardData, paymentData) {
check(transactionType, String);
check(cardData, {
name: String,
number: String,
expirationMonth: String,
expirationYear: String,
cvv2: String,
type: String
});
check(paymentData, {
total: String,
currency: String
});
let gateway = getGateway();
let paymentObj = getPaymentObj();
if (transactionType === "authorize") {
paymentObj.options.submitForSettlement = false;
}
paymentObj.creditCard = parseCardData(cardData);
paymentObj.amount = paymentData.total;
let fut = new Future();
gateway.transaction.sale(paymentObj, Meteor.bindEnvironment(function (error, result) {
if (error) {
fut.return({
saved: false,
error: error
});
} else if (!result.success) {
fut.return({
saved: false,
response: result
});
} else {
fut.return({
saved: true,
response: result
});
}
}, function (error) {
Reaction.Events.warn(error);
}));
return fut.wait();
},


/**
* braintree/payment/capture
* Capture payments from Braintree
* https://developers.braintreepayments.com/reference/request/transaction/submit-for-settlement/node
* @param {Object} paymentMethod - Object containing everything about the transaction to be settled
* @return {Object} results - Object containing the results of the transaction
*/
"braintree/payment/capture": function (paymentMethod) {
check(paymentMethod, PaymentMethod);
let transactionId = paymentMethod.transactions[0].transaction.id;
let amount = paymentMethod.transactions[0].transaction.amount;
let gateway = getGateway();
const fut = new Future();
this.unblock();
gateway.transaction.submitForSettlement(transactionId, amount, Meteor.bindEnvironment(function (error, result) {
if (error) {
fut.return({
saved: false,
error: error
});
} else {
fut.return({
saved: true,
response: result
});
}
}, function (e) {
Logger.warn(e);
}));
return fut.wait();
},
/**
* braintree/refund/create
* Refund BrainTree payment
* https://developers.braintreepayments.com/reference/request/transaction/refund/node
* @param {Object} paymentMethod - Object containing everything about the transaction to be settled
* @param {Number} amount - Amount to be refunded if not the entire amount
* @return {Object} results - Object containing the results of the transaction
*/
"braintree/refund/create": function (paymentMethod, amount) {
check(paymentMethod, PaymentMethod);
check(amount, Number);
let transactionId = paymentMethod.transactions[0].transaction.id;
let gateway = getGateway();
const fut = new Future();
gateway.transaction.refund(transactionId, amount, Meteor.bindEnvironment(function (error, result) {
if (error) {
fut.return({
saved: false,
error: error
});
} else if (!result.success) {
if (result.errors.errorCollections.transaction.validationErrors.base[0].code === "91506") {
fut.return({
saved: false,
error: "Cannot refund transaction until it\'s settled. Please try again later"
});
} else {
fut.return({
saved: false,
error: result.message
});
}
} else {
fut.return({
saved: true,
response: result
});
}
}, function (e) {
Logger.fatal(e);
}));
return fut.wait();
},

/**
* braintree/refund/list
* List all refunds for a transaction
* https://developers.braintreepayments.com/reference/request/transaction/find/node
* @param {Object} paymentMethod - Object containing everything about the transaction to be settled
* @return {Array} results - An array of refund objects for display in admin
*/
"braintree/refund/list": function (paymentMethod) {
check(paymentMethod, Object);
let transactionId = paymentMethod.transactionId;
let gateway = getGateway();
this.unblock();
let braintreeFind = Meteor.wrapAsync(gateway.transaction.find, gateway.transaction);
let findResults = braintreeFind(transactionId);
let result = [];
if (findResults.refundIds.length > 0) {
for (let refund of findResults.refundIds) {
let refundDetails = getRefundDetails(refund);
result.push({
type: "refund",
amount: parseFloat(refundDetails.amount),
created: moment(refundDetails.createdAt).unix() * 1000,
currency: refundDetails.currencyIsoCode,
raw: refundDetails
});
}
}
return result;
}
"braintreeSubmit": BraintreeMethods.paymentSubmit,
"braintree/payment/capture": BraintreeMethods.paymentCapture,
"braintree/refund/create": BraintreeMethods.createRefund,
"braintree/refund/list": BraintreeMethods.listRefunds
});

getRefundDetails = function (refundId) {
check(refundId, String);
let gateway = getGateway();
let braintreeFind = Meteor.wrapAsync(gateway.transaction.find, gateway.transaction);
let findResults = braintreeFind(refundId);
return findResults;
};

Loading

0 comments on commit f7c56a5

Please sign in to comment.