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