Gloria food integration


#1

So I thought this was impressive for my small restaurant. Here are my numbers using Gloria food since last June.


#2

impressive!!! me too in 2 weeks I should start with gloriafood, here is a small country but I do it more for advertising.
Do you have any advice to give?


#3

Yeah the only thing I really have to stay on top of is good availability. Do not run out if you do run out of something be proactive in disabling it off the menu. Also go with a decent iPad for the tablet. I’ve used both and the iPad edges out the android devices in reliability.

My tablet is attached to a stand with a lock. I enable kiosk mode by starting guided access in IOS that way employees can’t use it for anything but order taking


#4

It will save a list of clients with emails so for advertising you will have great access to do email campaigns the contacts can be exported in csv format.

I use reports to see my top spending clients and email them loyalty promotions by using the generated promo codes they can enter when viewing their shopping carts.

Gloria food has added around $200 a day to my revenue stream. I’ve gained new clients every week. Since starting it last year.


#5

Just curious, how so? What do you mean about reliability? Something failing on the hardware side? Surely this is not a software issue, correct?


#6

Primarily dealing with the ease of using guided access mode to make it function as a kiosk and the app on IOS has had zero issues. The Android app periodically would stop responding.

Mind you it was rare for the Droid app to mess up I used a Lenovo tab 10 up until last month. IPad mini can be found cheap now. I bought one off amazon refurbished for $130.


#7

what do you see that has more impact, the button on the website or on facebook?


#8

Mobile Website is my biggest by far followed by mobile facebook but the shared food booking app is starting to gain

I keep my website basic and mobile friendly. The main focus is the online ordering button. https://www.mooresdairycreme.com

PS. Use pictures in your menu it helps but make sure they look good and don’t look like a 20 year old phone took them.


#9

perfect !, I’m still on the high seas, I have a draft of the site I’m working on

https://ilbrigantepiedimonte.netsons.org/


#10

I would love to have the branded app but $60 a month is not worth it considering all the other methods used.


#11

I enabled online card payments and use Braintree as my processor it was a little complex to get Braintree set up but once I did its worked great.


#12

I have looked at all the online ordering out there and honestly I think Gloria food is the best and the fact it integrates with Sambapos is a huge plus. I really think the Sambapos guys should invest more into making an official integration. Most other pos systems have shitty online ordering experience if they even have one at all


#13

have you managed to integrate your gloriafood account with sambapos ?
if yes , can you show me instructions on how to do it ?


#14

Have you tried searching the forum?
Plenty of Gloria food topics in. Emres basic intergration tutorial.


#15

according to SambasPOS , they are preparing an EXE to easily install gloriafood , it was supposed to be released by end of jan , so i was wondering if they did or not yet .


#16

There is very ;little official sambapos activity on the forum while emre is away so no idea, suggest you contact whoever told you that as not seen any mention of it on the forum (may have missed it though).
Im not sure why they would make an exe though… would have expected maybe a configeration task or is that what you mean?


#17

They released an exe but it’s basically the exact same setup so why pay for that when you can do it for free by following some instructions.

And yes I’ve been using a customized integration for quite a while now. I don’t deliver so I didn’t use the delivery portion of the tutorial.


#18

Hello Kendash, we also dont deliver and really looking at the customised setup without deliverers entity , any possibility of help me out in this thank you in advance


#19

I posted the script I use for my system. I do not use an entity screeen at all with it. I just use the POS likee normal but when online orders come in the tickets pop up. Its the same as someone submitting an order from a terminal.

Let me find thee thread wheree I shared my script.

Here is my script I currently use. It is modified for my needs. It actually uses Order Tags in SambaPOS so you neeed to mirror the Gloriafood modifiers with order tags in SambaPOS or it will not work. I also modified it so it will reflect any promotion discounts from Gloriafood.

var express = require('express');
var request = require('request');
var querystring = require('querystring');

var messageServer = 'localhost';
var messageServerPort = 9000;
var gloriaFoodKey = 'xxxxxxxxxxxxxx';
var serverKey = 'xxxxxxxxxx';
var timeout = 30000;
var customerEntityType = 'Customers';
var itemTagName = 'Gloria Name';
var ticketType = 'Online Ticket';
var departmentName = 'Restaurant';
var userName = 'Administrator';
var terminalName = 'Server';
var printJobName = 'Print Orders to Kitchen Printer';
var miscProductName = 'Misc';
var deliveryFeeCalculation = 'Delivery Service';
var promotionDiscount = 'Discount';
var tipCalculation = 'Tip';
var accessToken = undefined;
var accessTokenExpires = '';

Authorize(loop());

function Authorize(callback) {
    accessToken = undefined;
    var form = { grant_type: 'client_credentials', client_secret: serverKey, client_id: 'gloria' };
    var formData = querystring.stringify(form);
    var contentLength = formData.length;

    request({
        headers: {
            'Content-Length': contentLength,
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        uri: 'http://' + messageServer + ':' + messageServerPort + '/Token',
        body: formData,
        method: 'POST'
    }, function (err, res, body) {
        if (err) {
            console.log('Error while trying to authorize >', err.message);
        }
        else if (res.statusCode === 400) {
            console.log(body);
            if (callback) callback();
        }
        else {
            var result = JSON.parse(body);
            accessToken = result.access_token;
            accessTokenExpires = new Date(result['.expires']);
            if (callback) callback();
        }
    });
}

function gql(query, callback) {
    if (!accessToken) {
        console.log('Valid access Token is needed to execute GQL calls.')
        return;
    }
    var data = JSON.stringify({ query: query });
    request({
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Authorization': 'Bearer ' + accessToken
        },
        uri: 'http://' + messageServer + ':' + messageServerPort + '/api/graphql',
        body: data,
        method: 'POST'
    }, function (err, res, body) {
        if (res.statusCode === 401) {
            console.log('Should Authorize...');
            Authorize(() => gql(query, callback));
        }
        else {
            var data = JSON.parse(body).data;
            if (callback) callback(data);
        }
    });
}

