// 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\
  1. Log in to your Helcim account
  2. Click \"All Tools\"
  3. Select \"Integrations\"
  4. \n\
  5. Click Webhooks
  6. Turn the toggle on
  7. Click into the \"Deliver URL\" box
  8. \n\
  9. Paste the URL (Ctrl-V or right-click and Paste)
  10. \n\
  11. Check all the boxes next to \"Notify app when changes made to\"
  12. \n\
  13. Click \"Save\"


" + 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." } ];