-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix Braintree discounts and refunds (#1265)
* 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
1 parent
cb9e65c
commit f7c56a5
Showing
10 changed files
with
469 additions
and
244 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
244 changes: 5 additions & 239 deletions
244
imports/plugins/included/braintree/server/methods/braintree.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; | ||
|
Oops, something went wrong.