function readTickets(callback) {
    request({
        method: 'POST',
        uri: 'https://pos.gloriafood.com/pos/order/pop',
        headers: {
            'Authorization': gloriaFoodKey,
            'Accept': 'application/json',
            'Glf-Api-Version': '2'
        }
    }, function (err, res, body) {
        if (err) {
            console.log(`problem with request: ${err.message}`);
        } else {
            callback(JSON.parse(body));
        }
    });
}

function loop() {
    if (!accessToken) {
        console.log('There is no valid access token. Skipping...')
        Authorize();
    }
    else if (accessTokenExpires < new Date()) {
        console.log('Access Token Expired. Reauthenticating...');
        Authorize(() => loop());
        return;
    }
    else {
        console.log('Reading Tickets...');
        readTickets((tickets) => processTickets(tickets));
    }
    setTimeout(loop, timeout);
}

function processTickets(tickets) {
    if (tickets.count == 0) return;
    tickets.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 => {
        var services = order.items
           .filter(x => x.type === 'tip' || x.type === 'delivery_fee' || x.type === 'promo_cart')
           .map(x => { return { name: getCalculationName(x.type), amount: Math.abs((x.cart_discount_rate) * 100) || x.price}; }) 
            .filter(x => x.name);
        loadItems(order.items.map(x => processItem(x)), items => {
            createTicket(customer, items, order.instructions, order.fulfill_at, services, order.payment, ticketId => {
                gql('mutation m {postTicketRefreshMessage(id:0){id}}', () => {
                    console.log(`Ticket ${ticketId} created...`);
                });
            });
        });
    });
}

function getCalculationName(name) {
    if (name === 'promo_cart') return promotionDiscount;
    if (name === 'tip') return tipCalculation;
    if (name === 'delivery_fee') return deliveryFeeCalculation;
    return undefined;
}

function loadItems(items, callback) {
    var script = getLoadItemsScript(items);
    gql(script, data => {
        callback(items.filter(x => x.type === 'item').map(item => {
            return {
                id: item.id,
                name: item.name,
                type: item.type,
                sambaName: data[`i${item.id}`][0] ? data[`i${item.id}`][0].name : miscProductName,
                price: item.price,
                quantity: item.quantity,
                instructions: item.instructions,
                options: item.options,
                portions: item.portions
            }
        }));
    });
}

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, instructions, fulfill_at, services, payment, callback) {
    var newCustomer = isNewCustomer(customer);
    gql(getAddTicketScript(items, customer.name, newCustomer, instructions, fulfill_at, services, payment), 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: "${printJobName}", 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:"${x.group_name}",tag:"${x.name}",price:${x.price},quantity:${x.quantity}}`);
        if (order.instructions) {
            options.push(`{tagName:"Default",tag:"Instructions",note:"${order.instructions}"}`);
        }
        var result = options.join();
        return `tags:[${result}],`
    }
    return "";
}

function GetPortions(order) {
    if (order.portions) {
        var portions = order.portions.map(x => `portion:"${x.name}",` );
        var result = portions.join();
        return `${result}`
    } 
    return "";  
}

function GetOrderPrice(order) {
    if(order.portions){
        var price = order.portions.map(x => `price:${Math.abs((x.price) + (order.price))},`);
        var result = price.join();
        return `${result}`
        }
    if (order.price > 0)
        return `price:${order.price},`;
    return "";
    
}

function getAddTicketScript(orders, customerName, newCustomer, instructions, fulfill_at, services, payment) {
    var orderLines = orders.map(order => {
        return `{
            name:"${order.sambaName ? order.sambaName : order.name}",
            menuItemName:"${order.sambaName === miscProductName ? order.name : ''}",
            quantity:${order.quantity > 0 ? order.quantity : 1},
            ${GetPortions(order)}
            ${GetOrderPrice(order)}
            ${GetOrderTags(order)}
            states:[
                {stateName:"Status",state:"Submitted"}
            ]
        }`;
    });

    var entityPart = customerName
        ? `entities:[{entityType:"${customerEntityType}",name:"${customerName}"}],`
        : '';
    var calculationsPart = services
        ? `calculations:[${services.map(x => `{name:"${x.name}",amount:${x.amount}}`).join()}],`
        : '';

    var result = `
        mutation m{addTicket(
            ticket:{type:"${ticketType}",
                department:"${departmentName}",
                user:"${userName}",
                terminal:"${terminalName}",
                note:"${instructions !== null ? instructions : ''}",
                ${entityPart}
                states:[
                    {stateName:"Status",state:"Unpaid"},
                    {stateName:"Source",state:"Gloria"},
                    {stateName:"Payment",state:"${payment}"}
                ],
                tags:[{tagName:"Cook Time Minutes",tag:"${Math.ceil(Math.abs(new Date(fulfill_at) - Date.now()) / 60000)}"}],
                ${calculationsPart}
                orders:[${orderLines.join()}]
            }){id}}`;
    return result;
}

function processItem(item) {
    var result = {
        id: item.id,
        name: item.name,
        type: item.type,
        price: item.price,
        quantity: item.quantity,
        instructions: item.instructions,
        options: item.options.filter(x => x.type === 'option').map(x => { return { group_name: x.group_name, name: x.name, quantity: x.quantity, price: x.price } }),
        portions: item.options.filter(x => x.type === 'size').map(x => { return { name: x.name, price: x.price}})
    };
    return result;
}

#20

In order to get this to work you need to follow the tutorial to setup Node, GQL, and gloriafood.