##Integrations
This is a sample implementation to integrate Gloria Food service to SambaPOS. Please keep in mind we intend to demonstrate GraphQL API usage here. We’re planning to release integration modules for such services on future releases.
This is a node.js application that checks new orders once in 30 seconds and creates tickets for new orders. It also uses express module to publish a web interface so you can test your setup by creating sample tickets by using http://localhost:3000/test url. This part of application is not needed for production.
###Configuration
On SambaPOS Side
- Sample is based on our Delivery Setup Tutorial. I added an additional ticket lister widget to display tickets which Delivery State is
Unconfirmed
. When a customer orders food for the first time it creates ticket as Unconfirmed so you can call customer and confirm their address. After confirmation further tickets will appear under Waiting Orders
section. It prints tickets to kitchen for confirmed customers immediately. Unconfirmed tickets will print to kitchen on confirmation.
- You’ll need to create an order tag group called
Default
. Modifiers will apply to that group as free tags. If you want to use detailed groups you can map them inside source code.
- You need to have a custom product tag called
Gloria Name
and type product names as they appear on Gloria Food there. SambaPOS will match products by using that custom tag.
- You’ll setup Phone field as Customer Entity’s primary field. We check customer’s existence by searching with phone number.
- You’ll add First Name, Last Name and EMail custom entity fields for customer entity type and setup entity type’s display format as
[First Name] [Last Name]
-
Confirm
Command loads ticket, updates ticket state as Waiting and updates entity’s (Customer) CStatus as Confirmed. Finally it closes ticket.
For running Server application
- You’ll need to create a restaurant in Gloria Food service and obtain a api access key (POLL V2). You can send an e-mail to Gloria Food to receive your key.
- You can install node.js application from nodejs.org website.
- For the project create a folder and create script.js file with source code pasted here.
- Run
npm init -y
and npm install express -S
commands under project folder.
- Run
node server.js
to start application.
var express = require('express');
var https = require('https');
var http = require('http');
var app = express();
var messageServer = 'localhost';
var messageServerPort = 9000;
var gloriaFoodKey = 'xxxxxxxxxxxxxxxxxxxxxx';
var timeout = 30000;
var customerEntityType = "Customers";
var itemTagName = "Gloria Name";
function gql(query, callback) {
console.log(query);
var data = JSON.stringify({ query: query });
var options = {
hostname: messageServer,
path: '/api/graphql',
port: messageServerPort,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
};
var req = http.request(options, (res) => {
res.setEncoding('utf8');
res.on('data', (chunk) => {
callback(JSON.parse(chunk).data);
});
});
req.on('error', (e) => {
console.log(`problem with request: ${e.message}`);
});
req.write(data);
req.end();
}
function readOrders(callback) {
var options = {
hostname: 'pos.gloriafood.com',
path: '/pos/order/pop',
method: 'POST',
headers: {
'Authorization': gloriaFoodKey,
'Accept': 'application/json',
'Glf-Api-Version': '2'
}
};
var req = https.request(options, (res) => {
res.setEncoding('utf8');
res.on('data', (chunk) => {
callback(JSON.parse(chunk));
});
});
req.on('error', (e) => {
console.log(`problem with request: ${e.message}`);
});
req.end();
}
var testData2 = `{
"count": 1,
"orders": [
{
"coupons": [],
"id": 776113,
"restaurant_id": 4172,
"client_id": 188995,
"type": "delivery",
"source": "website",
"sub_total_price": 47.88,
"tax_value": 4.13,
"total_price": 62.41,
"client_first_name": "John",
"client_last_name": "White",
"client_email": "john.brown@sambapos.com",
"client_phone": "222-345 6255",
"pin_skipped": 0,
"restaurant_name": "John's Excellent Pizza",
"restaurant_phone": "+15558964567",
"restaurant_country": "United States of America",
"restaurant_state": "California",
"restaurant_city": "San Francisco",
"restaurant_street": "10 Market Street",
"restaurant_zipcode": "1234678",
"restaurant_latitude": "37.7944872589999",
"restaurant_longitude": "-122.395311999999",
"instructions": null,
"currency": "USD",
"latitude": "37.79448725889753",
"longitude": "-122.395311680426",
"tax_type": "NET",
"tax_name": "Sales Tax",
"fulfill_at": "2016-06-20T13:30:00.000Z",
"pos_system_id": 1,
"restaurant_key": "8yCPCvb3dDo1k",
"api_version": 2,
"payment": "ONLINE",
"client_address": "21 Market Street, San Francisco",
"items": [
{
"id": 1678316,
"name": "DELIVERY_FEE",
"total_item_price": 5,
"price": 5,
"quantity": 1,
"instructions": null,
"type_id": null,
"type": "delivery_fee",
"tax_rate": 0.1,
"tax_value": 0.5,
"parent_id": null,
"cart_discount_rate": 0,
"cart_discount": 0,
"tax_type": "NET",
"item_discount": 0,
"options": []
},
{
"id": 1678317,
"name": "TIP",
"total_item_price": 5.67,
"price": 5.67,
"quantity": 1,
"instructions": null,
"type_id": null,
"type": "tip",
"tax_rate": 0.05,
"tax_value": 0.2702,
"parent_id": null,
"cart_discount_rate": 0,
"cart_discount": 0,
"tax_type": "GROSS",
"item_discount": 0,
"options": []
},
{
"id": 1678322,
"name": "Pizza Margherita",
"total_item_price": 8.2,
"price": 7,
"quantity": 1,
"instructions": "",
"type_id": 58424,
"type": "item",
"tax_rate": 0.07,
"tax_value": 0,
"parent_id": 1678332,
"cart_discount_rate": 0,
"cart_discount": 0,
"tax_type": "NET",
"item_discount": 8.2,
"options": [
{
"id": 1771325,
"name": "Small",
"price": 0,
"group_name": "Size",
"quantity": 1,
"type": "size"
},
{
"id": 1771326,
"name": "Crispy",
"price": 0,
"group_name": "Crust",
"quantity": 1,
"type": "option"
},
{
"id": 1771327,
"name": "Extra mozzarella",
"price": 1.2,
"group_name": "Extra Toppings (Small)",
"quantity": 1,
"type": "option"
}
]
},
{
"id": 1678324,
"name": "Pizza Prosciutto",
"total_item_price": 11.7,
"price": 8,
"quantity": 1,
"instructions": "",
"type_id": 58425,
"type": "item",
"tax_rate": 0.07,
"tax_value": 0.819,
"parent_id": 1678332,
"cart_discount_rate": 0,
"cart_discount": 0,
"tax_type": "NET",
"item_discount": 0,
"options": [
{
"id": 1771331,
"name": "Large",
"price": 2,
"group_name": "Size",
"quantity": 1,
"type": "size"
},
{
"id": 1771332,
"name": "Crispy",
"price": 0,
"group_name": "Crust",
"quantity": 1,
"type": "option"
},
{
"id": 1771333,
"name": "Extra mozzarella",
"price": 1.7,
"group_name": "Extra Toppings (Large)",
"quantity": 1,
"type": "option"
}
]
},
{
"id": 1678331,
"name": "Pizza Prosciutto",
"total_item_price": 8.7,
"price": 8,
"quantity": 1,
"instructions": "",
"type_id": 58425,
"type": "item",
"tax_rate": 0.07,
"tax_value": 0.609,
"parent_id": 1678332,
"cart_discount_rate": 0,
"cart_discount": 0,
"tax_type": "NET",
"item_discount": 0,
"options": [
{
"id": 1771343,
"name": "Small",
"price": 0,
"group_name": "Size",
"quantity": 1,
"type": "size"
},
{
"id": 1771344,
"name": "Fluffy",
"price": 0,
"group_name": "Crust",
"quantity": 1,
"type": "option"
},
{
"id": 1771345,
"name": "Corn",
"price": 0.7,
"group_name": "Extra Toppings (Small)",
"quantity": 1,
"type": "option"
}
]
},
{
"id": 1678332,
"name": "2 + 1 Pizza Special",
"total_item_price": 28.6,
"price": 0,
"quantity": 1,
"instructions": null,
"type_id": 251,
"type": "promo_item",
"tax_rate": 0.07,
"tax_value": 1.3566,
"parent_id": null,
"cart_discount_rate": 0.05,
"cart_discount": 1.02,
"tax_type": "NET",
"item_discount": 8.2,
"options": []
},
{
"id": 1678334,
"name": "Spaghetti Bolognese",
"total_item_price": 18,
"price": 9,
"quantity": 2,
"instructions": "",
"type_id": 58426,
"type": "item",
"tax_rate": 0.07,
"tax_value": 1.197,
"parent_id": null,
"cart_discount_rate": 0.05,
"cart_discount": 0.9,
"tax_type": "NET",
"item_discount": 0,
"options": []
},
{
"id": 1678335,
"name": "Spaghetti Frutti di Mare",
"total_item_price": 12,
"price": 12,
"quantity": 1,
"instructions": "",
"type_id": 58427,
"type": "item",
"tax_rate": 0.07,
"tax_value": 0.798,
"parent_id": null,
"cart_discount_rate": 0.05,
"cart_discount": 0.6,
"tax_type": "NET",
"item_discount": 0,
"options": []
},
{
"id": 1678336,
"name": "5% off total larger than 40$",
"total_item_price": 0,
"price": 0,
"quantity": 1,
"instructions": null,
"type_id": 250,
"type": "promo_cart",
"tax_rate": 0.07,
"tax_value": 0,
"parent_id": null,
"cart_discount_rate": 0.05,
"cart_discount": -2.52,
"tax_type": "NET",
"item_discount": 2.52,
"options": []
}
]
}
]
}`;
app.get('/', function (req, res) {
res.write('Hello World!');
res.end;
});
app.get('/gqltest', function (req, res) {
gql('{getProducts{id,name,price}}', (data) => {
data.getProducts.forEach(x => res.write(`<div>${x.name} $${x.price}</div>`))
res.end();
});
});
app.get('/ctest', function (req, res) {
loadCustomer({ phone: "222-344 1123" }, data => {
res.write(JSON.stringify(data));
res.end();
})
});
app.get('/test', function (req, res) {
processOrders(JSON.parse(testData2));
res.send("Ticket Created! See log for details");
});
app.listen(3000, function () {
console.log('Example app listening on port 3000!');
loop();
});
function loop() {
readOrders((r) => processOrders(r));
setTimeout(loop, timeout);
}
function processOrders(ticket) {
console.log(ticket);
if (ticket.count == 0) return;
ticket.orders.forEach((order) => processOrder(order));
}
function processOrder(order) {
var customer = {
firstName: order.client_first_name,
lastName: order.client_last_name,
email: order.client_email,
phone: order.client_phone,
address: order.client_address,
newCustomer: false
}
loadCustomer(customer, customer => {
loadItems(order.items.map(x => processItem(x)), items => {
createTicket(customer, items, order.fulfill_at, ticketId => {
gql('mutation m {postTicketRefreshMessage(id:0){id}}', () => {
console.log(`Ticket ${ticketId} created...`);
});
});
});
});
}
function loadItems(items, callback) {
var script = getLoadItemsScript(items);
gql(script, data => {
callback(items.filter(item => data[`i${item.id}`][0]).map(item => {
return {
id: item.id,
name: item.name,
sambaName: data[`i${item.id}`][0].name,
price: item.price,
quantity: item.quantity,
options: item.options
}
}));
});
}
function isNewCustomer(customer) {
if (customer.states && customer.states.find(x => x.stateName === 'CStatus')) {
return customer.states.find(x => x.stateName === 'CStatus').state === 'Unconfirmed';
}
return false;
}
function createTicket(customer, items, fulfill_at, callback) {
var newCustomer = isNewCustomer(customer);
gql(getAddTicketScript(items, customer.name, newCustomer, fulfill_at), data => {
console.log(data);
if (newCustomer)
callback(data.addTicket.id);
else printTicketToKitchen(data.addTicket.id, () => callback(data.addTicket.id));
});
}
function printTicketToKitchen(ticketId, callback) {
gql(getKitchenPrintScript(ticketId), callback);
}
function loadCustomer(customer, callback) {
gql(getIsEntityExistsScript(customer), (data) => {
if (!data.isEntityExists) {
createCustomer(customer, callback);
} else getCustomer(customer.phone, callback);
});
}
function createCustomer(customer, callback) {
gql(getAddCustomerScript(customer), (data) => {
gql(getNewCustomerStateScript(customer), () => {
getCustomer(data.addEntity.name, callback);
})
});
}
function getCustomer(customerName, callback) {
gql(getCustomerScript(customerName), (data) => {
callback(data.getEntity);
});
}
function getLoadItemsScript(items) {
var part = items.map(item => `i${item.id}: getProducts(itemTag:{name:"${itemTagName}",value:"${item.name}"}){name} `);
return `{${part}}`;
}
function getCustomerScript(name) {
return `{getEntity(type:"${customerEntityType}",name:"${name}"){name,customData{name,value},states{stateName,state}}}`;
}
function getIsEntityExistsScript(customer) {
return `{isEntityExists(type:"${customerEntityType}",name:"${customer.phone}")}`;
}
function getAddCustomerScript(customer) {
return `
mutation m{addEntity(entity:{
entityType:"${customerEntityType}",name:"${customer.phone}",customData:[
{name:"First Name",value:"${customer.firstName}"},
{name:"Last Name",value:"${customer.lastName}"},
{name:"Address",value:"${customer.address}"},
{name:"EMail",value:"${customer.email}"}
])
{name}
}`;
}
function getNewCustomerStateScript(customer) {
return `mutation m{updateEntityState(entityTypeName:"${customerEntityType}",entityName:"${customer.phone}",state:"Unconfirmed",stateName:"CStatus"){name}}`;
}
function getKitchenPrintScript(ticketId) {
return `mutation m {
executePrintJob(name: "Print Orders to Kitchen Printer", ticketId: ${ticketId},
orderStateFilters: [{stateName: "Status", state: "New"}],
nextOrderStates:[{stateName:"Status",currentState:"New",state:"Submitted"}])
{name}
}`;
}
function GetOrderTags(order) {
if (order.options) {
var options = order.options.map(x => `{tagName:"Default",tag:"${x.name}",price:${x.price},quantity:${x.quantity}}`);
var result = options.join();
return `tags:[${result}],`
}
return "";
}
function GetOrderPrice(order) {
if (order.price > 0)
return `price:${order.price},`;
return "";
}
function getAddTicketScript(orders, customerName, newCustomer, fulfill_at) {
var orderLines = orders.map(order => {
return `{
name:"${order.sambaName ? order.sambaName : order.name}",
quantity:${order.quantity > 0 ? order.quantity : 1},
${GetOrderPrice(order)}
${GetOrderTags(order)}
states:[
{stateName:"Status",state:"New"}
]
}`;
});
var entityPart = customerName
? `entities:[{entityType:"${customerEntityType}",name:"${customerName}"}],`
: '';
return `
mutation m{addTicket(
ticket:{ticketType:"Delivery Ticket",
department:"Restaurant",
user:"Administrator",
terminal:"Server",
${entityPart}
states:[
{stateName:"Status",state:"Unpaid"},
{stateName:"Source",state:"Gloria"},
{stateName:"Delivery",state:"${newCustomer ? 'Unconfirmed' : 'Waiting'}"}
],
tags:[{tagName:"Delivery Minutes",tag:"${Math.ceil(Math.abs(new Date(fulfill_at) - Date.now()) / 60000)}"}],
orders:[${orderLines.join()}]
}){id}}`;
}
function processItem(item) {
var result = {
id: item.id,
name: item.name,
price: item.price,
quantity: item.quantity,
options: item.options.map(x => { return { name: x.name, quantity: x.quantity, price: x.price } })
};
return result;
}