SambaPOS 5.1.60 Release

Hello. This release contains lots of new features, improvements and fixes. It is tested on production but as always it will be a good idea to ensure you have latest backups before upgrade.

Here is the download link.

  • Rule constraint checking optimized for faster evaluation.
  • German translations added. Thank you @klaus
  • Chinese translations updated.
  • Executing Task Editor commands with Set Widget Value action. Task widget commands are Complete - Toggle - Duplicate>[Task Type] - Change Type>[Task Type] - Up - Down - Left - Right - Refresh.
  • Executing “Toggle” and “Complete” Task editor commands by position by appending position number with # symbol. For example Toggle#2 completes task in second position.
  • ToggleIf and CompleteIf task editor commands added for a kind of toggle confirmation. These works when called with position number and ToggleIf#2 command selects (highlights) second item if not selected and completes item on second execution.
  • Assigning hotkeys to Automation Command Buttons by using Keyboard Map setting. These are virtual keycodes so you need to enter 8 to map command to backspace or 13 to map to enter key.
  • {LOCAL SETTING:X} and {GLOBAL SETTING:X} tags added to let you decide to read from local cache or force database read.
  • {ORDER TAG JSON} tag added to read order’s order tag data in JSON format.
  • Order Tag JSON parameter added to “Add Order” action to set order tag value in JSON format.
  • Duplicate Orders action added to duplicate selected orders in ticket. That action works silently and does not trigger order related events like “Order Added”.
  • Ticket duration reading (T.Duration) added to REPORT TICKET DETAILS custom report.
    Some other tiny fixes for reported issues. Some translation related issues fixed too.
  • {Start} & {End} tags for date filtering added for REPORT SQL DETAILS custom report tag.
  • Duplication issue on Cost reports fixed.
  • Updating mapped portion tags on portion switches.
  • [:DocumentAmount] variable added to Account Transaction Document Created rule.
  • An issue with updating tendered amount with script payment processor fixed.
  • Timeout function added for ask question action, display popup action and prompt dialog.
  • 2 line product name wrapping on menus.
  • <hover> tag added to formatting tags.
  • {PRICE:<price tag>} tag added.
  • {PLAIN TOTAL:<price tag>} tag added.
  • Added a setting to Ask Question action to execute Automation Command in a Background thread.
  • Logging application shutdown errors correctly.
  • Displaying prices on portion buttons for orders with price tags.
  • Some improvements for date selection of Account Details screen.
  • Ticket Explorer date editors changed to calendar type and does not reset end date when start date entered.
  • GraphQL API infrastructure added to message server.
  • A new message server implemented to allow html apps to connect to message server.
  • A web server added to message server to publish html apps.
  • GraphiQL application added to Message server to test GraphQL queries.
  • <timer> formatting tag added to display timers on various screens. Syntax <timer (hh:mm) (due minutes)>Display Format</timer>. Display format is the display format that used in task editor timers. <timer 12:00></timer> will display time passed from 12 o’clock.

On next posts you can find more details about new SambaPOS API. Please keep in mind this feature is currently under development and we’re still adding new features. If you’re interested on that you can help us by sharing your ideas.

4 Likes

##Using GraphQL API

GraphQL is a new REST like API language to allow external applications to access SambaPOS features. For example you can fetch menus, change prices or create tickets to integrate external applications to SambaPOS.

###Sample

OK lets start simple. You can run GraphQL queries from both inside JScript and on Server. Before setting up the server we’ll test API through JScript to have an idea about what it does.

###Executing GraphQL

To execute GraphQL we’ll use gql.Exec(query); JScript function. query parameter will be the query in string format.

return gql.Exec('{getLocalSetting(name:Promotion){name,value}}');

:bulb: gql.Exec() is a new JScript helper function. You can use it in your Automation Scripts to benefit from GraphQL API inside SambaPOS itself.

###GraphQL Query structure

