Create docs and examples
This commit is contained in:
commit
d2e998eafc
199
README.md
Normal file
199
README.md
Normal file
@ -0,0 +1,199 @@
|
||||
# PostalPoint Plugins
|
||||
|
||||
PostalPoint® supports plugin extensions. Plugins can hook into PostalPoint to add features.
|
||||
|
||||
## What plugins can do
|
||||
|
||||
* Process card payments and handle saved payment methods
|
||||
* Add additional carriers and provide shipping rates
|
||||
* Print to label and receipt printers, letting PostalPoint handle hardware support and drivers
|
||||
* Extend support for prepaid label acceptance, prepaid barcode recognition, and carrier dropoff QR codes
|
||||
* Install pages in the Tools interface, creating new interfaces and features
|
||||
* Receive transaction receipts for ingestion into third-party accounting or business software
|
||||
* Run both Node.JS and browser code.
|
||||
|
||||
## Plugin Package Structure
|
||||
|
||||
A plugin is distributed as a simple ZIP file, containing a folder. That folder's name is the plugin
|
||||
name. The folder then has at least one file, named `plugin.js`.
|
||||
he `exports.init` function in `plugin.js` is executed when PostalPoint launches, allowing the plugin
|
||||
to request involvement with various events in PostalPoint.
|
||||
|
||||
PostalPoint installs plugin packages by unzipping their contents into a plugins folder.
|
||||
Plugins are uninstalled by deleting their folder.
|
||||
|
||||
## Minimal Plugin Code
|
||||
|
||||
```javascript
|
||||
// plugin-name/plugin.js
|
||||
exports.init = function () {
|
||||
global.apis.alert("This message appears when PostalPoint launches.", "Hello!");
|
||||
};
|
||||
```
|
||||
|
||||
Yes, the smallest plugin really is just two lines of code, and accessing PostalPoint features
|
||||
really is that easy.
|
||||
|
||||
## Example Plugins
|
||||
|
||||
Check out the `plugins` folder for fully-functional plugins.
|
||||
|
||||
## PostalPoint Plugin API
|
||||
|
||||
The PostalPoint plugin API is a globally-available object.
|
||||
|
||||
### API List
|
||||
|
||||
#### Core
|
||||
|
||||
`global.apis.`:
|
||||
|
||||
* `alert(text, title)`: Display an alert dialog.
|
||||
* `getAppFolder()`: Get the filesystem path to the PostalPoint installation folder.
|
||||
* `getPluginFolder(pluginName)`: Get the filesystem path to a plugin's folder.
|
||||
If used without arguments, gets the root plugin storage folder.
|
||||
* `f7`: The Framework7 app instance for PostalPoint's entire UI, created by `new Framework7()`. See https://framework7.io/docs/app for details. Be very careful.
|
||||
|
||||
|
||||
#### Barcode
|
||||
|
||||
`global.apis.barcode.`:
|
||||
|
||||
* `TrackingBarcode`: A class defining a prepaid barcode and related information. See docs/TrackingBarcode.md for details.
|
||||
* `onPrepaidScan(function (codeString) {})`: The function passed to onPrepaidScan is run
|
||||
when a barcode is scanned on the Prepaid page. The function is passed one argument, a string
|
||||
containing the raw barcode data. The function shall return boolean `false` if unable or unwilling
|
||||
to handle the barcode. If the barcode is handled by this function, it shall return a TrackingBarcode object.
|
||||
* `addPrepaidBarcode(trackingBarcodeData)`: Add a TrackingBarcode object to the transaction receipt at any time other than `onPrepaidScan`.
|
||||
|
||||
#### Database
|
||||
|
||||
PostalPoint supports multiple SQL databases, currently SQLite and MariaDB.
|
||||
|
||||
`global.apis.database.`:
|
||||
|
||||
* `async getConnection()`: Returns a database driver. See docs/Database.md for details.
|
||||
|
||||
|
||||
#### Graphics
|
||||
|
||||
PostalPoint uses the Jimp library version 1.6 for creating and manipulating images and shipping labels.
|
||||
|
||||
`global.apis.graphics.`:
|
||||
|
||||
* `Jimp()`: The [JavaScript Image Manipulation Program](https://jimp-dev.github.io/jimp/). Access like so: `const {Jimp} = global.apis.graphics.Jimp();`.
|
||||
* `async loadFont(filename)`: Replacement for [Jimp's loadFont](https://jimp-dev.github.io/jimp/api/jimp/functions/loadfont/), which gets very confused about our JS environment and ends up crashing everything.
|
||||
|
||||
#### Kiosk
|
||||
|
||||
`global.apis.kiosk.`:
|
||||
|
||||
* `isKiosk()`: Returns a boolean to indicate if PostalPoint is running in kiosk mode.
|
||||
|
||||
#### Point of Sale
|
||||
|
||||
`global.apis.pos.`:
|
||||
|
||||
* `addReceiptItem(item)`: Add a `ReceiptItem` to the current transaction.
|
||||
* `addReceiptPayment(item)`: Add a `ReceiptPayment` to the current transaction.
|
||||
* `addOnscreenPaymentLog(string)`: Append a line of text to the onscreen log displayed during credit card processing. Not shown in kiosk mode.
|
||||
* `onReceiptChange(function (receipt) {})`: Add a function to be called whenever the transaction data/receipt is changed.
|
||||
* `onTransactionFinished(function (receipt) {})`: Same as `onReceiptChange` except run when a transaction is completed.
|
||||
* `registerCardProcessor(...)`: Register the plugin as a credit card processor. See plugins/payment-processor for details.
|
||||
* `ReceiptItem`: A class representing a sale item in the current transaction. See docs/Receipt.md for details.
|
||||
* `ReceiptPayment`: A class representing a payment entry for the current transaction. See docs/Receipt.md for details.
|
||||
|
||||
#### Printing
|
||||
|
||||
`global.apis.print.`:
|
||||
|
||||
* `async printLabelImage(image)`: Print a 300 DPI image to the configured shipping label printer. centered on a 4x6 shipping label.
|
||||
If the printer is 200/203 DPI, the image is automatically scaled down to keep the same real size.
|
||||
Accepts a Jimp image, or the raw image file data as a Node Buffer, ArrayBuffer, or Uint8Array.
|
||||
Also accepts an http/https image URL, but it is recommended you fetch the image yourself and handle any network errors.
|
||||
Recommended image size is 1200x1800, which is 4x6 inches at 300 DPI. 800x1200 images are assumed to be 4x6 but at 200 DPI.
|
||||
For compatibility reasons, the bottom 200 pixels on an 800x1400 image is cropped off.
|
||||
Images wider than they are tall will be rotated 90 degrees. Images that are 1200 or 800 pixels
|
||||
wide and multiples of 1800 or 1200 pixels tall, respectively, are automatically split and printed
|
||||
onto multiple 1200x1800 or 800x1200 labels.
|
||||
* `async getReceiptPrinter()`: Get the receipt printer interface. See docs/ReceiptPrinter.md for available printer functions and example code.
|
||||
* `async printReceiptData(data)`: Print the raw data generated by the receipt printer interface.
|
||||
* `imageToBitmap(jimpImage, dpiFrom = 300, dpiTo = 300)`: Convert a Jimp image to monochrome 1-bit
|
||||
pixel data for printing on a receipt printer. Converts color to grayscale, and grayscale is dithered to monochrome.
|
||||
Optionally changes the image DPI. Use this to get image data for sending to a receipt printer.
|
||||
Returns an object such as {width: 300, height: 200, img: Uint8Array}. Pass the img value to `drawImage`.
|
||||
|
||||
|
||||
#### Settings/Configuration Storage
|
||||
|
||||
PostalPoint provides a UI for user-configurable plugin settings. See `exports.config` in plugins/basic-demo/plugin.js for details.
|
||||
|
||||
Settings are typically very short strings. Do not store data in settings. Non-string settings values
|
||||
are transparently converted to/from JSON objects.
|
||||
|
||||
Use a unique prefix for your plugin to prevent key name conflicts.
|
||||
|
||||
`global.apis.settings.`:
|
||||
|
||||
* `get(key, defaultValue)`: Get a setting value, or the `defaultValue` if the setting is not set.
|
||||
* `set(key, value)`: Set a setting value.
|
||||
|
||||
|
||||
#### Storing Data
|
||||
|
||||
Behavior is the same as the settings storage. `setBig` stores the data to disk as a JSON file,
|
||||
while `setSmall` uses the settings storage.
|
||||
Use `setBig` and `getBig` for storing data except for very short string or number values.
|
||||
|
||||
`global.apis.storage.`:
|
||||
|
||||
* `getBig(key, defaultValue)`
|
||||
* `getSmall(key, defaultValue)`
|
||||
* `setBig(key, value)`
|
||||
* `setSmall(key, value)`
|
||||
|
||||
|
||||
#### Shipping and Rates
|
||||
|
||||
`global.apis.shipping.`:
|
||||
|
||||
* `Address`: A class representing an address. See docs/Address.md.
|
||||
* `getZIPCode(zip)`: Get data for a 5-digit US ZIP Code, such as
|
||||
`{"city": "HELENA", "state": "MT", "type": "STANDARD"}`.
|
||||
Returns `false` if the ZIP Code isn't in the database.
|
||||
* `registerRateEndpoint(getRate, purchase, idPrefix)`: Register the plugin as a shipping rate and label provider.
|
||||
See plugins/shipping/plugin.js for example usage.
|
||||
|
||||
#### UI
|
||||
|
||||
`global.apis.ui.`:
|
||||
|
||||
* `addToolsPage(page, title, id = "", description = "", cardTitle = "", icon = "")`: Add a page to the Tools screen. See plugins/basic-demo for example usage.
|
||||
* `showProgressSpinner(title, text = "", subtitle = "")`: Show a Framework7 notification with a loading icon.
|
||||
* `hideProgressSpinner()`: hide the UI element created by `showProgressSpinner`.
|
||||
* `openInternalWebBrowser(url)`: Open a web browser UI, navigating to the URL. The browser has forward/back/close buttons.
|
||||
* `openSystemWebBrowser(url)`: Open the native OS default browser to the URL given.
|
||||
|
||||
|
||||
#### Utilities
|
||||
|
||||
Various useful helper functions.
|
||||
|
||||
`global.apis.util.`:
|
||||
|
||||
* `barcode.getBuffer(data, type = "code128", height = 10, scale = 2, includetext = false)`: Get a PNG image buffer of a barcode. Uses library "bwip-js".
|
||||
* `clipboard.copy(text, showNotification = false)`: Copy a string to the system clipboard, optionally showing a "copied" notification to the user.
|
||||
* `async delay(ms = 1000)`: Pause execution for some amount of time in an async function, i.e., returns a Promise that resolves in some number of milliseconds.
|
||||
* `async http.fetch(url, responseType = "text", timeout = 15)`: Fetch a URL. `responseType` can be "text", "blob", "buffer", or "json". Timeout is in seconds.
|
||||
* `async http.post(url, data, responseType = "text", headers = {"Content-Type": "application/json"}, method = "POST", continueOnBadStatusCode = false)`: POST to a URL. `data` is sent as a JSON body.
|
||||
* `objectEquals(a, b)`: Compare two objects for equality.
|
||||
* `string.chunk(str, chunksize)`: Split a string into an array of strings of length `chunksize`.
|
||||
* `string.split(input, separator, limit)`: Split a string with a RegExp separator an optionally limited number of times.
|
||||
* `time.diff(compareto)`: Get the number of seconds between now and the given UNIX timestamp.
|
||||
* `time.format(format, timestamp = time.now())`: Take a UNIX timestamp in seconds and format it to a string. (Mostly) compatible with PHP's date() function.
|
||||
* `time.now()`: Get the current UNIX timestamp in seconds.
|
||||
* `time.strtotime(str)`: Parse a string date and return a UNIX timestamp.
|
||||
* `time.toDateString(timestamp)`: Get a localized date string for a UNIX timestamp.
|
||||
* `time.toTimeString(timestamp)`: Get a time string for a UNIX timestamp, for example, "2:01 PM".
|
||||
* `uuid.v4()`: Generate a version 4 UUID string, for example, "fcca5b12-6a11-46eb-96e4-5ed6365de977".
|
||||
* `uuid.short()`: Generate a 16-character random alphanumeric string, for example, "4210cd8f584e6f6c".
|
69
docs/Address.md
Normal file
69
docs/Address.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Address object
|
||||
|
||||
```javascript
|
||||
export default class Address {
|
||||
constructor(uuid = "", name = "", company = "", street1 = "", street2 = "", zip = "", city = "", state = "", country = "", phone = "", email = "") {
|
||||
this.uuid = uuid;
|
||||
this.name = name;
|
||||
this.company = company;
|
||||
this.street1 = street1;
|
||||
this.street2 = street2;
|
||||
this.zip = zip;
|
||||
this.city = city;
|
||||
this.state = state;
|
||||
this.country = country;
|
||||
this.phone = phone;
|
||||
this.email = email;
|
||||
this.residential = null;
|
||||
}
|
||||
|
||||
static fromObject(address) {
|
||||
if (address instanceof Address) {
|
||||
return address;
|
||||
}
|
||||
return new Address(address.uuid ?? "", address.name, address.company, address.street1,
|
||||
address.street2, address.zip, address.city, address.state, address.country,
|
||||
address.phone, address.email);
|
||||
}
|
||||
|
||||
toStringArray() {
|
||||
var citystatezipLine = [this.city, this.state, this.zip].filter(Boolean);
|
||||
return [this.name, this.company, this.street1, this.street2, `${citystatezipLine.join(" ")}`, (this.country == "US" ? "" : this.country)].filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the address provided is the same as this address.
|
||||
*/
|
||||
equals(address, checkUUID = false) {
|
||||
if (
|
||||
(checkUUID ? this.uuid == address.uuid : true)
|
||||
&& this.name == address.name
|
||||
&& this.company == address.company
|
||||
&& this.street1 == address.street1
|
||||
&& this.street2 == address.street2
|
||||
&& this.city == address.city
|
||||
&& this.state == address.state
|
||||
&& this.zip == address.zip
|
||||
&& this.country == address.country) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if an address is the same delivery point as this address.
|
||||
*/
|
||||
dpEquals(address) {
|
||||
if (
|
||||
this.street1 == address.street1
|
||||
&& this.street2 == address.street2
|
||||
&& this.city == address.city
|
||||
&& this.state == address.state
|
||||
&& this.zip == address.zip
|
||||
&& this.country == address.country) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
144
docs/Database.md
Normal file
144
docs/Database.md
Normal file
@ -0,0 +1,144 @@
|
||||
# SQL Database Drivers
|
||||
|
||||
`global.apis.database.getConnection()` returns one of these, depending on which database is in use.
|
||||
|
||||
## SQLite
|
||||
|
||||
```javascript
|
||||
export class SQLiteAdapter {
|
||||
constructor(db) {
|
||||
this.type = "sqlite";
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async query(query, replace) {
|
||||
if (global.devMode) {
|
||||
console.info(query, replace);
|
||||
}
|
||||
return await this.db.all(query, replace);
|
||||
}
|
||||
|
||||
async run(statement, replace) {
|
||||
if (global.devMode) {
|
||||
console.info(statement, replace);
|
||||
}
|
||||
return await this.db.run(statement, replace);
|
||||
}
|
||||
|
||||
async exec(statement) {
|
||||
if (global.devMode) {
|
||||
console.info(statement);
|
||||
}
|
||||
return await this.db.exec(statement);
|
||||
}
|
||||
|
||||
async exists(table, where, replace) {
|
||||
const q = await this.db.all("SELECT EXISTS(SELECT 1 FROM " + table + " WHERE " + where + ") as n", replace);
|
||||
if (q[0].n > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async close() {
|
||||
|
||||
}
|
||||
|
||||
async tableExists(table) {
|
||||
return (await this.db.get(`SELECT count(name) AS cnt FROM sqlite_master WHERE type='table' AND name=?`, table)).cnt > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version code set in the database by setSchemaVersion().
|
||||
*/
|
||||
async getSchemaVersion() {
|
||||
var res = await this.db.all(`PRAGMA user_version`);
|
||||
return res[0].user_version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database version, using PRAGMA user_version. Must be an integer.
|
||||
*/
|
||||
async setSchemaVersion(version) {
|
||||
await this.db.exec(`PRAGMA user_version = ${version}`);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## MariaDB/MySQL
|
||||
|
||||
```javascript
|
||||
export class MariaDBAdapter {
|
||||
constructor(connection) {
|
||||
this.type = "mariadb";
|
||||
this.conn = connection;
|
||||
}
|
||||
|
||||
async query(query, replace) {
|
||||
if (global.devMode) {
|
||||
console.info(query, replace);
|
||||
}
|
||||
return await this.conn.query(query, replace);
|
||||
}
|
||||
|
||||
async run(statement, replace) {
|
||||
if (global.devMode) {
|
||||
console.info(statement, replace);
|
||||
}
|
||||
return await this.query(statement, replace);
|
||||
}
|
||||
|
||||
async exec(statement) {
|
||||
if (global.devMode) {
|
||||
console.info(statement);
|
||||
}
|
||||
return await this.run(statement);
|
||||
}
|
||||
|
||||
async exists(table, where, replace) {
|
||||
const q = await this.query("SELECT EXISTS(SELECT 1 FROM " + table + " WHERE " + where + ") as n", replace);
|
||||
if (q[0].n > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.conn.release();
|
||||
}
|
||||
|
||||
async tableExists(table) {
|
||||
return (await this.query("SHOW TABLES LIKE ?", table)).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version code set in the database by setSchemaVersion(). Returns zero if not set.
|
||||
*/
|
||||
async getSchemaVersion() {
|
||||
if (await this.tableExists("database_metadata")) {
|
||||
var res = await this.query("SELECT `value` FROM database_metadata WHERE `key`='schema_version' LIMIT 1");
|
||||
console.log(res);
|
||||
console.log(res[0].value);
|
||||
if (res.length == 1) {
|
||||
return res[0].value;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a version number for the database schema.
|
||||
* Must be an integer to maintain code compatibility with SQLite driver.
|
||||
* Will create a "database_metadata" table if required to store the version number.
|
||||
*/
|
||||
async setSchemaVersion(version) {
|
||||
if (await this.tableExists("database_metadata")) {
|
||||
await this.query("REPLACE INTO `database_metadata` (`key`, `value`) VALUES (?, ?)", ["schema_version", version]);
|
||||
} else {
|
||||
await this.exec("CREATE TABLE IF NOT EXISTS `database_metadata` ( `key` VARCHAR(50) NOT NULL, `value` VARCHAR(255) NOT NULL DEFAULT '', PRIMARY KEY (`key`))");
|
||||
await this.setSchemaVersion(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
394
docs/Parcel.md
Normal file
394
docs/Parcel.md
Normal file
@ -0,0 +1,394 @@
|
||||
# Parcel/Package Object
|
||||
|
||||
This object is supplied a plugin registered with `registerRateEndpoint` when PostalPoint requests
|
||||
shipping rates from the plugin.
|
||||
|
||||
```javascript
|
||||
export class Package {
|
||||
constructor(isPrepaid = false) {
|
||||
this.prepaid = isPrepaid;
|
||||
this.packaging = {
|
||||
type: "Parcel",
|
||||
service: "",
|
||||
carrier: "",
|
||||
length: 999999,
|
||||
width: 999999,
|
||||
height: 999999,
|
||||
weightOz: 999999,
|
||||
nonmachinable: false,
|
||||
internalid: 100,
|
||||
oversizeFlag: false
|
||||
};
|
||||
this.extraServices = {
|
||||
certifiedMail: false,
|
||||
barcode3800: "",
|
||||
registeredMail: false,
|
||||
registeredMailAmount: false, // can be a number in USD
|
||||
returnReceipt: false,
|
||||
returnReceiptElectronic: false,
|
||||
insurance: false, // can be a number in USD
|
||||
signature: false, // can be false, "SIGNATURE", or "SIGNATURE_RESTRICTED"
|
||||
hazmat: false,
|
||||
liveAnimal: false, // DAY_OLD_POULTRY
|
||||
cod: false, // Collect on Delivery
|
||||
codAmount: false,
|
||||
endorsement: "" // ADDRESS_SERVICE_REQUESTED, CHANGE_SERVICE_REQUESTED, FORWARDING_SERVICE_REQUESTED, LEAVE_IF_NO_RESPONSE, RETURN_SERVICE_REQUESTED
|
||||
};
|
||||
this.specialRateEligibility = false;
|
||||
this.customs = {
|
||||
contents: "",
|
||||
contentsExplanation: "", // needed if contents is "other"
|
||||
signature: "",
|
||||
restriction: "",
|
||||
restrictionComments: "", // needed if restriction is "other"
|
||||
nonDelivery: "return", // "return" or "abandon",
|
||||
items: [] // {index: 0, description: "", qty: "", weight: "", value: "", hscode: "", origin: US"}
|
||||
};
|
||||
this.toAddress = new Address();
|
||||
this.returnAddress = new Address();
|
||||
this.originAddress = new Address();
|
||||
this.trackingNumber = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format as EasyPost shipment object
|
||||
* @returns {Package.toEasyPostShipment.shipment}
|
||||
*/
|
||||
async toEasyPostShipment() {
|
||||
// removed
|
||||
}
|
||||
|
||||
/**
|
||||
* Format as Endicia shipment object
|
||||
* @returns {Package.toSERAShipment.shipment}
|
||||
*/
|
||||
async toSERAShipment() {
|
||||
// removed
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
prepaid: this.prepaid,
|
||||
packaging: this.packaging,
|
||||
extraServices: this.extraServices,
|
||||
specialRateEligibility: this.specialRateEligibility,
|
||||
customs: this.customs,
|
||||
toAddress: this.toAddress,
|
||||
returnAddress: this.returnAddress,
|
||||
originAddress: this.originAddress,
|
||||
trackingNumber: this.trackingNumber
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable summary of size and options.
|
||||
* Does not include address data.
|
||||
* @returns {String}
|
||||
*/
|
||||
async toString() {
|
||||
let summary = [];
|
||||
let packaging = await getPackagingByID(this.packaging.internalid);
|
||||
let weight = ozToLbsOz(this.packaging.weightOz);
|
||||
let weightStr = this.packaging.weightOz >= 16 ? `${weight[0]} lbs ${weight[1]} oz` : `${weight[1]} oz`;
|
||||
if (packaging != false) {
|
||||
if (packaging.weight === false) {
|
||||
summary.push(packaging.name);
|
||||
} else {
|
||||
summary.push(`${weightStr} ${packaging.name}`);
|
||||
}
|
||||
} else {
|
||||
summary.push(weightStr);
|
||||
}
|
||||
if (this.extraServices.liveAnimal) {
|
||||
summary.push("Contains Live Animals");
|
||||
}
|
||||
if (this.extraServices.certifiedMail) {
|
||||
summary.push("Certified Mail");
|
||||
} else if (this.extraServices.registeredMail) {
|
||||
summary.push("Registered Mail");
|
||||
summary.push("Registered for $" + (this.extraServices.registeredMailAmount * 1.0).toFixed(2));
|
||||
} else if (this.extraServices.signature == "SIGNATURE") {
|
||||
summary.push("Signature Required");
|
||||
}
|
||||
if (this.extraServices.signature == "SIGNATURE_RESTRICTED") {
|
||||
summary.push("Restricted Delivery");
|
||||
}
|
||||
if (this.extraServices.returnReceiptElectronic) {
|
||||
summary.push("Return Receipt Electronic");
|
||||
}
|
||||
if (this.extraServices.returnReceipt) {
|
||||
summary.push("Return Receipt");
|
||||
}
|
||||
if (this.extraServices.insurance) {
|
||||
summary.push("Insured for $" + (this.extraServices.insurance * 1.0).toFixed(2));
|
||||
}
|
||||
if (this.extraServices.cod) {
|
||||
summary.push("Collect on Delivery: $" + (this.extraServices.codAmount * 1.0).toFixed(2));
|
||||
}
|
||||
return summary.join("\n");
|
||||
}
|
||||
|
||||
async needsHAZMATPrompt() {
|
||||
try {
|
||||
let packagingInfo = await getPackagingByID(this.packaging.internalid);
|
||||
if (packagingInfo.hazmat) {
|
||||
return true;
|
||||
}
|
||||
if (this.packaging.weight > 10) {
|
||||
return true;
|
||||
}
|
||||
if (packagingInfo.l >= -1 && Math.max(this.packaging.length, this.packaging.width, this.packaging.height) > 0.5) {
|
||||
return true;
|
||||
}
|
||||
switch (packagingInfo.type) {
|
||||
case "Letter":
|
||||
case "Card":
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (ex) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
get isPrepaid() {
|
||||
return this.prepaid == true;
|
||||
}
|
||||
|
||||
setCustomsInfo(contents, contentsExplanation, signature, restriction, restrictionComments, nonDelivery) {
|
||||
let items = this.customs.items; // Save this and copy it back in so we don't overwrite it
|
||||
this.customs = {
|
||||
contents: contents,
|
||||
contentsExplanation: contentsExplanation, // needed if contents is "other"
|
||||
signature: signature,
|
||||
restriction: restriction,
|
||||
restrictionComments: restrictionComments, // needed if restriction is "other"
|
||||
nonDelivery: nonDelivery, // "return" or "abandon",
|
||||
items: items
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customs items, ignoring any that are blank.
|
||||
* @returns {Array}
|
||||
*/
|
||||
getCustomsItems() {
|
||||
let items = [];
|
||||
for (let i = 0; i < this.customs.items.length; i++) {
|
||||
let item = this.customs.items[i];
|
||||
if (item.description == "" && (item.qty == "" || item.qty == 0) && (item.weight == "" || item.weight == 0) && (item.value == "" || item.value == 0)) {
|
||||
continue;
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
setCustomsItems(items) {
|
||||
this.customs.items = items;
|
||||
}
|
||||
|
||||
getCustoms() {
|
||||
this.customs.items = this.getCustomsItems();
|
||||
return this.customs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to automatically fix simple issues like overweight letters.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
async fixIssues() {
|
||||
if (this.packaging.type == "Letter" && this.packaging.weightOz > 3.5) {
|
||||
if (this.packaging.nonmachinable) {
|
||||
return; // Has to be a parcel, can't fix without dimensions
|
||||
}
|
||||
this.packaging.type = "Flat";
|
||||
this.packaging.internalid = 104;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do some basic checks to see if this package is even remotely shippable
|
||||
* @param {boolean} kioskMode If true, returned strings are suitable for display in kiosk mode.
|
||||
* @returns {boolean|string} true if okay, human-readable error message and instructions if not okay
|
||||
*/
|
||||
async isValid(kioskMode = false) {
|
||||
// removed for brevity. Just a bunch of if statements.
|
||||
}
|
||||
|
||||
/**
|
||||
* Set package characteristics
|
||||
* @param {string} type "Parcel", "Letter", "Flat", "Card"
|
||||
* @param {type} service
|
||||
* @param {type} carrier
|
||||
* @param {type} length
|
||||
* @param {type} width
|
||||
* @param {type} height
|
||||
* @param {type} weightOz
|
||||
* @returns {undefined}
|
||||
*/
|
||||
setPackaging(type, service, carrier, length, width, height, weightOz, nonmachinable) {
|
||||
if (typeof nonmachinable == "undefined") {
|
||||
nonmachinable = false;
|
||||
}
|
||||
if (type == "Card") {
|
||||
// Postcards
|
||||
weightOz = 1;
|
||||
this.packaging.internalid = 105;
|
||||
} else if (type == "Flat") {
|
||||
this.packaging.internalid = 104;
|
||||
} else if (type == "Letter") {
|
||||
this.packaging.internalid = 102;
|
||||
if (nonmachinable) {
|
||||
this.packaging.internalid = 103;
|
||||
}
|
||||
}
|
||||
this.packaging.type = type;
|
||||
this.packaging.service = service;
|
||||
this.packaging.carrier = carrier;
|
||||
this.packaging.weightOz = weightOz;
|
||||
this.packaging.nonmachinable = nonmachinable;
|
||||
|
||||
// Enforce Length > Width > Height
|
||||
let size = [length, width, height];
|
||||
size.sort(function (a, b) {
|
||||
return b - a;
|
||||
});
|
||||
this.packaging.length = size[0];
|
||||
this.packaging.width = size[1];
|
||||
this.packaging.height = size[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an extra service
|
||||
* @param {string} id Service ID
|
||||
* @param {boolean} enabled Turn it on or off
|
||||
* @param {string} value Service value, if needed (some are not just a boolean)
|
||||
* @returns {undefined}
|
||||
*/
|
||||
setExtraService(id, enabled, value) {
|
||||
if (typeof value != "undefined" && enabled) {
|
||||
this.extraServices[id] = value;
|
||||
} else {
|
||||
this.extraServices[id] = enabled == true;
|
||||
}
|
||||
}
|
||||
|
||||
getExtraServices() {
|
||||
return this.extraServices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to "MEDIA_MAIL", "LIBRARY_MAIL", or false
|
||||
* @param {type} rate
|
||||
* @returns {undefined}
|
||||
*/
|
||||
set specialRate(rate) {
|
||||
if (rate == "MEDIA") {
|
||||
rate = "MEDIA_MAIL";
|
||||
} else if (rate == "LIBRARY") {
|
||||
rate = "LIBRARY_MAIL";
|
||||
}
|
||||
if (rate != "MEDIA_MAIL" && rate != "LIBRARY_MAIL") {
|
||||
rate = false;
|
||||
}
|
||||
this.specialRateEligibility = rate;
|
||||
}
|
||||
|
||||
get specialRate() {
|
||||
return this.specialRateEligibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an address to this package.
|
||||
* @param {string} type "to", "return", or "origin"
|
||||
* @param {string} name
|
||||
* @param {string} company
|
||||
* @param {string} street1
|
||||
* @param {string} street2
|
||||
* @param {string} city
|
||||
* @param {string} state
|
||||
* @param {string} zip
|
||||
* @param {string} country ISO 2-char country code
|
||||
* @param {string} phone
|
||||
* @param {string} email
|
||||
* @returns {undefined}
|
||||
*/
|
||||
setAddress(type, name, company, street1, street2, city, state, zip, country, phone, email) {
|
||||
let address = Address.fromObject({
|
||||
name: name,
|
||||
company: company,
|
||||
street1: street1,
|
||||
street2: street2,
|
||||
city: city,
|
||||
state: state,
|
||||
zip: zip,
|
||||
country: country,
|
||||
phone: phone,
|
||||
email: email
|
||||
});
|
||||
switch (type) {
|
||||
case "to":
|
||||
this.toAddress = address;
|
||||
break;
|
||||
case "return":
|
||||
this.returnAddress = address;
|
||||
break;
|
||||
case "origin":
|
||||
this.originAddress = address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an address using an object that matches the internal form (see setAddress())
|
||||
* @param {string} type
|
||||
* @param {object} data
|
||||
* @returns {undefined}
|
||||
*/
|
||||
setAddressWhole(type, address) {
|
||||
switch (type) {
|
||||
case "to":
|
||||
this.toAddress = Address.fromObject(address);
|
||||
break;
|
||||
case "return":
|
||||
this.returnAddress = Address.fromObject(address);
|
||||
break;
|
||||
case "origin":
|
||||
this.originAddress = Address.fromObject(address);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
get tracking() {
|
||||
return this.trackingNumber;
|
||||
}
|
||||
|
||||
set tracking(n) {
|
||||
this.trackingNumber = n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "from" address that will be shown,
|
||||
* using the return address or origin address as needed
|
||||
* @returns {address}
|
||||
*/
|
||||
getReturnAddress() {
|
||||
if (typeof this.returnAddress == "object") {
|
||||
return this.returnAddress;
|
||||
}
|
||||
return this.originAddress;
|
||||
}
|
||||
|
||||
getToAddress() {
|
||||
return this.toAddress;
|
||||
}
|
||||
|
||||
getFromAddress() {
|
||||
if (typeof this.originAddress == "object") {
|
||||
return this.originAddress;
|
||||
}
|
||||
return this.returnAddress;
|
||||
}
|
||||
}
|
||||
```
|
253
docs/Receipt.md
Normal file
253
docs/Receipt.md
Normal file
@ -0,0 +1,253 @@
|
||||
# Receipt Objects
|
||||
|
||||
## global.apis.pos.ReceiptItem
|
||||
|
||||
```javascript
|
||||
export class ReceiptItem {
|
||||
/**
|
||||
*
|
||||
* @param {string|number} id Unique ID number for this item (UPC code, inventory number, etc). Used to deduplicate line items. Unique items (like shipping labels) should be random or empty.
|
||||
* @param {string} label One-line item information.
|
||||
* @param {string} text Extra item information.
|
||||
* @param {number} priceEach Price per unit
|
||||
* @param {number} quantity Number of units
|
||||
* @param {number} cost Cost per unit. Used for automatic expense tracking.
|
||||
* @param {number} taxrate Examples: 0 (for 0%), 0.05 (for 5%), etc
|
||||
* @returns {ReceiptItem}
|
||||
*/
|
||||
constructor(id, label, text, priceEach, quantity, cost, taxrate) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
if (text == null) {
|
||||
this.txt == "";
|
||||
} else {
|
||||
this.txt = text;
|
||||
}
|
||||
this.priceEach = num(priceEach);
|
||||
this.qty = num(quantity);
|
||||
this.cost = num(cost);
|
||||
if (isNaN(taxrate)) {
|
||||
this.taxRate = 0;
|
||||
} else {
|
||||
this.taxRate = num(taxrate);
|
||||
}
|
||||
this.merch = false;
|
||||
this.merchid = null;
|
||||
this.surcharge = false;
|
||||
this.retail = 0; // For ensuring PostalPoint fee collection on office mode shipments
|
||||
}
|
||||
|
||||
static fromJSON(obj) {
|
||||
var item = new ReceiptItem(obj.id, obj.label, obj.text, obj.priceEach, obj.qty, obj.cost, obj.taxRate);
|
||||
item.free = obj.free;
|
||||
item.barcode = obj.barcode;
|
||||
item.certifiedInfo = obj.certifiedInfo;
|
||||
item.toAddress = obj.toAddress;
|
||||
item.fromAddress = obj.fromAddress;
|
||||
item.merch = obj.isMerch == true;
|
||||
item.merchid = item.merch ? obj.merchid : null;
|
||||
item.surcharge = obj.surcharge;
|
||||
item.retailPrice = obj.retail;
|
||||
return item;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
label: this.label,
|
||||
text: this.text,
|
||||
priceEach: num(this.priceEach),
|
||||
qty: num(this.qty),
|
||||
cost: num(this.cost),
|
||||
retail: num(this.retail),
|
||||
taxRate: num(this.taxRate),
|
||||
free: this.free,
|
||||
barcode: this.barcode,
|
||||
certifiedInfo: this.certifiedInfo,
|
||||
isMerch: this.merch,
|
||||
merchid: this.merchid,
|
||||
surcharge: this.surcharge,
|
||||
toAddress: this.toAddress,
|
||||
fromAddress: this.fromAddress
|
||||
};
|
||||
}
|
||||
|
||||
get text() {
|
||||
if (typeof this.txt == "string") {
|
||||
return this.txt;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
set text(t) {
|
||||
if (typeof t == "string") {
|
||||
this.txt = t;
|
||||
} else {
|
||||
this.txt = "";
|
||||
}
|
||||
}
|
||||
|
||||
get certifiedInfo() {
|
||||
if (typeof this.certified == "undefined") {
|
||||
return false;
|
||||
}
|
||||
return this.certified;
|
||||
}
|
||||
|
||||
set certifiedInfo(info) {
|
||||
this.certified = info;
|
||||
}
|
||||
|
||||
setCertifiedInfo(tracking, certfee, extrafees, postage, date, location, toaddress) {
|
||||
this.certified = {
|
||||
tracking: tracking,
|
||||
certifiedFee: num(certfee),
|
||||
extraFees: extrafees,
|
||||
postage: num(postage),
|
||||
date: date,
|
||||
location: location,
|
||||
to: toaddress
|
||||
};
|
||||
}
|
||||
|
||||
setQuantity(q) {
|
||||
this.qty = num(q);
|
||||
}
|
||||
|
||||
get free() {
|
||||
return this.isFree == true;
|
||||
}
|
||||
|
||||
set free(free) {
|
||||
this.isFree = free == true;
|
||||
}
|
||||
|
||||
get barcode() {
|
||||
if (typeof this.barcodeData != "string") {
|
||||
return "";
|
||||
}
|
||||
return this.barcodeData;
|
||||
}
|
||||
|
||||
set barcode(data) {
|
||||
this.barcodeData = data;
|
||||
}
|
||||
|
||||
get linePrice() {
|
||||
return round(m(this.priceEach, this.qty), 2);
|
||||
}
|
||||
|
||||
get priceEachFormatted() {
|
||||
return "$" + round(num(this.priceEach), 2).toFixed(2);
|
||||
}
|
||||
|
||||
get linePriceFormatted() {
|
||||
return "$" + round(num(this.linePrice), 2).toFixed(2);
|
||||
}
|
||||
|
||||
get texthtml() {
|
||||
if (typeof this.text != "string") {
|
||||
return "";
|
||||
}
|
||||
var lines = this.text.split("\n");
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].startsWith("Tracking # ")) {
|
||||
// Allow copying tracking number
|
||||
lines[i] = "Tracking # <span class=\"usall\">" + lines[i].replace("Tracking # ", "") + "</span>";
|
||||
}
|
||||
}
|
||||
return lines.join("<br />");
|
||||
}
|
||||
|
||||
get taxAmount() {
|
||||
return round(m(this.linePrice, this.taxRate), 2);
|
||||
}
|
||||
|
||||
get retailPrice() {
|
||||
if (typeof this.retail == "number") {
|
||||
return this.retail;
|
||||
}
|
||||
return this.priceEach * this.qty;
|
||||
}
|
||||
|
||||
set retailPrice(price) {
|
||||
this.retail = num(price);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## global.apis.pos.ReceiptPayment
|
||||
|
||||
```javascript
|
||||
export class ReceiptPayment {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} amount amount paid
|
||||
* @param {string} type payment type
|
||||
* @param {string} text extra data (credit card info, etc)
|
||||
* @returns {ReceiptPayment}
|
||||
*/
|
||||
constructor(amount, type, text) {
|
||||
this.id = (Math.random() * 100000000) + "_" + type + "_" + amount;
|
||||
this.text = (typeof text != "string" ? "" : text);
|
||||
this.type = type;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
static fromJSON(obj) {
|
||||
var item = new ReceiptPayment(obj.amount, obj.type, obj.text);
|
||||
item.id = obj.id;
|
||||
return item;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
amount: round(this.amount, 2),
|
||||
type: this.type,
|
||||
text: this.text,
|
||||
id: this.id
|
||||
};
|
||||
}
|
||||
|
||||
get texthtml() {
|
||||
if (typeof this.text != "string") {
|
||||
return "";
|
||||
}
|
||||
return this.text.replaceAll("\n", "<br />");
|
||||
}
|
||||
|
||||
get amountFormatted() {
|
||||
return "$" + this.amount.toFixed(2);
|
||||
}
|
||||
|
||||
get label() {
|
||||
if (typeof this.type != "string") {
|
||||
return "Payment";
|
||||
}
|
||||
switch (this.type) {
|
||||
case "cash":
|
||||
return "Cash";
|
||||
case "check":
|
||||
return "Check";
|
||||
case "card":
|
||||
return "Card";
|
||||
case "card_manual":
|
||||
return "Card";
|
||||
case "account":
|
||||
return "Account";
|
||||
case "free":
|
||||
return "Free";
|
||||
case "discount":
|
||||
return "Discount";
|
||||
case "crypto":
|
||||
return "Cryptocurrency";
|
||||
case "ach":
|
||||
return "ACH Debit";
|
||||
default:
|
||||
return this.type;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
64
docs/ReceiptPrinter.md
Normal file
64
docs/ReceiptPrinter.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Receipt Printer driver functions
|
||||
|
||||
PostalPoint abstracts the receipt printer hardware commands, so the same functions are available on
|
||||
all brands and languages of receipt printer, and printer media size and settings are also handled for you.
|
||||
|
||||
The drivers operate in line mode, where each successive command appends content to the bottom of the page.
|
||||
|
||||
These functions are available on the object supplied by the promise returned from
|
||||
`global.apis.print.getReceiptPrinter()`.
|
||||
|
||||
## Functions
|
||||
|
||||
```javascript
|
||||
|
||||
//
|
||||
// Add one or more lines of text, with automatic wrapping.
|
||||
// If both firsttext and secondtext are provided, two columns of text are generated,
|
||||
// with the first left-justified and the second right-justified.
|
||||
// `firstjustify` can be "L" (left), "C" (center), or "R" (right).
|
||||
// Not all printers support all the formatting options, and may render them in different ways,
|
||||
// but the formatting intent is made clear regardless.
|
||||
addFieldBlock(firsttext, firstjustify, secondtext = "", secondjustify = "R", bold = false, doubleheight = false, underline = false);
|
||||
|
||||
// Add a blank line to the label.
|
||||
newLine();
|
||||
|
||||
// Draw a horizontal line across the page.
|
||||
drawLine();
|
||||
|
||||
// Render a Code 128 barcode, centered horizontally, with a human-readable label beneath.
|
||||
// Important: this function is sometimes asynchronous depending on the printer driver.
|
||||
barcode128(content);
|
||||
|
||||
// Print an image. Width is in pixels.
|
||||
// pixelByteArray is a Uint8Array where each bit is a pixel (1=black, 0=white),
|
||||
// starting at the top-left of the image and going across and then down. Use `imageToBitmap` to
|
||||
// obtain this data from a Jimp image.
|
||||
// Use "L" as the position to print on the next line, centered horizontally.
|
||||
// Some printers also support position = "C", which will
|
||||
// ignore other commands and print the image centered on the label,
|
||||
// but if you're doing that, just use `global.apis.print.printLabelImage()` instead.
|
||||
drawImage(width, position, pixelByteArray);
|
||||
|
||||
// If supported by the printer, opens an attached cash drawer. Command is ignored if unavailable.
|
||||
openCashDrawer();
|
||||
|
||||
// The last command to run, when ready to print. Returns the raw data to send to the printer.
|
||||
// Important: this function is sometimes asynchronous depending on the printer driver.
|
||||
getData();
|
||||
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```javascript
|
||||
var printer = await global.apis.print.getReceiptPrinter();
|
||||
|
||||
printer.addFieldBlock("Hello Bold World!", "C", "", "", true);
|
||||
printer.drawLine();
|
||||
await printer.barcode128("1234567890");
|
||||
printer.newLine();
|
||||
|
||||
await global.apis.printer.printReceiptData(await printer.getData());
|
||||
```
|
149
docs/TrackingBarcode.md
Normal file
149
docs/TrackingBarcode.md
Normal file
@ -0,0 +1,149 @@
|
||||
# TrackingBarcode class
|
||||
|
||||
For your reference, here is the source code of the TrackingBarcode class, used to represent a prepaid drop-off.
|
||||
This class is provided to plugins as `global.apis.barcode.TrackingBarcode`.
|
||||
|
||||
```javascript
|
||||
export class TrackingBarcode {
|
||||
/**
|
||||
* Create a tracking barcode object.
|
||||
* @param {string} code Tracking number.
|
||||
* @returns {TrackingBarcode}
|
||||
*/
|
||||
constructor(code) {
|
||||
// All data are optional except for the tracking number. Missing data is gracefully handled by the PostalPoint UI.
|
||||
this.cleanCode = code;
|
||||
// Destination ZIP Code, for domestic shipments. The city and state are automatically added. If toAddress is specified, toZip is ignored in favor of it.
|
||||
this.toZip = "";
|
||||
// Two-letter destination country code. If not "US", toZip is ignored, and the full country name is appended to the displayed address information.
|
||||
this.toCountry = "US";
|
||||
// If toAddress is set, it will be used instead of the toZip when displaying the destination.
|
||||
// If both toZip and toAddress are empty strings, no destination will be displayed.
|
||||
this.toAddress = "";
|
||||
// If message is not empty, the barcode will NOT be added and the message will be displayed to the user.
|
||||
this.message = "";
|
||||
// If warning is not empty, the barcode WILL be added and a message will be displayed to the user.
|
||||
this.warning = "";
|
||||
// Shipping carrier name.
|
||||
this.carrier = "";
|
||||
// Shipping service/mail class full name and description. Example: "Priority Mail Adult Signature Required".
|
||||
this.serviceName = "";
|
||||
// Shipping service/mail class name, without extra info such as "signature required".
|
||||
// Example: "Priority Mail"
|
||||
this.serviceShort = "";
|
||||
// If set to false, the barcode will be rejected with a suitable message when PostalPoint is running in self-serve kiosk mode.
|
||||
this.dropoff = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tracking number
|
||||
* @param {string} str
|
||||
* @returns {undefined}
|
||||
*/
|
||||
set tracking(str) {
|
||||
this.cleanCode = str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the service/mail class description string.
|
||||
* @param {string} str
|
||||
* @returns {undefined}
|
||||
*/
|
||||
set service(str) {
|
||||
this.serviceShort = str;
|
||||
this.serviceName = str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tracking number.
|
||||
* @returns {String}
|
||||
*/
|
||||
get tracking() {
|
||||
return this.cleanCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the destination ZIP code.
|
||||
* @returns {String}
|
||||
*/
|
||||
get zip() {
|
||||
return this.toZip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the service/mail class description.
|
||||
* @returns {String}
|
||||
*/
|
||||
get service() {
|
||||
if (this.serviceShort != "") {
|
||||
return this.serviceShort;
|
||||
} else if (this.serviceName != "") {
|
||||
return this.serviceName;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the carrier and service info.
|
||||
* @returns {String}
|
||||
*/
|
||||
get serviceString() {
|
||||
var str = [];
|
||||
if (this.carrier != "") {
|
||||
str.push(this.carrier);
|
||||
}
|
||||
if (this.serviceShort != "") {
|
||||
str.push(this.serviceShort);
|
||||
} else if (this.serviceName != "") {
|
||||
str.push(this.serviceName);
|
||||
}
|
||||
return str.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the destination information as a human-presentable multiline string.
|
||||
* @returns {String}
|
||||
*/
|
||||
get destString() {
|
||||
var addressLines = [];
|
||||
if (this.toAddress != "") {
|
||||
addressLines.push(...this.toAddress.split("\n"));
|
||||
}
|
||||
if (this.toCountry.toUpperCase() == "US" && this.toZip != "" && this.toAddress == "") {
|
||||
var zipdata = getZIP(this.toZip);
|
||||
if (zipdata != false) {
|
||||
addressLines.push(`${zipdata.city} ${zipdata.state} ${this.toZip}`);
|
||||
} else {
|
||||
addressLines.push(`${this.toZip}`);
|
||||
}
|
||||
} else if (this.toCountry.toUpperCase() != "US") {
|
||||
addressLines.push(getCountryNameForISO(this.toCountry));
|
||||
}
|
||||
return addressLines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the package information in a format suitable for display on a receipt.
|
||||
* @param {boolean} includeTrackingNumber If false, the tracking number will be suppressed.
|
||||
* @returns {String}
|
||||
*/
|
||||
toString(includeTrackingNumber = true) {
|
||||
var lines = [];
|
||||
if (includeTrackingNumber && this.cleanCode) {
|
||||
lines.push(this.cleanCode);
|
||||
}
|
||||
var serv = this.serviceString;
|
||||
if (serv != "") {
|
||||
lines.push(serv);
|
||||
}
|
||||
var dest = this.destString;
|
||||
if (dest != "") {
|
||||
var destlines = dest.split("\n");
|
||||
destlines[0] = "To " + destlines[0];
|
||||
lines.push(...destlines);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
```
|
62
plugins/basic-demo/plugin.js
Normal file
62
plugins/basic-demo/plugin.js
Normal file
@ -0,0 +1,62 @@
|
||||
// Sample plugin to demonstrate plugin capabilities and structure.
|
||||
|
||||
async function getPage() {
|
||||
// A Framework7 component page
|
||||
return global.apis.getPluginFolder("basic-demo") + "/uipluginpage.f7";
|
||||
}
|
||||
|
||||
// This is run when PostalPoint loads the plugin at launch.
|
||||
// Use it to register for things you want to do, like adding a page, hooking into payments or shipping rates, etc.
|
||||
exports.init = function () {
|
||||
console.log(global.apis.settings.get("basic-demo_secretcode"));
|
||||
global.apis.ui.addToolsPage(getPage, "Sample Page Title", "sampletool1234", "A sample plugin page", "Sample", "fa-solid fa-circle");
|
||||
};
|
||||
|
||||
// This defines a settings UI to display for the plugin.
|
||||
// If exports.config is a function instead of an array, it will be executed when opening the settings
|
||||
// and must return an array like the one below.
|
||||
// If exports.config is undefined, a settings menu will not be provided to the user.
|
||||
exports.config = [
|
||||
{
|
||||
type: "button",
|
||||
label: "Test Button",
|
||||
text: "Some text about the button",
|
||||
onClick: function () {
|
||||
global.apis.alert("Button pressed");
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
key: "app.postalpoint.basic-demo_somestring", // Try to make sure this is unique by using a prefix,
|
||||
// settings storage is global so there could be conflicts if you aren't careful
|
||||
defaultVal: "",
|
||||
label: "Type a string",
|
||||
placeholder: "",
|
||||
text: "Description text next to the input box"
|
||||
},
|
||||
{
|
||||
type: "password",
|
||||
key: "app.postalpoint.basic-demo_secretcode",
|
||||
defaultVal: "",
|
||||
label: "Secret Code",
|
||||
placeholder: "",
|
||||
text: "Don't tell anyone this secret code:"
|
||||
},
|
||||
{
|
||||
type: "textarea",
|
||||
key: "app.postalpoint.basic-demo_sometext",
|
||||
defaultVal: "",
|
||||
label: "Text Box",
|
||||
placeholder: "...",
|
||||
text: "You can type a few lines of text here."
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
key: "app.postalpoint.basic-demo_dropdownbox",
|
||||
defaultVal: "",
|
||||
label: "Choose an option",
|
||||
placeholder: "",
|
||||
text: "",
|
||||
options: [["key1", "Value 1"], ["key2", "Value 2"]]
|
||||
}
|
||||
];
|
56
plugins/basic-demo/uipluginpage.f7
Normal file
56
plugins/basic-demo/uipluginpage.f7
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="navbar">
|
||||
<div class="navbar-bg"></div>
|
||||
<div class="navbar-inner">
|
||||
<div class="title">${title}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<a class="button" @click=${openAlert}>Open Alert</a>
|
||||
<a class="button" @click=${printSomething}>Print Something</a>
|
||||
<div class="list simple-list">
|
||||
<ul>
|
||||
${names.map((name) => $h`
|
||||
<li>${name}</li>
|
||||
`)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- component styles -->
|
||||
<style>
|
||||
.red-link {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
<!-- rest of component logic -->
|
||||
<script>
|
||||
// script must return/export component function
|
||||
export default (props, { $f7, $on }) => {
|
||||
const title = 'Component Page';
|
||||
const names = ['John', 'Vladimir', 'Timo'];
|
||||
|
||||
const openAlert = () => {
|
||||
$f7.dialog.alert('Hello world!\nblah blah blah');
|
||||
}
|
||||
|
||||
async function printSomething() {
|
||||
// Print some text to the receipt printer
|
||||
var printer = await global.apis.print.getReceiptPrinter();
|
||||
printer.addFieldBlock('Hello world!\nblah blah blah\n\n', "C");
|
||||
global.apis.print.printReceiptData(await printer.getData());
|
||||
}
|
||||
|
||||
$on('pageInit', () => {
|
||||
// do something on page init
|
||||
});
|
||||
$on('pageAfterOut', () => {
|
||||
// page has left the view
|
||||
});
|
||||
|
||||
// component function must return render function
|
||||
return $render;
|
||||
}
|
||||
</script>
|
236
plugins/payment-processor/plugin.js
Normal file
236
plugins/payment-processor/plugin.js
Normal file
@ -0,0 +1,236 @@
|
||||
// This is a sample PostalPoint plugin that adds a card payment processor.
|
||||
|
||||
exports.init = function () {
|
||||
global.apis.pos.registerCardProcessor({
|
||||
name: "Demo Card Processor",
|
||||
init: async function () {
|
||||
// This function runs once after starting PostalPoint
|
||||
// and before any other card processor functions are called.
|
||||
},
|
||||
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 {
|
||||
if (capture) {
|
||||
// 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
|
||||
await global.apis.util.delay(1000); // Replace this with something useful!
|
||||
global.apis.pos.addReceiptPayment(
|
||||
new global.apis.pos.ReceiptPayment(
|
||||
(amount / 100).toFixed(2) * 1,
|
||||
"card", // 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.
|
||||
"Demo Card\nCardholder Name, etc\nMore info for receipt" // Additional text for receipt
|
||||
)
|
||||
);
|
||||
global.apis.pos.addOnscreenPaymentLog("Payment successful!");
|
||||
return true;
|
||||
} else {
|
||||
// only authorize the payment, don't actually capture/charge the payment method,
|
||||
// and return whatever transaction data that will be passed to finishPayment to capture the payment.
|
||||
await global.apis.util.delay(1000); // Replace this with something useful!
|
||||
return {amount: amount};
|
||||
}
|
||||
} catch (ex) {
|
||||
global.apis.pos.addOnscreenPaymentLog(`Error: ${ex.message} [okay to put extra details here for troubleshooting or tech support, it's visible to the cashier]`);
|
||||
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: function () {
|
||||
// The user requested to cancel the payment.
|
||||
// Reset the terminal to its resting state, clear its screen, etc.
|
||||
},
|
||||
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.
|
||||
await global.apis.util.delay(1000); // Replace this with something useful!
|
||||
global.apis.pos.addReceiptPayment(
|
||||
new global.apis.pos.ReceiptPayment(
|
||||
(checkoutResponse.amount / 100).toFixed(2) * 1,
|
||||
"card", // Payment type.
|
||||
"Demo Card\nCardholder Name, etc\nMore info for receipt" // Additional text for receipt
|
||||
)
|
||||
);
|
||||
return true;
|
||||
},
|
||||
updateCartDisplay: function (receipt) {
|
||||
// Show transaction data on the card reader display.
|
||||
// This function will be called when the cart or total changes.
|
||||
console.log(receipt);
|
||||
// Sample structure of the receipt variable:
|
||||
receipt = {
|
||||
"items": [
|
||||
{
|
||||
"id": "testitem",
|
||||
"label": "Test Item",
|
||||
"text": "",
|
||||
"priceEach": 2,
|
||||
"qty": 1,
|
||||
"cost": 0,
|
||||
"retail": 2,
|
||||
"taxRate": 0.1,
|
||||
"free": false,
|
||||
"barcode": "",
|
||||
"certifiedInfo": false,
|
||||
"isMerch": true,
|
||||
"surcharge": false
|
||||
},
|
||||
{
|
||||
"id": "9100123456789012345678",
|
||||
"label": "Test Package",
|
||||
"text": "Package Details\nTracking # 9100 1234 5678 9012 3456 78\nTo:\nTEST PERSON\nORGANIZATION INC\n123 TEST ROAD\nTESTTOWN TE 99999-0001",
|
||||
"priceEach": 8,
|
||||
"qty": 1,
|
||||
"cost": 0,
|
||||
"retail": 8,
|
||||
"taxRate": 0,
|
||||
"free": false,
|
||||
"barcode": "9100123456789012345678",
|
||||
"certifiedInfo": false,
|
||||
"isMerch": false,
|
||||
"surcharge": false,
|
||||
"toAddress": {
|
||||
"name": "TEST PERSON",
|
||||
"company": "ORGANIZATION INC",
|
||||
"street1": "123 TEST ROAD",
|
||||
"street2": null,
|
||||
"city": "TESTTOWN",
|
||||
"state": "TE",
|
||||
"zip": "99999-0001",
|
||||
"email": null,
|
||||
"phone": null,
|
||||
"country": "US"
|
||||
},
|
||||
"fromAddress": {
|
||||
"name": "TEST PERSON",
|
||||
"company": "ORGANIZATION INC",
|
||||
"street1": "123 TEST ROAD",
|
||||
"street2": null,
|
||||
"city": "TESTTOWN",
|
||||
"state": "TE",
|
||||
"zip": "99999-0001",
|
||||
"email": null,
|
||||
"phone": null,
|
||||
"country": "US"
|
||||
}
|
||||
}
|
||||
],
|
||||
"payments": [
|
||||
{
|
||||
"amount": 10,
|
||||
"amountFormatted": "$10.00",
|
||||
"type": "cash",
|
||||
"label": "Cash",
|
||||
"text": "",
|
||||
"texthtml": "",
|
||||
"id": "12345678_cash_10"
|
||||
},
|
||||
{
|
||||
"amount": 12.34,
|
||||
"amountFormatted": "$12.34",
|
||||
"type": "card",
|
||||
"label": "Card",
|
||||
"text": "Card Details here\n1234abcd",
|
||||
"texthtml": "Card Details here<br />1234abcd",
|
||||
"id": "87654321_card_12.34"
|
||||
}
|
||||
],
|
||||
"subtotal": 10,
|
||||
"subtotalFormatted": "$10.00",
|
||||
"tax": 0.2,
|
||||
"taxFormatted": "$0.20",
|
||||
"grandTotal": 10.2,
|
||||
"grandTotalFormatted": "$10.20",
|
||||
"paid": 22.34,
|
||||
"paidFormatted": "$22.34",
|
||||
"due": -12.14, // If negative, is the amount of change owed to the customer instead
|
||||
"dueFormatted": "$12.14"
|
||||
}
|
||||
},
|
||||
checkoutSavedMethod: async function ({cardProcessorCustomerID, paymentMethodID, amount}) {
|
||||
// Same as checkout() except using a payment method already on file.
|
||||
// cardProcessorCustomerID and paymentMethodID are provided by getSavedPaymentMethods below.
|
||||
await global.apis.util.delay(1000); // Replace this with something useful!
|
||||
global.apis.pos.addReceiptPayment(
|
||||
new global.apis.pos.ReceiptPayment(
|
||||
(amount / 100).toFixed(2) * 1,
|
||||
"card", // Payment type.
|
||||
"Card on File\nx1234" // Additional text for receipt
|
||||
)
|
||||
);
|
||||
return true;
|
||||
},
|
||||
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.
|
||||
|
||||
// statusCallback(string, boolean) updates the progress message on the cashier's screen.
|
||||
// If the boolean is true, the progress message is replaced with a confirmation message.
|
||||
statusCallback("Insert the card into the reader.", false);
|
||||
|
||||
await global.apis.util.delay(1000); // Wait for the customer to insert their card,
|
||||
//then save it for later offline billing
|
||||
|
||||
statusCallback("Saving card details...", false);
|
||||
|
||||
await global.apis.util.delay(1000);
|
||||
|
||||
statusCallback("Card saved!", true);
|
||||
|
||||
return true; // Card saved to customer
|
||||
// If an error occurred, you can throw it and the error message will be displayed to the cashier.
|
||||
// Alternatively, return boolean false and display the error yourself with global.apis.alert(message, title) or something.
|
||||
},
|
||||
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.
|
||||
var methods = [];
|
||||
methods.push({
|
||||
customer: "<internal string referencing the customer>", // Passed to checkoutSavedMethod as cardProcessorCustomerID
|
||||
customer_uuid: customerUUID,
|
||||
id: "<card/payment method identifier>", // Passed to checkoutSavedMethod as paymentMethodID
|
||||
type: "card", // Payment type. Accepted values are card, ach, crypto, cash, check, account, and free.
|
||||
label: "Visa debit x1234 (exp. 12/29)", // Label for payment method
|
||||
label_short: "Visa debit x1234" // Abbreviated label for payment method
|
||||
});
|
||||
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 global.apis.util.delay(1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Plugin settings to display.
|
||||
exports.config = [
|
||||
{
|
||||
type: "password",
|
||||
key: "democardprocessor_apikey",
|
||||
defaultVal: "",
|
||||
label: "API Key",
|
||||
placeholder: "",
|
||||
text: "API Key"
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
label: "Test Button",
|
||||
text: "Some text about the button",
|
||||
onClick: function () {
|
||||
global.apis.ui.openSystemWebBrowser("https://postalpoint.app");
|
||||
}
|
||||
}
|
||||
];
|
77
plugins/shipping/plugin.js
Normal file
77
plugins/shipping/plugin.js
Normal file
@ -0,0 +1,77 @@
|
||||
// This is a sample PostalPoint plugin for adding support for a shipping carrier.
|
||||
|
||||
var rateCache = [];
|
||||
var parcelCache = {};
|
||||
|
||||
exports.init = function () {
|
||||
global.apis.shipping.registerRateEndpoint(getRates, purchase, "uniqueprefixhere_");
|
||||
global.apis.barcode.onPrepaidScan(function (barcode) {
|
||||
if (barcode.startsWith("mycarrierbarcode")) { // Replace this with your checks for barcode validity
|
||||
var data = new global.apis.barcode.TrackingBarcode(barcode);
|
||||
data.carrier = "Carrier Name";
|
||||
data.service = "Service Name";
|
||||
return data;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
async function purchase(rateid) {
|
||||
for (var i = 0; i < rateCache.length; i++) {
|
||||
if (rateCache[i].rateid == rateid) {
|
||||
var rate = rateCache[i];
|
||||
//
|
||||
// Fetch label and tracking and such
|
||||
//
|
||||
var label;
|
||||
var tracking = "123456";
|
||||
var toAddressLines = parcelCache.toAddress.toStringArray();
|
||||
|
||||
// Create receipt item
|
||||
var receiptitem = new global.apis.pos.ReceiptItem(`uniqueprefixhere_${tracking}`,
|
||||
`${rate.carrierName} ${rate.serviceName}`,
|
||||
`Tracking # ${global.apis.util.string.chunk(tracking, 3).join(" ")}\nTo:\n${toAddressLines.join("\n")}`,
|
||||
rate.retail_rate, 1, rate.cost_rate, 0
|
||||
);
|
||||
receiptitem.barcode = tracking;
|
||||
|
||||
return {
|
||||
label: label,
|
||||
labeltype: "PNG",
|
||||
receiptItem: receiptitem,
|
||||
tracking: tracking,
|
||||
cost: rate.cost_rate,
|
||||
price: rate.retail_rate,
|
||||
carrier: rate.carrierName,
|
||||
service: rate.serviceName,
|
||||
delivery_days: rate.delivery_days,
|
||||
delivery_date: rate.delivery_date,
|
||||
to: toAddressLines
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getRates(parcel) {
|
||||
// parcel is an object as shown in docs/Parcel.md
|
||||
var rates = [];
|
||||
rates.push({
|
||||
rateid: "uniqueprefixhere_" + global.apis.util.uuid.v4(),
|
||||
carrier: "Carrier",
|
||||
carrierName: "Carrier Name",
|
||||
service: "CARRIER_SERVICE_ID",
|
||||
cost_rate: 10,
|
||||
retail_rate: 15,
|
||||
delivery_days: 3,
|
||||
delivery_date: null,
|
||||
guaranteed: true,
|
||||
serviceName: "Service Name",
|
||||
color: "green" // Rate card color
|
||||
});
|
||||
|
||||
// Save details for later use if purchased
|
||||
rateCache = rates;
|
||||
parcelCache = parcel;
|
||||
|
||||
return rates;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user