// 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?" + "
" + "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?" + "
" + resp.errors[0], "Test failed");
}
} catch (ex) {
global.apis.ui.hideProgressSpinner();
global.apis.alert("Did you remember to save settings before testing?" + "
" + 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;
}
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();
try {
// 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: global.apis.i18n.moneyToFixed(amount / 100.0) * 1.0,
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 = "";
while (paymentID == "") {
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") {
paymentID = resultBody.id;
global.apis.util.http.webhook.ack(pollResults[i].id);
} else if (resultBody.type == "terminalCancel") {
if (resultBody.data.deviceCode == code && resultBody.data.invoiceNumber == receiptID) {
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.");
global.apis.alert("The card payment was canceled.", "Payment canceled");
return false;
}
console.log("Got transaction ID from Helcim webhook:", paymentID);
purchaseResp = await apiRequest(`card-transactions/${paymentID}`, null, "GET");
console.log("purchaseResp", purchaseResp);
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");
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
)
);
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");
}
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");
}
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.
},
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\