GraphQL queries formatted as a JSON like structure. There are two basic terms to remember. A Query and a Mutation. We’ll use queries to query data and mutations to mutate (change) data.

###Queries
A query have a field name and a result format.

{getGlobalSettings{name,value}}

This query will execute getGlobalSettings function and expects result as name & value format.

If we have 2 global settings stored in database query should return a JSON formatted result.

{  
   "data":{  
      "getGlobalSettings":[  
         {  
            "name":"Promotion",
            "value":"Active"
         },
         {  
            "name":"Printing",
            "value":"Active"
         }
      ]
   }
}

We have two settings named as Promotion and Printing.

#Aliases

You’ll notice query result includes function names and field names as we define them. You can optionally use aliases to change names defined in queries.

{settingNames:getGlobalSettings{setting:name}}

settingNames is an alias for function name name and we want name field named as setting. This is how result appears. In this query we only want names so values does not appear in result.

{  
   "data":{  
      "settingNames":[  
         {  
            "setting":"Promotion"
         },
         {  
            "setting":"Printing"
         }
      ]
   }
}

###Arguments
To query a single setting we’ll use getGlobalSetting function. To be able to query a single setting by name we’ll use arguments. We can define an argument by typing it after function name in parenthesis.

{getGlobalSetting(name:Promotion){name,value}}

We’re executing getGlobalSetting function with name argument. So the result will display setting named as Promotion.

{  
   "data":{  
      "getGlobalSetting":{  
         "name":"Promotion",
         "value":"Active"
      }
   }
}

You’ll notice result contains single result instead of an array on previous example.

This is what we should see when we query a setting named NotExists.

{  
   "data":{  
      "getGlobalSetting":{  
         "name":"NotExists",
         "value":null
      }
   }
}

###Uses in JScript

You can convert this to a JScript function for easy accessing settings from SambaPOS automation features.

function readSetting(settingName)
{
   var response = gql.Exec('{getGlobalSetting(name:'+settingName+'){name,value}}');
   var result = JSON.parse(response);
   return result.data.getGlobalSetting.value;
}

###Mutations

Until now we used shortcut syntax for queries. Full syntax for a query starts with query keyword and a custom name for the query. For example it may start with query myQuery.

query myQuery{getGlobalSettings{name}}

Naming queries is useful while using some advanced techniques to merge results of multiple queries but we won’t cover that for now. We should remember for queries we can skip query keyword and query name.

Mutation is a special kind of query that we use to mutate (change) data. For example to update or delete a global setting we need to use a mutation. Unlike queries we should start it with mutation keyword and give it a name. There is no shortcut syntax for that.

mutation myMutation{
    updateGlobalSetting(name:Promotion,value:Disabled){name,value}
}

This mutation calls updateGlobalSetting function with two arguments. Name and value and changes Promotion setting as Disabled.

updateGlobalSetting(name:Promotion,value:Disabled)

Like queries all mutations should return a result so we also append result query format to the mutation.

{name,value}

So mutation will return resulting name and value.

This is how the mutation should respond after a successful call.

{  
   "data":{  
      "updateGlobalSetting":{  
         "name":"Promotion",
         "value":"Disabled"
      }
   }
}

Another mutation we have can be used to delete settings from database.

mutation m{deleteSetting(name:Promotion){name,value}}

That should also return deleted value as the query result.

{  
   "data":{  
      "deleteSetting":{  
         "name":"Promotion",
         "value":"Disabled"
      }
   }
}

###How you’ll know what to write?

When you enable new Message server it will serve an app called GraphiQL from root path.

You can test your queries here and by clicking Docs button you can browse API documentation.

##Setting Up Server

As you probably know message server is a service that works on server to send notifications to terminals. By 5.1.60 release message server will also respond to GraphQL queries. To enable this feature you need to make some changes on server and on clients.

Standalone message server does not support GraphQL queries so you need to use service application.

###Server Side

Run Samba.MessagingServerServiceTool.exe application to bring up service setup helper tool.

