353 lines
16 KiB
JavaScript
353 lines
16 KiB
JavaScript
// Adds Helcim as a payment processor option.
|
|
|
|
const PLUGINID = "net.postalportal.helcimplugin";
|
|
|
|
const URL_BASE = "https://api.helcim.com/v2";
|
|
const PARTNER_TOKEN = "f29104118c0d65"; // f29104118c0d65 is a test partner token
|
|
const WEBHOOK_SOURCE = "hcpaymentstatus";
|
|
|
|
async function apiRequest(url, data = {}, method = "POST", responseType = "json") {
|
|
var apikey = global.apis.settings.get(PLUGINID + ".apikey", "");
|
|
console.log(`${url}:`, data);
|
|
var headers = {
|
|
"accept": `application/json`,
|
|
"api-token": `${apikey}`
|
|
};
|
|
if (PARTNER_TOKEN != "") {
|
|
headers["partner-token"] = PARTNER_TOKEN;
|
|
}
|
|
if (method == "POST") {
|
|
headers["content-type"] = "application/json";
|
|
}
|
|
|
|
return await global.apis.util.http.post(`${URL_BASE}/${url}`, data, responseType, headers, method, true);
|
|
}
|
|
|
|
|
|
async function pingDevice() {
|
|
global.apis.ui.showProgressSpinner("Pinging Helcim device...");
|
|
var deviceCode = global.apis.settings.get(PLUGINID + ".devicecode", "");
|
|
if (deviceCode == "") {
|
|
global.apis.alert("Did you remember to save settings before testing?" + "<br /><br />" + "No device code saved. Enter the device code from the reader screen.", "Test failed");
|
|
return;
|
|
}
|
|
try {
|
|
var resp = await apiRequest(`devices/${deviceCode}/ping`, {}, "GET", "text");
|
|
global.apis.ui.hideProgressSpinner();
|
|
global.apis.alert("The card reader is set up correctly.", "Success!");
|
|
if (typeof resp.errors != "undefined" && resp.errors.length > 0) {
|
|
global.apis.alert("Did you remember to save settings before testing?" + "<br /><br />" + resp.errors[0], "Test failed");
|
|
}
|
|
} catch (ex) {
|
|
global.apis.ui.hideProgressSpinner();
|
|
global.apis.alert("Did you remember to save settings before testing?" + "<br /><br />" + ex.message, "Test failed");
|
|
}
|
|
}
|
|
|
|
function getCardBrand(cardType) {
|
|
switch (cardType) {
|
|
case "VI":
|
|
cardType = "Visa";
|
|
break;
|
|
case "MC":
|
|
cardType = "MasterCard";
|
|
break;
|
|
case "AX":
|
|
cardType = "American Express";
|
|
break;
|
|
case "DI":
|
|
cardType = "Discover";
|
|
break;
|
|
case "DCI":
|
|
cardType = "Diners Club";
|
|
break;
|
|
case "JCB":
|
|
cardType = "JCB";
|
|
break;
|
|
case "UP":
|
|
cardType = "China Union Pay";
|
|
break;
|
|
case "MR":
|
|
cardType = "Maestro";
|
|
break;
|
|
case "AF":
|
|
cardType = "AFFN";
|
|
break;
|
|
case "AO":
|
|
cardType = "Alaska Option";
|
|
break;
|
|
case "CU":
|
|
cardType = "Credit Union 24";
|
|
break;
|
|
case "EB":
|
|
cardType = "EBT Network";
|
|
break;
|
|
case "EX":
|
|
cardType = "Accel";
|
|
break;
|
|
case "IL":
|
|
cardType = "Interlink";
|
|
break;
|
|
case "NT":
|
|
cardType = "Nets";
|
|
break;
|
|
case "NY":
|
|
cardType = "NYCE";
|
|
break;
|
|
case "PS":
|
|
cardType = "Pulse";
|
|
break;
|
|
case "ST":
|
|
cardType = "Star";
|
|
break;
|
|
case "SZ":
|
|
cardType = "Shazam";
|
|
break;
|
|
case "AT":
|
|
cardType = "ATH";
|
|
break;
|
|
case "IN":
|
|
cardType = "Interac";
|
|
break;
|
|
case "DB":
|
|
cardType = "Debit";
|
|
break;
|
|
}
|
|
return cardType;
|
|
}
|
|
|
|
var currentReceiptID = null;
|
|
|
|
var checkoutCancelled = false;
|
|
|
|
exports.init = function () {
|
|
global.apis.pos.registerCardProcessor({
|
|
name: "Helcim",
|
|
init: async function () {
|
|
// This function runs once after starting PostalPoint
|
|
// and before any other card processor functions are called.
|
|
console.info("Hello from Helcim plugin!");
|
|
},
|
|
checkout: async function ( {amount, capture = true}) {
|
|
// amount is an integer number of pennies.
|
|
|
|
// If an error is encountered during processing,
|
|
// display an error message in a dialog and return boolean false.
|
|
// If this function returns anything except false or undefined, and doesn't throw an error,
|
|
// it is assumed the payment was successful.
|
|
const code = global.apis.settings.get(PLUGINID + ".devicecode", "");
|
|
const receiptID = global.apis.pos.getReceiptID();
|
|
checkoutCancelled = false;
|
|
currentReceiptID = receiptID;
|
|
try {
|
|
var transactionAmount = global.apis.i18n.moneyToFixed(amount / 100.0) * 1.0;
|
|
// authorize, capture, add a ReceiptPayment to the receipt, and return boolean true.
|
|
global.apis.pos.addOnscreenPaymentLog("Getting card payment..."); // Add a line to the onscreen card processing status log
|
|
var purchaseResp = await apiRequest(`devices/${code}/payment/purchase`, {
|
|
currency: global.apis.i18n.currency().toUpperCase(),
|
|
transactionAmount: transactionAmount,
|
|
invoiceNumber: receiptID
|
|
}, "POST", "text");
|
|
if (purchaseResp.length > 0) {
|
|
var json = JSON.parse(purchaseResp);
|
|
if (typeof json.errors != "undefined" && json.errors.length > 0) {
|
|
global.apis.pos.addOnscreenPaymentLog("Helcim card reader error: " + json.errors[0]);
|
|
global.apis.alert("Could not start card payment: " + json.errors[0], "Card Reader Error");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var paymentID = "";
|
|
var purchaseResp;
|
|
while (paymentID == "") {
|
|
if (checkoutCancelled) {
|
|
checkoutCancelled = false;
|
|
global.apis.pos.addOnscreenPaymentLog("Checkout cancelled. Warning: It's still possible for the payment to succeed on the reader but not be recognized in PostalPoint; cancel it on the reader too.");
|
|
return false;
|
|
}
|
|
await global.apis.util.delay(1000); // Wait a second
|
|
try {
|
|
var pollResults = await global.apis.util.http.webhook.poll(WEBHOOK_SOURCE);
|
|
for (var i = 0; i < pollResults.length; i++) {
|
|
var resultBody = JSON.parse(pollResults[i].body);
|
|
if (resultBody.type == "cardTransaction") {
|
|
purchaseResp = await apiRequest(`card-transactions/${resultBody.id}`, null, "GET");
|
|
global.apis.util.http.webhook.ack(pollResults[i].id);
|
|
if (purchaseResp.invoiceNumber == currentReceiptID) {
|
|
console.log("Got transaction ID from Helcim webhook:", paymentID);
|
|
console.log("purchaseResp", purchaseResp);
|
|
paymentID = resultBody.id;
|
|
}
|
|
} else if (resultBody.type == "terminalCancel") {
|
|
if (resultBody.data.deviceCode == code && resultBody.data.invoiceNumber == currentReceiptID && resultBody.data.transactionAmount == transactionAmount) {
|
|
paymentID = "CANCEL";
|
|
global.apis.util.http.webhook.ack(pollResults[i].id);
|
|
}
|
|
}
|
|
}
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
|
|
if (paymentID == "CANCEL") {
|
|
global.apis.pos.addOnscreenPaymentLog("Helcim payment not completed: cancelled on the card terminal.");
|
|
global.apis.alert("The card payment was cancelled on the card terminal.", "Payment cancelled");
|
|
currentReceiptID = null;
|
|
return false;
|
|
}
|
|
|
|
|
|
if (typeof purchaseResp?.errors != "undefined" && purchaseResp.errors.length > 0) {
|
|
global.apis.pos.addOnscreenPaymentLog("Helcim card payment error: " + purchaseResp.errors[0]);
|
|
global.apis.alert("Could not finish card payment: " + purchaseResp.errors[0], "Card Payment Error");
|
|
currentReceiptID = null;
|
|
return false;
|
|
}
|
|
|
|
if (purchaseResp.status == "APPROVED") {
|
|
global.apis.pos.addOnscreenPaymentLog("Payment approved!");
|
|
if (purchaseResp.type == "purchase") {
|
|
pstring = `${purchaseResp.cardHolderName ?? ""}\n${getCardBrand(purchaseResp.cardType)}\nx${purchaseResp.cardNumber.slice(-4)}\nApproval: ${purchaseResp.approvalCode}`;
|
|
global.apis.pos.addReceiptPayment(
|
|
new global.apis.pos.ReceiptPayment(
|
|
purchaseResp.amount,
|
|
"card",
|
|
pstring
|
|
)
|
|
);
|
|
currentReceiptID = null;
|
|
return true;
|
|
}
|
|
} else if (purchaseResp.status == "DECLINED") {
|
|
global.apis.pos.addOnscreenPaymentLog("Card payment declined.");
|
|
if (global.apis.kiosk.isKiosk()) {
|
|
global.apis.alert("Your card was declined.", "Card Error");
|
|
} else {
|
|
global.apis.alert("The customer's card was declined.", "Card Error");
|
|
}
|
|
currentReceiptID = null;
|
|
return false;
|
|
}
|
|
|
|
if (global.apis.kiosk.isKiosk()) {
|
|
global.apis.pos.addOnscreenPaymentLog("Unknown card payment error.");
|
|
global.apis.alert("Your card payment did not go through for an unknown reason.", "Card Error");
|
|
} else {
|
|
global.apis.pos.addOnscreenPaymentLog("Unknown card payment error.");
|
|
global.apis.alert("The transaction didn't complete correctly (Helcim card plugin reached an undefined state)", "Card Error");
|
|
}
|
|
|
|
currentReceiptID = null;
|
|
return false;
|
|
} catch (ex) {
|
|
global.apis.pos.addOnscreenPaymentLog(`Error: ${ex.message}`);
|
|
if (global.apis.kiosk.isKiosk()) {
|
|
// This message will be shown to an end-user/customer, not a cashier/employee
|
|
global.apis.alert("Your card payment was not successful due to a system error.", "Card Error");
|
|
} else {
|
|
global.apis.alert("The customer's payment was not successful due to a system error.", "Card Error");
|
|
}
|
|
return false;
|
|
}
|
|
;
|
|
},
|
|
cancelCheckout: async function () {
|
|
// The user requested to cancel the payment.
|
|
// Stop the webhook wait loop.
|
|
checkoutCancelled = true;
|
|
},
|
|
finishPayment: async function ( {checkoutResponse}) {
|
|
// Finish a payment that was authorized but not captured because checkout was called with capture = false
|
|
// If payment was already captured and added to the receipt for some reason, just return true.
|
|
return true;
|
|
},
|
|
updateCartDisplay: function (receipt) {
|
|
// no-op
|
|
},
|
|
checkoutSavedMethod: async function ( {customerID, paymentMethodID, amount}) {
|
|
// Same as checkout() except using a payment method already on file.
|
|
// customerID and paymentMethodID are provided by getSavedPaymentMethods below.
|
|
global.apis.pos.addOnscreenPaymentLog("Saved card payments not supported with Helcim yet.");
|
|
throw new Error("Saved card payments not supported with Helcim yet.");
|
|
return false;
|
|
},
|
|
saveCardForOfflineUse: async function ( {statusCallback, customerUUID, name, company, street1, street2, city, state, zip, country, email, phone}) {
|
|
// Use the card reader to capture an in-person card and save it for offline use.
|
|
// Provided details are the customer's info, which might be empty strings except for the customerUUID.
|
|
// Saved card details must be tied to the customerUUID, as that's how saved cards are looked up.
|
|
global.apis.pos.addOnscreenPaymentLog("Saved card payments not supported with Helcim yet.");
|
|
throw new Error("Saved card payments not supported with Helcim yet.");
|
|
return false;
|
|
},
|
|
cancelSaveCardForOfflineUse: function () {
|
|
// Cancel the process running in saveCardForOfflineUse() at the user/cashier's request.
|
|
},
|
|
getSavedPaymentMethods: async function ( {customerUUID}) {
|
|
// Return all saved payment methods tied to the provided customer UUID.
|
|
return [];
|
|
},
|
|
deleteSavedPaymentMethod: async function ( {customerUUID, customerID, paymentMethodID}) {
|
|
// Delete the payment method identified by paymentMethodID and tied to the PostalPoint customerUUID and the card processor customerID.
|
|
// If unable to delete, throw an error and the error message will be displayed to the cashier.
|
|
global.apis.pos.addOnscreenPaymentLog("Saved card payments not supported with Helcim yet.");
|
|
throw new Error("Saved card payments not supported with Helcim yet.");
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
async function webhookSetup() {
|
|
var url = await global.apis.util.http.webhook.geturl(WEBHOOK_SOURCE);
|
|
global.apis.util.clipboard.copy(url);
|
|
global.apis.alert("The webhook URL shown below has been copied to your clipboard. Setup steps: \n\
|
|
<ol><li>Log in to your Helcim account</li><li>Click \"All Tools\"</li><li>Select \"Integrations\"</li>\n\
|
|
<li>Click Webhooks</li><li>Turn the toggle on</li><li>Click into the \"Deliver URL\" box</li>\n\
|
|
<li>Paste the URL (Ctrl-V or right-click and Paste)</li>\n\
|
|
<li>Check all the boxes next to \"Notify app when changes made to\"</li>\n\
|
|
<li>Click \"Save\"</li></ol><br /><br />" + url, "Webhook Setup");
|
|
}
|
|
|
|
// Plugin settings to display.
|
|
exports.config = [
|
|
{
|
|
type: "password",
|
|
key: PLUGINID + ".apikey",
|
|
defaultVal: "",
|
|
label: "API Token",
|
|
placeholder: "",
|
|
text: "To get an API Token, in your Helcim dashboard, click All Tools -> Integrations -> API Access Configurations. Generate a new API Access. Fill in the name (PostalPoint). Under Access Restrictions set General to Read & Write, and set Transaction Processing to Admin. Press Create to get the token."
|
|
},
|
|
{
|
|
type: "text",
|
|
key: PLUGINID + ".devicecode",
|
|
defaultVal: "",
|
|
label: "Device Code",
|
|
placeholder: "123A",
|
|
text: "The short code shown on your Helcim card reader's \"Ready to pair\" screen."
|
|
},
|
|
{
|
|
type: "button",
|
|
label: "Test Connection",
|
|
text: "After saving settings, press this button to test the card reader.",
|
|
onClick: function () {
|
|
pingDevice();
|
|
}
|
|
},
|
|
{
|
|
type: "button",
|
|
label: "Set Up Webhook",
|
|
text: "You must add a webhook in your Helcim account so PostalPoint can receive payment status updates.",
|
|
onClick: function () {
|
|
webhookSetup();
|
|
}
|
|
},
|
|
{
|
|
type: "password",
|
|
key: PLUGINID + ".verifiertoken",
|
|
defaultVal: "",
|
|
label: "Verifier Token",
|
|
placeholder: "",
|
|
text: "For extra security, enter the Verifier Token from the Helcim dashboard, found under Integrations -> Webhooks."
|
|
}
|
|
]; |