2026-02-13 14:59:30 -07:00
// 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 ;
}
2026-03-31 17:41:50 -06:00
var currentReceiptID = null ;
var checkoutCancelled = false ;
2026-02-13 14:59:30 -07:00
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 ( ) ;
2026-03-31 17:41:50 -06:00
checkoutCancelled = false ;
currentReceiptID = receiptID ;
2026-02-13 14:59:30 -07:00
try {
2026-03-31 17:41:50 -06:00
var transactionAmount = global . apis . i18n . moneyToFixed ( amount / 100.0 ) * 1.0 ;
2026-02-13 14:59:30 -07:00
// 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 ( ) ,
2026-03-31 17:41:50 -06:00
transactionAmount : transactionAmount ,
2026-02-13 14:59:30 -07:00
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 = "" ;
2026-03-31 17:41:50 -06:00
var purchaseResp ;
2026-02-13 14:59:30 -07:00
while ( paymentID == "" ) {
2026-03-31 17:41:50 -06:00
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 ;
}
2026-02-13 14:59:30 -07:00
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" ) {
2026-03-31 17:41:50 -06:00
purchaseResp = await apiRequest ( ` card-transactions/ ${ resultBody . id } ` , null , "GET" ) ;
2026-02-13 14:59:30 -07:00
global . apis . util . http . webhook . ack ( pollResults [ i ] . id ) ;
2026-03-31 17:41:50 -06:00
if ( purchaseResp . invoiceNumber == currentReceiptID ) {
console . log ( "Got transaction ID from Helcim webhook:" , paymentID ) ;
console . log ( "purchaseResp" , purchaseResp ) ;
paymentID = resultBody . id ;
}
2026-02-13 14:59:30 -07:00
} else if ( resultBody . type == "terminalCancel" ) {
2026-03-31 17:41:50 -06:00
if ( resultBody . data . deviceCode == code && resultBody . data . invoiceNumber == currentReceiptID && resultBody . data . transactionAmount == transactionAmount ) {
2026-02-13 14:59:30 -07:00
paymentID = "CANCEL" ;
global . apis . util . http . webhook . ack ( pollResults [ i ] . id ) ;
}
}
}
} catch ( ex ) {
console . error ( ex ) ;
}
}
if ( paymentID == "CANCEL" ) {
2026-03-31 17:41:50 -06:00
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 ;
2026-02-13 14:59:30 -07:00
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" ) ;
2026-03-31 17:41:50 -06:00
currentReceiptID = null ;
2026-02-13 14:59:30 -07:00
return false ;
}
if ( purchaseResp . status == "APPROVED" ) {
global . apis . pos . addOnscreenPaymentLog ( "Payment approved!" ) ;
if ( purchaseResp . type == "purchase" ) {
pstring = ` ${ purchaseResp . cardHolderName ? ? "" } \n ${ getCardBrand ( purchaseResp . cardType ) } \n x ${ purchaseResp . cardNumber . slice ( - 4 ) } \n Approval: ${ purchaseResp . approvalCode } ` ;
global . apis . pos . addReceiptPayment (
new global . apis . pos . ReceiptPayment (
purchaseResp . amount ,
"card" ,
pstring
)
) ;
2026-03-31 17:41:50 -06:00
currentReceiptID = null ;
2026-02-13 14:59:30 -07:00
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" ) ;
}
2026-03-31 17:41:50 -06:00
currentReceiptID = null ;
2026-02-13 14:59:30 -07:00
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" ) ;
}
2026-03-31 17:41:50 -06:00
currentReceiptID = null ;
2026-02-13 14:59:30 -07:00
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.
2026-03-31 17:41:50 -06:00
// Stop the webhook wait loop.
checkoutCancelled = true ;
2026-02-13 14:59:30 -07:00
} ,
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 < / l i > < l i > C l i c k \ " A l l T o o l s \ " < / l i > < l i > S e l e c t \ " I n t e g r a t i o n s \ " < / l i > \ n \
< li > Click Webhooks < / l i > < l i > T u r n t h e t o g g l e o n < / l i > < l i > C l i c k i n t o t h e \ " D e l i v e r U R L \ " b o x < / l i > \ n \
< li > Paste the URL ( Ctrl - V or right - click and Paste ) < / l i > \ n \
< li > Check all the boxes next to \ "Notify app when changes made to\" < / l i > \ 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."
}
] ;