After setting up desired port add “+” sign at the end of port number and click Update Port button.

If it does not start automatically click “Start” button to start service.

When you add “+” sign at the end of port number it disables old message server and starts serving new message server and web server to respond to your queries.

###Setting up client

To connect SambaPOS application to the new message server you also need to make a simple change on local settings page.

While typing server name you need to add “http://” in front of it.

Instead of localhost you need to type server name. For example if your server name is MyServer you need to type http://MyServer as server name. Here you’ll setup server port without adding a + sign.

Be sure your firewalls allows communication through these ports. When firewall prompts allow communication or configure Windows Firewall to open these ports. If you already running message server no additional setup is needed.

On this setup message server allow running queries only on server. If you need to allow access from other devices you need to run Message Server service as a user that have Administrator privileges.

###Testing Setup

You can test it by navigating to localhost:port on your server. GraphiQL application should appear.

If you allowed other terminals to access server by running service as administrator navigating to http://<server ip>:<port>/ URL should also display GraphiQL app.

##Applications

Our new message server also contains a web server to serve your web applications. On SambaPOS installation folder (or where message server executable is running) create a sub folder called /apps/test and place an index.html file in it. When you navigate to that url you should see your index.html running.

PoorMan POS is a simple test application we developed to demonstrate how to access SambaPOS API from web applications. To test PMPos you can teplace the content of index.html with this code.

<!DOCTYPE html>
<html>

<head>
    <script src='https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js'></script>
    <script src='http://ajax.aspnetcdn.com/ajax/signalr/jquery.signalr-2.2.0.min.js'></script>
    <script src="/signalr/hubs"></script>
    <script>
     
$.postJSON = function(url, data, callback) {
    return jQuery.ajax({
    'type': 'POST',
    'url': url,
    'contentType': 'application/json',
    'data': JSON.stringify(data),
    'dataType': 'json',
    'success': callback
    });
};
    
var orders = [];

$(document).ready(function(){
    $('#header').html('<b>PoorMan POS</b>');
    //initialize menu
    updateCategories();
    
    $('#categories').on('click', '.cBtn', function(){
        updateMenuItems(this.innerHTML);
    });
    
    $('#menuItems').on('click', '.mBtn', function(){
        orders.push({name:this.innerHTML,quantity:1,price:this.getAttribute('price')});
        updateOrders();
    });
    
    $('#addTicketButton').click(()=>{
        if(!orders || orders.length == 0) return;
        var tableName = $('#tableNameEditor').val();
        createTicket(orders,tableName);
        updateTableColor(tableName);
        orders = [];
        updateOrders();
        $('#tableNameEditor').val('');
    });
});

function getCategoriesScript(){
    return `{categories: getMenuCategories(menu:"Menu"){id,name}}`;
}

function getMenuItemsScript(category){
    return `{items:getMenuItems(menu:"Menu",category:"${category}"){id,name,product{price}}}`;
}

function getAddTicketScript(orders,tableName){
    
    var orderLines = orders.map(order=>{
       return `{name:"${order.name}",states:[{stateName:"Status",state:"New"}]}`; 
    });
    
    var entityPart = tableName
        ? `entities:[{entityType:"Tables",name:"${tableName}"}],`
        : '';
    
    return `
        mutation m{addTicket(
            ticket:{ticketType:"Ticket",
                department:"Restaurant",
                user:"Administrator",
                terminal:"Server",
                ${entityPart}
                states:[{stateName:"Status",state:"Unpaid"}],
                orders:[${orderLines.join()}]
            }){id}}
    `;
}

function getUpdateTableColorScript(tableName){
    return `
        mutation m{updateEntityState(
            entityTypeName:"Tables",
            entityName:"${tableName}",
            stateName:"Status",
            state:"New Orders"
        ){name}}
    `;
}

function createTicket(orders,tableName){
    var query = getAddTicketScript(orders,tableName);
        $.postJSON('/api/graphql/', {query: query}, function(response) {
        if (response.errors) {
            // handle errors
        } else {                                   
           sendRefreshMessage();
        }
    });
}

