647 lines
30 KiB
JavaScript
647 lines
30 KiB
JavaScript
// Adds Square Terminal as a payment processor option.
|
|
|
|
const SQUARE_ENV = "PROD"; // "SANDBOX"
|
|
var URL_BASE = "https://connect.squareup.com";
|
|
if (SQUARE_ENV == "SANDBOX") {
|
|
URL_BASE = "https://connect.squareupsandbox.com";
|
|
} else {
|
|
URL_BASE = "https://connect.squareup.com";
|
|
}
|
|
const APP_ID = "sq0idp-mrUlE9gCQ9yLevtbqyurDw";// "sandbox-sq0idb-EGkAFQuSaL3mPwzG8fxPIA";
|
|
const SQUARE_API_VERSION = "2025-05-21";
|
|
const REDIRECT_URI = "https://postalpoint.app/oauth/square";
|
|
|
|
var code_verifier_code = null;
|
|
|
|
var checkout_id = null;
|
|
|
|
var latestTerminalActionID = "";
|
|
|
|
async function apiRequest(url, data = {}, method = "POST") {
|
|
var accessToken = global.apis.settings.get("app.postalpoint.squareterminal.access_token", "");
|
|
console.log(`${url}:`, data);
|
|
return await global.apis.util.http.post(`${URL_BASE}/${url}`, data, "json", {
|
|
"Content-Type": "application/json",
|
|
"Square-Version": `${SQUARE_API_VERSION}`,
|
|
"Authorization": `Bearer ${accessToken}`
|
|
}, method, true);
|
|
}
|
|
|
|
async function loginToSquare() {
|
|
code_verifier_code = global.apis.util.uuid.v4();
|
|
var code_challenge_array = await window.crypto.subtle.digest("SHA-256", (new TextEncoder().encode(code_verifier_code)));
|
|
const code_challenge = Array.from(new Uint8Array(code_challenge_array))
|
|
.map((item) => item.toString(16).padStart(2, "0"))
|
|
.join("");
|
|
const scope = ["DEVICE_CREDENTIAL_MANAGEMENT", "DEVICES_READ", "MERCHANT_PROFILE_READ", "MERCHANT_PROFILE_WRITE", "CUSTOMERS_WRITE", "CUSTOMERS_READ", "PAYMENTS_READ", "PAYMENTS_WRITE"].join("+");
|
|
const state = global.apis.util.uuid.v4();
|
|
const url = `${URL_BASE}/oauth2/authorize?client_id=${APP_ID}&scope=${scope}&session=false&redirect_uri=${REDIRECT_URI}&code_challenge=${code_challenge}&state=${state}`;
|
|
|
|
async function onBrowserLogin(url) {
|
|
console.log(url);
|
|
if (url.startsWith(REDIRECT_URI)) {
|
|
console.log("Got oauth redirect!");
|
|
global.apis.eventbus.emit("browserCloseRequest");
|
|
global.apis.eventbus.off("browserNavigate", onBrowserLogin);
|
|
|
|
var params = url.split("?")[1];
|
|
var query = new URLSearchParams(params);
|
|
if (query.get("error") != null) {
|
|
global.apis.alert(`Could not authorize PostalPoint to access your Square account. Error code ${query.get("error")} (${query.get("error_description")})`, "Square Login Error");
|
|
return;
|
|
}
|
|
|
|
if (query.get("response_type") == "code" && query.get("code") != null) {
|
|
var authorizationCode = query.get("code");
|
|
|
|
var resp = await global.apis.util.http.post(`${URL_BASE}/oauth2/token`, {
|
|
client_id: APP_ID,
|
|
grant_type: "authorization_code",
|
|
redirect_uri: REDIRECT_URI,
|
|
code: authorizationCode,
|
|
code_verifier: code_verifier_code
|
|
}, "json", {
|
|
"Content-Type": "application/json",
|
|
"Square-Version": `${SQUARE_API_VERSION}`
|
|
});
|
|
|
|
if (!resp.access_token) {
|
|
global.apis.alert(`Could not authorize PostalPoint to access your Square account. The token request failed.`, "Square Login Error");
|
|
return;
|
|
}
|
|
global.apis.settings.set("app.postalpoint.squareterminal.access_token", resp.access_token);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.merchant_id", resp.merchant_id);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.refresh_token", resp.refresh_token);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.refresh_token_expires_at", resp.refresh_token_expires_at);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.access_token_expires_at", resp.expires_at);
|
|
|
|
global.apis.alert("PostalPoint is now logged in to your Square merchant account.", "Connected to Square");
|
|
|
|
} else {
|
|
global.apis.alert(`Could not authorize PostalPoint to access your Square account. No authorization code (or error code) was received from Square.`, "Square Login Error");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
global.apis.eventbus.on("browserNavigate", onBrowserLogin);
|
|
|
|
global.apis.ui.openInternalWebBrowser(url);
|
|
}
|
|
|
|
/**
|
|
* Checks if the Square tokens are expiring "soon" and refresh them if needed.
|
|
* @returns {undefined}
|
|
*/
|
|
async function refreshTokens() {
|
|
var accessExpiresAt = global.apis.settings.get("app.postalpoint.squareterminal.access_token_expires_at", "");
|
|
var refreshExpiresAt = global.apis.settings.get("app.postalpoint.squareterminal.refresh_token_expires_at", "");
|
|
if (accessExpiresAt == "" || refreshExpiresAt == "") {
|
|
return;
|
|
}
|
|
const refreshExpireThreshold = 60 * 60 * 24 * 30; // 30 days
|
|
const accessExpireThreshold = 60 * 60 * 24 * 7; // 7 days
|
|
if (global.apis.util.time.strtotime(accessExpiresAt) < global.apis.util.time.now() - accessExpireThreshold || global.apis.util.time.strtotime(refreshExpiresAt) < global.apis.util.time.now() - refreshExpireThreshold) {
|
|
// Renew tokens
|
|
console.log("Renewing Square tokens");
|
|
var resp = await global.apis.util.http.post(`${URL_BASE}/oauth2/token`, {
|
|
client_id: APP_ID,
|
|
grant_type: "refresh_token",
|
|
redirect_uri: REDIRECT_URI,
|
|
refresh_token: global.apis.settings.get("app.postalpoint.squareterminal.refresh_token", ""),
|
|
}, "json", {
|
|
"Content-Type": "application/json",
|
|
"Square-Version": `${SQUARE_API_VERSION}`
|
|
});
|
|
|
|
if (!resp.access_token) {
|
|
global.apis.alert(`You need to log in to Square again in the plugin settings.`, "Square Logged Out");
|
|
return;
|
|
}
|
|
global.apis.settings.set("app.postalpoint.squareterminal.access_token", resp.access_token);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.merchant_id", resp.merchant_id);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.refresh_token", resp.refresh_token);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.refresh_token_expires_at", resp.refresh_token_expires_at);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.access_token_expires_at", resp.expires_at);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the Square access tokens and info from the settings storage, then display a message to the user.
|
|
* @returns {undefined}
|
|
*/
|
|
function logoutFromSquare() {
|
|
global.apis.settings.set("app.postalpoint.squareterminal.access_token", null);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.merchant_id", null);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.refresh_token", null);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.refresh_token_expires_at", null);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.access_token_expires_at", null);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.terminal_device_code_id", null);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.location_id", null);
|
|
global.apis.alert(`PostalPoint is now logged out of your Square account. You'll need to log in again to use Square with PostalPoint.`, "Square Logged Out");
|
|
}
|
|
|
|
async function pairTerminal() {
|
|
var locationId = "main";
|
|
var locations = await apiRequest("v2/locations", {}, "GET");
|
|
var locationList = [];
|
|
for (var i = 0; i < locations.locations.length; i++) {
|
|
if (locations.locations[i].type != "PHYSICAL") {
|
|
continue;
|
|
}
|
|
if (locations.locations[i].status != "ACTIVE") {
|
|
continue;
|
|
}
|
|
if (locations.locations[i].capabilities.includes("CREDIT_CARD_PROCESSING") != true) {
|
|
continue;
|
|
}
|
|
locationList.push({
|
|
id: locations.locations[i].id,
|
|
name: locations.locations[i].name
|
|
});
|
|
}
|
|
if (locationList.length == 0) {
|
|
global.apis.alert("Your Square account doesn't have a physical location that can process card payments. Set one up in your Square Dashboard.", "Square Error");
|
|
return;
|
|
}
|
|
if (locationList.length == 1) {
|
|
locationId = locationList[0].id;
|
|
getTerminalPairingCode(locationId);
|
|
} else {
|
|
var buttons = [];
|
|
for (let i = 0; i < locationList.length; i++) {
|
|
buttons.push({
|
|
text: locationList[i].name,
|
|
onClick: function () {
|
|
getTerminalPairingCode(locationList[i].id);
|
|
}
|
|
});
|
|
}
|
|
global.apis.f7.dialog.create({
|
|
title: "Select Square location",
|
|
text: "Your Square account has multiple locations. Which one is PostalPoint set up at?",
|
|
verticalButtons: true,
|
|
buttons: buttons
|
|
}).show();
|
|
}
|
|
|
|
}
|
|
|
|
async function getTerminalPairingCode(locationId = "main") {
|
|
var resp = await apiRequest("v2/devices/codes", {
|
|
idempotency_key: global.apis.util.uuid.v4(),
|
|
device_code: {
|
|
product_type: "TERMINAL_API",
|
|
location_id: locationId
|
|
}
|
|
});
|
|
|
|
console.log(resp);
|
|
|
|
global.apis.f7.dialog.alert(`On your Square Terminal device's sign in page, tap "Use a device code" and enter this pairing code within five minutes. Wait for the Square Terminal to be fully loaded, then press OK here.<br /><br /><h3>${resp.device_code.code}</h3>`, "Pairing Code", async function () {
|
|
resp = await apiRequest(`v2/devices/codes/${resp.device_code.id}`, {}, "GET");
|
|
console.log(resp);
|
|
if (resp.device_code.status != "PAIRED") {
|
|
global.apis.ui.showProgressSpinner("Pairing...", "Pairing to the Square Terminal is taking a little while.");
|
|
await global.apis.util.delay(10000);
|
|
resp = await apiRequest(`v2/devices/codes/${resp.device_code.id}`, {}, "GET");
|
|
console.log(resp);
|
|
}
|
|
global.apis.ui.hideProgressSpinner();
|
|
if (resp.device_code.status != "PAIRED") {
|
|
global.apis.alert("Your Square Terminal is not paired. Please try again.", "Pairing Failed");
|
|
return;
|
|
}
|
|
global.apis.settings.set("app.postalpoint.squareterminal.terminal_device_id", resp.device_code.device_id);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.terminal_device_code_id", resp.device_code.id);
|
|
global.apis.settings.set("app.postalpoint.squareterminal.location_id", resp.device_code.location_id);
|
|
global.apis.alert("Your Square Terminal has been paired with PostalPoint. You're ready to take card payments!", "Pairing Complete");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Poll a list of payment IDs and wait for them to stop being PENDING. If a payment needs capture, do that too.
|
|
* @param {array} paymentIDs
|
|
* @returns {nm$_plugin.waitForPaymentsToFinish.paymentData}
|
|
*/
|
|
async function waitForPaymentsToFinish(paymentIDs = []) {
|
|
var paymentData = {
|
|
amountCaptured: 0,
|
|
paymentsCaptured: []
|
|
};
|
|
for (let i = 0; i < paymentIDs.length; i++) {
|
|
var payment = await apiRequest(`v2/payments/${paymentIDs[i]}`, {}, "GET");
|
|
do {
|
|
if (payment.payment.status == "PENDING") {
|
|
global.apis.pos.addOnscreenPaymentLog("Waiting for payment to process...");
|
|
await global.apis.util.delay(2000);
|
|
payment = await apiRequest(`v2/payments/${paymentIDs[i]}`, {}, "GET");
|
|
} else if (payment.payment.status == "APPROVED") {
|
|
// capture payment
|
|
payment = await apiRequest(`v2/payments/${paymentIDs[i]}/complete`, {});
|
|
}
|
|
} while (payment.payment.status == "PENDING" || payment.payment.status == "APPROVED");
|
|
|
|
switch (payment.payment.status) {
|
|
case "COMPLETED":
|
|
paymentData.amountCaptured += payment.payment.amount_money.amount;
|
|
paymentData.paymentsCaptured.push(payment.payment);
|
|
break;
|
|
case "CANCELED":
|
|
break;
|
|
case "FAILED":
|
|
break;
|
|
}
|
|
}
|
|
return paymentData;
|
|
}
|
|
|
|
function addPaymentsToReceipt(payments) {
|
|
for (let i = 0; i < payments.length; i++) {
|
|
var p = payments[i];
|
|
if (p.payment) {
|
|
p = p.payment;
|
|
}
|
|
var ptype = "card";
|
|
var pstring = "";
|
|
switch (p.source_type) {
|
|
case "CARD":
|
|
ptype = "card";
|
|
if (p.card_details.entry_method == "EMV") {
|
|
pstring = `${p.card_details.card.cardholder_name}\n${p.card_details.card.card_brand}\nx${p.card_details.card.last_4}\nEMV Chip\n${p.card_details.application_name}\nAID: ${p.card_details.application_identifier}`;
|
|
} else {
|
|
pstring = `${p.card_details.card.cardholder_name ?? ""}\n${p.card_details.card.card_brand}\nx${p.card_details.card.last_4}\n`;
|
|
switch (p.card_details.entry_method) {
|
|
case "KEYED":
|
|
pstring += "Keyed-in";
|
|
break;
|
|
case "SWIPED":
|
|
pstring += "Swiped";
|
|
break;
|
|
case "ON_FILE":
|
|
pstring += "Card on file";
|
|
break;
|
|
case "CONTACTLESS":
|
|
pstring += "Contactless";
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case "BANK_ACCOUNT":
|
|
ptype = "ach";
|
|
if (p.bank_account_details.transfer_type == "ACH") {
|
|
pstring = `${p.bank_account_details.bank_name}\nx${p.bank_account_details.ach_details.account_number_suffix}\n${p.bank_account_details.ach_details.account_type}`;
|
|
} else {
|
|
pstring = `${p.bank_account_details.bank_name}`;
|
|
}
|
|
break;
|
|
case "CASH":
|
|
ptype = "cash";
|
|
break;
|
|
case "WALLET":
|
|
ptype = "card";
|
|
pstring = `${p.wallet_details.brand}`;
|
|
if (p.wallet_details.brand == "CASH_APP") {
|
|
pstring += `\n${p.wallet_details.cash_app_details.buyer_full_name}\n${p.wallet_details.cash_app_details.buyer_cashtag}`;
|
|
}
|
|
break;
|
|
case "BUY_NOW_PAY_LATER":
|
|
ptype = "card";
|
|
pstring = `Buy Now, Pay Later\n${p.buy_now_pay_later_details.brand}`;
|
|
break;
|
|
case "SQUARE_ACCOUNT":
|
|
ptype = "card";
|
|
pstring = "Square Account";
|
|
break;
|
|
case "EXTERNAL":
|
|
switch (p.external_details.type) {
|
|
case "CHECK":
|
|
ptype = "check";
|
|
break;
|
|
case "BANK_TRANSFER":
|
|
ptype = "ach";
|
|
break;
|
|
case "CRYPTO":
|
|
ptype = "crypto";
|
|
break;
|
|
default:
|
|
ptype = "card";
|
|
break;
|
|
}
|
|
pstring = `${p.external_details.type}\n${p.external_details.source}`;
|
|
break;
|
|
}
|
|
global.apis.pos.addReceiptPayment(
|
|
new global.apis.pos.ReceiptPayment(
|
|
(p.amount_money.amount / 100).toFixed(2) * 1,
|
|
ptype, // Payment type. Accepted values are card, ach, crypto, cash, check, account, and free. Other types will be displayed as-is to the user and on the receipt.
|
|
pstring // Additional text for receipt
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
exports.init = function () {
|
|
setInterval(refreshTokens, 1000 * 60 * 60);
|
|
global.apis.pos.registerCardProcessor({
|
|
name: "Square",
|
|
init: async function () {
|
|
// This function runs once after starting PostalPoint
|
|
// and before any other card processor functions are called.
|
|
console.info("Hello from Square Terminal 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.
|
|
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 checkoutData = await apiRequest("v2/terminals/checkouts", {
|
|
idempotency_key: global.apis.util.uuid.v4(),
|
|
checkout: {
|
|
amount_money: {
|
|
amount: amount,
|
|
currency: "USD"
|
|
},
|
|
device_options: {
|
|
device_id: global.apis.settings.get("app.postalpoint.squareterminal.terminal_device_id", ""),
|
|
tip_settings: {
|
|
allow_tipping: false
|
|
},
|
|
skip_receipt_screen: true
|
|
},
|
|
payment_options: {
|
|
autocomplete: capture
|
|
}
|
|
}
|
|
});
|
|
|
|
checkout_id = checkoutData.checkout.id;
|
|
|
|
checkoutStatus = checkoutData.checkout.status;
|
|
|
|
while (checkoutStatus != "COMPLETED" && checkoutStatus != "CANCELED") {
|
|
await global.apis.util.delay(1000); // Wait a second
|
|
checkoutData = await apiRequest(`v2/terminals/checkouts/${checkout_id}`, {}, "GET");
|
|
checkoutStatus = checkoutData.checkout.status;
|
|
}
|
|
|
|
if (checkoutStatus != "COMPLETED") {
|
|
global.apis.pos.addOnscreenPaymentLog("Square Terminal payment not completed.");
|
|
if (checkoutStatus == "CANCELED") {
|
|
global.apis.alert("The card payment was canceled before it finished.", "Payment canceled");
|
|
switch (checkoutData.checkout.cancel_reason) {
|
|
case "BUYER_CANCELED":
|
|
global.apis.pos.addOnscreenPaymentLog("The payment was canceled from the Square device.");
|
|
break;
|
|
case "SELLER_CANCELED":
|
|
global.apis.pos.addOnscreenPaymentLog("The payment was canceled by the merchant.");
|
|
break;
|
|
case "TIMED_OUT":
|
|
global.apis.pos.addOnscreenPaymentLog("The payment was canceled because it took too long to complete.");
|
|
break;
|
|
}
|
|
} else {
|
|
global.apis.alert(`The card payment was not completed, and has status code ${checkoutStatus}.`, "Payment not complete");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
console.log(checkoutData);
|
|
if (checkoutData.checkout.payment_options.autocomplete) {
|
|
var paymentInfos = await waitForPaymentsToFinish(checkoutData.checkout.payment_ids);
|
|
console.log(paymentInfos);
|
|
addPaymentsToReceipt(paymentInfos.paymentsCaptured);
|
|
global.apis.pos.addOnscreenPaymentLog("Payment successful!");
|
|
return true;
|
|
} else {
|
|
return checkoutData.checkout.payment_ids.join(",");
|
|
}
|
|
} 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 was declined.", "Card Error");
|
|
} else {
|
|
global.apis.alert("The customer's card was declined.", "Card Error");
|
|
}
|
|
return false;
|
|
}
|
|
},
|
|
cancelCheckout: async function () {
|
|
// The user requested to cancel the payment.
|
|
// Reset the terminal to its resting state, clear its screen, etc.
|
|
try {
|
|
if (checkout_id != null) {
|
|
var resp = await apiRequest(`v2/terminals/checkouts/${checkout_id}/cancel`);
|
|
if (resp.checkout.status == "CANCELED") {
|
|
checkout_id = null;
|
|
}
|
|
}
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
},
|
|
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.
|
|
|
|
var paymentIDs = checkoutResponse.split(",");
|
|
|
|
var paymentInfos = await waitForPaymentsToFinish(paymentIDs);
|
|
|
|
addPaymentsToReceipt(paymentInfos.paymentsCaptured);
|
|
|
|
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.
|
|
|
|
var paymentResponse = await apiRequest(`v2/payments`, {
|
|
idempotency_key: global.apis.util.uuid.v4(),
|
|
amount_money: {
|
|
amount: amount,
|
|
currency: "USD"
|
|
},
|
|
autocomplete: true,
|
|
source_id: paymentMethodID,
|
|
customer_id: customerID,
|
|
location_id: global.apis.settings.get("app.postalpoint.squareterminal.location_id", "main")
|
|
}, "POST");
|
|
|
|
console.log(paymentResponse);
|
|
|
|
if (typeof paymentResponse.errors != "undefined") {
|
|
global.apis.pos.addOnscreenPaymentLog("Error processing saved payment: " + paymentResponse.errors[0].code);
|
|
global.apis.alert(paymentResponse.errors[0].code, "Saved payment error");
|
|
return false;
|
|
} else {
|
|
if (paymentResponse.payment.status == "COMPLETED") {
|
|
addPaymentsToReceipt([paymentResponse.payment]);
|
|
return true;
|
|
} else if (paymentResponse.payment.status == "CANCELED") {
|
|
global.apis.pos.addOnscreenPaymentLog("The saved card payment was canceled.");
|
|
global.apis.alert("The saved card payment was canceled before it finished.", "Payment canceled");
|
|
return false;
|
|
} else if (paymentResponse.payment.status == "FAILED") {
|
|
global.apis.pos.addOnscreenPaymentLog("The saved card payment failed.");
|
|
global.apis.alert("The saved card payment failed.", "Payment failed");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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.
|
|
|
|
// TODO: Lookup or create Square customer
|
|
statusCallback("Syncing customer data with Square", false);
|
|
|
|
var squareCustomerID = "";
|
|
var customerSearchResponse = await apiRequest(`v2/customers/search`, {
|
|
query: {
|
|
filter: {
|
|
reference_id: {
|
|
exact: customerUUID
|
|
}
|
|
}
|
|
},
|
|
limit: 2
|
|
}, "POST");
|
|
|
|
if (Object.keys(customerSearchResponse).length == 0 || customerSearchResponse.customers.length == 0) {
|
|
// Create customer
|
|
var newCustomerInfo = await apiRequest("v2/customers", {
|
|
idempotency_key: global.apis.util.uuid.v4(),
|
|
given_name: name.split(" ", 2)[0],
|
|
family_name: (name.split(" ", 2)[1] ?? ""),
|
|
nickname: name,
|
|
company_name: company,
|
|
email_address: email,
|
|
phone_number: phone,
|
|
address: {
|
|
address_line_1: street1,
|
|
locality: city,
|
|
administrative_district_level_1: state,
|
|
postal_code: zip
|
|
}
|
|
}, "POST");
|
|
squareCustomerID = newCustomerInfo.customer.id;
|
|
} else if (customerSearchResponse.customers.length == 1) {
|
|
// Customer found
|
|
squareCustomerID = customerSearchResponse.customers[0].id;
|
|
} else {
|
|
// Too many customers found
|
|
throw new Error("There are multiple customers in your Square account with the same PostalPoint customer UUID number (a.k.a. Square reference ID). PostalPoint can't tell which one to use for this action.");
|
|
return false;
|
|
}
|
|
|
|
statusCallback("Insert the card into the reader.", false);
|
|
|
|
var actionResponse = await apiRequest(`v2/terminals/actions`, {
|
|
idempotency_key: global.apis.util.uuid.v4(),
|
|
action: {
|
|
device_id: global.apis.settings.get("app.postalpoint.squareterminal.terminal_device_id", ""),
|
|
type: "SAVE_CARD",
|
|
save_card_options: {
|
|
customer_id: squareCustomerID,
|
|
reference_id: customerUUID
|
|
}
|
|
}
|
|
}, "POST");
|
|
|
|
// Save this in case we need to cancel
|
|
latestTerminalActionID = actionResponse.action.id;
|
|
|
|
do {
|
|
await global.apis.util.delay(1000);
|
|
actionResponse = await apiRequest(`v2/terminals/actions/${actionResponse.action.id}`, {}, 'GET');
|
|
} while (actionResponse.action.status == "PENDING" || actionResponse.action.status == "IN_PROGRESS");
|
|
|
|
if (actionResponse.action.status == "COMPLETED") {
|
|
statusCallback("Card saved!", true);
|
|
return true;
|
|
} else if (actionResponse.action.status == "CANCELED" || actionResponse.action.status == "CANCEL_REQUESTED") {
|
|
throw new Error("Operation cancelled.");
|
|
return false;
|
|
} else {
|
|
throw new Error("Can't determine if card was saved at this time.");
|
|
return false;
|
|
}
|
|
},
|
|
cancelSaveCardForOfflineUse: function () {
|
|
// Cancel the process running in saveCardForOfflineUse() at the user/cashier's request.
|
|
if (latestTerminalActionID != "") {
|
|
apiRequest(`v2/terminals/actions/${latestTerminalActionID}/cancel`, {}, "POST");
|
|
latestTerminalActionID = "";
|
|
}
|
|
},
|
|
getSavedPaymentMethods: async function ( {customerUUID}) {
|
|
// Return all saved payment methods tied to the provided customer UUID.
|
|
var methods = [];
|
|
var response = await apiRequest(`v2/cards?reference_id=${customerUUID}`, {}, "GET");
|
|
if (Object.keys(response).length == 0) {
|
|
return [];
|
|
}
|
|
console.log(response);
|
|
for (let i = 0; i < response.cards.length; i++) {
|
|
var card = response.cards[i];
|
|
methods.push({
|
|
customer: card.customer_id, // Passed to checkoutSavedMethod as cardProcessorCustomerID
|
|
customer_uuid: customerUUID,
|
|
id: card.id, // Passed to checkoutSavedMethod as paymentMethodID
|
|
type: "card", // Payment type. Accepted values are card, ach, crypto, cash, check, account, and free.
|
|
label: `${card.card_brand} ${card.card_type} x${card.last_4} (exp. ${card.exp_month}/${card.exp_year})`, // Label for payment method
|
|
label_short: `${card.card_brand} ${card.card_type} x${card.last_4}` // Abbreviated label for payment method
|
|
});
|
|
}
|
|
console.log(methods);
|
|
return methods;
|
|
},
|
|
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.
|
|
await apiRequest(`v2/cards/${paymentMethodID}/disable`, {}, "POST");
|
|
}
|
|
});
|
|
}
|
|
|
|
// Plugin settings to display.
|
|
exports.config = [
|
|
{
|
|
type: "button",
|
|
label: "Log In to Square",
|
|
text: "Step 1: Connect PostalPoint to your Square merchant account.",
|
|
onClick: function () {
|
|
loginToSquare();
|
|
}
|
|
},
|
|
{
|
|
type: "button",
|
|
label: "Pair Square Terminal hardware",
|
|
text: "Step 2: Connect a Square Terminal device to PostalPoint and accept card payments.",
|
|
onClick: function () {
|
|
pairTerminal();
|
|
}
|
|
},
|
|
{
|
|
type: "button",
|
|
label: "Log Out from Square",
|
|
text: "Disconnect PostalPoint from your Square merchant account.",
|
|
onClick: function () {
|
|
logoutFromSquare();
|
|
}
|
|
}
|
|
];
|