Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Braintree discounts and refunds #1265

Merged
merged 31 commits into from
Aug 16, 2016
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b4f50c1
add Braintree Payment error to en.json
kieckhafer Aug 8, 2016
49b2634
enable discounts for Braintree payments
kieckhafer Aug 8, 2016
927ffb9
removed else statement after an if containing a return
kieckhafer Aug 8, 2016
a348662
added testing for Braintree refunds
kieckhafer Aug 10, 2016
e9bafcf
remove no longer needed commented code
kieckhafer Aug 11, 2016
e2cd723
remove no longer needed commented code
kieckhafer Aug 11, 2016
993b608
added field to expected response for testing
kieckhafer Aug 11, 2016
43d61c0
moved braintree/refund/list methods into BraintreeApi wrapper
kieckhafer Aug 11, 2016
381a6a6
Merge branch 'development' into ek-refunds-braintree
kieckhafer Aug 11, 2016
cefb012
move braintree methods into new file
kieckhafer Aug 12, 2016
edd6deb
reconfigure all braintree payment code to no longer use ValidateMethod
kieckhafer Aug 12, 2016
af86321
fixed lint issues
kieckhafer Aug 12, 2016
e1475b6
removed code used to skip over 24 hour braintree delay (for testing)
kieckhafer Aug 12, 2016
5821c95
Rename braintreeapi.js to braintreeApi.js
kieckhafer Aug 12, 2016
4099285
display absolute number of adjustedTotal
kieckhafer Aug 12, 2016
7a141f3
updated schema to match supported payment methods
kieckhafer Aug 12, 2016
b81b61c
min -> max
kieckhafer Aug 12, 2016
753ab11
removed comments
kieckhafer Aug 12, 2016
8dec94f
test testing
kieckhafer Aug 12, 2016
8eda45b
update braintree test
kieckhafer Aug 15, 2016
e2127a3
update exports of braintreeApi functions
kieckhafer Aug 15, 2016
a621be5
Merge branch 'development' into ek-refunds-braintree
kieckhafer Aug 15, 2016
d75a4c9
fixed callback error when action has no callback
kieckhafer Aug 15, 2016
13572e8
Updated error message to make more sense to a human user
kieckhafer Aug 15, 2016
c9042bf
linter fixes
kieckhafer Aug 15, 2016
71f3625
Merge branch 'development' into ek-refunds-braintree
Aug 15, 2016
949e32b
updated 'Logger.info' to Logger.debug
kieckhafer Aug 15, 2016
a5c8e5d
removed unused test
kieckhafer Aug 15, 2016
12748c0
removed commented callback
kieckhafer Aug 15, 2016
fd364af
Merge branch 'development' into ek-refunds-braintree
Aug 16, 2016
a936672
don't log full order details on transaction error
Aug 16, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ Object.assign(Alerts, {
...options
}).then((isConfirm) => {
if (isConfirm === true) {
callback(isConfirm);
if (callback) {
callback(isConfirm);
}
// callback();
}
});
},
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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What CC uses 19 digits? Just curious. Is this something that should be changed across all payment methods?

Copy link
Member Author

@kieckhafer kieckhafer Aug 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Maestro debit card was the most popular one I could find that used 19 digits, but I made this change based on Error Code 81716.

Yes, I think It probably is something that needs to be changed for the other payment methods, if nothing else because Amex is only 15 digits, so we at least need to make the minimum less. I will update these in the other PR's currently open for the other providers.

Copy link
Member Author

@kieckhafer kieckhafer Aug 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maestro (min 12, max 19) is accepted by PayPal, Auth.net, and Braintree in SOME countries, not all countries. Do we want to use 12/19 to meet this standard where it's accepted, or do we use 13/16 (Visa can be 13 OR 16) and let those using Maestro update it themselves?

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