function updateTableColor(tableName){
    if(!tableName) return;
    var query = getUpdateTableColorScript(tableName);
        $.postJSON('/api/graphql/', {query: query});
}

function sendRefreshMessage(){
    var query = "mutation m{postTicketRefreshMessage(id:0){id}}";
    $.postJSON('/api/graphql/', {query: query});
}

function updateCategories(){
    var query = getCategoriesScript();
    $.postJSON('/api/graphql/', {query: query}, function(response) {
        if (response.errors) {
            // handle errors
        } else {                        
            $('#categories').empty();            
            response.data.categories.forEach(category=> {
                $('#categories').append(`<li class='cBtn' id='c_${category.id}'>${category.name}</li>`);
            });
            updateMenuItems(response.data.categories[0].name);
        }
    });
}

function updateMenuItems(category){
    var query = getMenuItemsScript(category);
    $.postJSON('/api/graphql/', {query: query}, function(response) {
        if (response.errors) {
            // handle errors
        } else {                      
            $('#menuItems').empty();        
            response.data.items.forEach(item=> {
                $('#menuItems').append(`<li price='${item.product.price}' class='mBtn' id='m_${item.id}'>${item.name}</li>`);
            });
        }
    });
}

function updateOrders(){
    $('#orders').empty();  
    orders.forEach(order=>{
        $('#orders').append(`
            <li>
                <span>${order.quantity}</span>
                <span>${order.name}</span>
                <span>${order.price}</span>
            </li>
        `)
    });
}

    </script>
</head>

<body>
    <p id='header'>Tooo List</p>
    <p>
        <span>Table Name:</span>
        <input id='tableNameEditor'></input>
        <button id='addTicketButton'>Add Ticket</button>
    </p>
    <p>
        <ul id='categories'></ul>
    </p>
    <p>
        <ul id='menuItems'></ul>
    </p>
    <p>
        <ul id='orders'></ul>
    </p>
</body>

</html>

That will fetch SambaPOS menus and allow adding tickets. You can run it next to SambaPOS to test how it works. You’ll notice the top part is menu categories, middle part is menu items and third part is added orders.

I hardcoded few stuff in source code so if your menu name, ticket type names or department names are different from default ones you need to edit source code to fix them.

Please note API primarily designed to give access to external applications so we don’t mean to replace SambaPOS client yet but there is an attempt in beta group to test possibilities. I think we’ll have more discussions about that so if you’re interested you can follow & contribute to related topics to discuss possibilities with us.

3 Likes

##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

  1. 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.
  2. 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.
  3. 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.
  4. You’ll setup Phone field as Customer Entity’s primary field. We check customer’s existence by searching with phone number.
  5. 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]
  6. 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

  1. 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.
  2. You can install node.js application from nodejs.org website.
  3. For the project create a folder and create script.js file with source code pasted here.
  4. Run npm init -y and npm install express -S commands under project folder.
  5. 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;
}
6 Likes

Download link takes me to the main sambapos.com page:

2 Likes

The download link address has been corrected in the original post. It should be:

https://sambapos.com/?wpfb_dl=142

2 Likes

Thanks, I just downloaded the file.

Any tutorial for pos, kitchen display, customer display setup with graphql api

1 Like

Doubt it, this is a very new feature and will take time for these to come.

Wow, i will wait until it publish

When I try to enter the POS entity screen on version 60, SambaPOS crashes. I installed and updated while I still had an open ticket and an open Work Period.

Please include the crash report.

I sent the crash report through the Windows Crash Reporting system. If you want, I can replicate the crash again.

Windows crash report? Can you create a backup and PM me the zip file?

Well I just reinstalled version 60 to replicate the crash, but now I am able to go into the POS entity screen. However, the tables are all scrambled.

Edit: And the Message Server won’t connect.

The Entities are not scrambled. They are being shown alphabetically.

2 Likes