# -*- coding: utf-8 -*-
import cPickle
import json
import logging
import os
import sys
import threading
import time

from datetime import datetime
from xml.sax import saxutils
from copy import deepcopy
from decimal import Decimal as D, Decimal
from xml.etree import cElementTree as eTree

from api import order_api_util
from mbcontextmessagehandler import MbContextMessageBus
from pos_api import PosService
from typing import (
    Any,
    Dict,
    List,
    Optional,
    Tuple,
    Union
)

import cfgtools
import persistence
import order_api.dto.exception

from persistence import (
    Connection,
    Cursor,
)
from _threadpooltemplate import ThreadPool
from memorycache import CacheManager, InfiniteExpiration
from msgbus import (
    TK_SLCTRL_OMGR_ORDERPICT,
    TK_SLCTRL_OMGR_VOIDORDER,
    TK_SLCTRL_OMGR_RECALLORDER,
    TK_SLCTRL_OMGR_ORDERREOPEN,
    TK_SLCTRL_OMGR_ORDERTOTAL,
    TK_SLCTRL_OMGR_DOTENDER,
    TK_SLCTRL_OMGR_CLEARTENDERS,
    TK_SLCTRL_OMGR_STOREORDER,
    TK_SLCTRL_OMGR_SETCUSTOMPROPERTIES,
    TK_SLCTRL_OMGR_SETCUSTOMPROPERTY,
    TK_SLCTRL_OMGR_UPDPROPERTIES,
    TK_CMP_TERM_NOW,
    TK_SYS_NAK,
    TK_EVT_EVENT,
    TK_SYS_ACK,
    TK_STORECFG_GET,
    TK_CMP_MESSAGE,
    MBException,
    MBEasyContext,
    FM_STRING,
    FM_XML,
    FM_PARAM, TK_SLCTRL_OMGR_RESETCURRORDER,
    MBMessage,
)
from repository import ProductRepository
from syserrors import SE_USERCANCEL, SE_CFGLOAD, SE_MB_ERR, SE_SUCCESS, SE_DBINIT
from systools import sys_log_info, sys_log_exception
from pos_model import (
    OrderStatus,
    POSTypeMapper,
    TableStatusDescr,
    TipType,
)

from ._TableServiceException import (
    MissingFiscalPropertyException,
    OrderException,
    TableServiceException,
)
from service import DiscountService
from util import (
    parse_string_into_int_list,
    set_table_loyalty_to_order,
)

SERVICE_NAME = "TABLEMGR"
SERVICE_TYPE = "TABLEMGR"
EXPORTED_SERVICES = "%s:%s" % (SERVICE_NAME, SERVICE_TYPE)
REQUIRED_SERVICES = "Persistence|StoreWideConfig"
DEFAULT_SPLIT_KEY = "SingleOrder"

NO_TABLE_FOUND = "$NO_TABLE_FOUND"
NO_SESSION_ID_FOUND = "$NO_SESSION_ID_FOUND"
INVALID_TABLE_STATUS = "$INVALID_TABLE_STATUS"
INVALID_PARAMETERS = "$INVALID_PARAMETERS"
ERROR_ABANDON_TABLE = "Error abandoning table {} (POS ID: {})"
UNEXPECTED_EXCEPTION = "Unexpected Exception"
TK_SALE_ORDER_MODIFIED = 10485810
TABLE_EXPECTED_STATUS = "Invalid Table Status: {} / Expecting: {}"

LOYALTY_TABLE_CUSTOMER_INFO = "LOYALTY_TABLE_CUSTOMER_INFO"
LOYALTY_TABLE_CUSTOMER_BENEFITS = "LOYALTY_TABLE_CUSTOMER_BENEFITS"
LOYALTY_TABLE_COUPONS_TO_APPLY_ON_TOTAL = "LOYALTY_TABLE_COUPONS_TO_APPLY_ON_TOTAL"

logger = logging.getLogger("TableManager")
mb_context = None                           # type: Optional[MBEasyContext]
dbd = None                                  # type: persistence.Driver
config = None
store_id = None
store_layout = None
persistence_service = "Persistence"
thread_pool_size = 10
recall_lock = threading.Lock()
order_modified_by_message = False

ORDER_MANAGER_TIMEOUT = 1000000 * 240
products_list_not_charge_tip = []
table_list_cache = None                     # type: Optional[List]
table_list_lock = threading.Lock()
SECTORS = []
OPERATORS_BY_SECTORS = {}


MANAGED_TOKENS = [
    "STORE_PICTURE",
    "LIST_TABLES",
    "TABLE_PICTURE",
    "SERVICE_PICTURE",
    "START_SERVICE",
    "SLICE_SERVICE",
    "ABANDON_SERVICE",
    "SET_CUSTOM_PROPERTY",
    "SET_CUSTOM_PROPERTIES",
    "GET_CUSTOM_PROPERTY",
    "CREATE_ORDER",
    "SET_PARENT_TABLE",
    "SET_CURRENT_ORDER",
    "JOIN_TABLES",
    "MOVE_TABLE",
    "MOVE_TABLE_ORDER",
    "UNLINK_TABLES",
    "TOTAL_SERVICE",
    "REOPEN_SERVICE",
    "CLOSE_SERVICE",
    "RECALL_SERVICE",
    "STORE_SERVICE",
    "REGISTER_SERVICE_TENDER",
    "CLEAR_SERVICE_TENDERS",
    "TABLE_READY4SERVICE",
    "CHANGE_TABLE_USER",
    "CHANGE_NUMBER_OF_SEATS",
    "CHANGE_SERVICE_TIP",
    "VOID_SERVICE_ORDER",
    "TABLE_INFO",
    "MOVE_ORDER_TO_SERVICE",
    "UPDATE_TABLE_TENDERS",
    "MOVE_CHILD_TABLE",
    "GET_SECTORS",
    "GET_OPERATOR_SECTOR_INFO",
    "UPDATE_OPERATOR_SECTOR_INFO",
    "RESET_STORE_LAYOUT",
]

OK_STATES = 'AVAILABLE / WAITING / SEATED / INPROGRESS'


def read_sw_config(key):
    for _ in range(0, 3):
        try:
            return_msg = mb_context.MB_EasySendMessage(
                dest_name="StoreWideConfig",
                token=TK_STORECFG_GET,
                format=FM_PARAM,
                data=key,
            )
            if return_msg.token == TK_SYS_ACK:
                return str(return_msg.data)
        except (Exception,):
            pass

        time.sleep(1)

    raise TableServiceException("Unable to get Store Wide Configuration {%s}" % key)


def read_config():
    global config, store_id, store_layout, persistence_service, thread_pool_size, order_modified_by_message
    try:
        config = cfgtools.read(os.environ["LOADERCFG"])
        thread_pool_size = int(config.find_value("TableManager/ThreadPoolSize") or "10")
        layout = config.find_group("TableManager/StoreLayout")
        store_layout = dict([(t.name, dict([(k.name, k.values[0]) for k in t.keys])) for t in layout.groups])
        persistence_service = (config.find_value("TableManager/PersistenceService") or "Persistence")
        store_id = read_sw_config("Store.Id")

        config_value = config.find_value("TableManager/OrderModifiedByMessage")
        order_modified_by_message = (config_value or 'false').lower() == "true"
    except (Exception,):
        sys_log_exception("Error reading configuration file. Terminating")
        sys.exit(SE_CFGLOAD)


def start_cache_objects():
    global products_list_not_charge_tip
    product_cache_manager = CacheManager(InfiniteExpiration(), new_object_func=_get_products_with_not_charge_tip)
    products_list_not_charge_tip = product_cache_manager.get_cached_object() or []


def _get_products_with_not_charge_tip():
    return ProductRepository(mb_context).get_products_with_not_charge_tip()


def init_msgbus():
    global mb_context
    try:
        mb_context = MBEasyContext(SERVICE_NAME)
        order_api_util.message_bus = MbContextMessageBus(mbcontext=mb_context)
        if SE_SUCCESS != mb_context.MB_EasyWaitStart(EXPORTED_SERVICES, REQUIRED_SERVICES):
            sys_log_info("Terminating Table Service component after 'MB_EasyWaitStart'")
            sys.exit(SE_SUCCESS)
    except MBException:
        sys_log_exception("Message-bus error while initializing the Table Service component. Terminating")
        sys.exit(SE_MB_ERR)


def init_persistence():
    global dbd
    try:
        dbd = persistence.Driver()
    except (Exception,):
        sys_log_exception("Error initializing Table service component persistence. Terminating")
        logger.exception("Error initializing Table service component persistence. Terminating")
        sys.exit(SE_DBINIT)


def get_db_connection(pos_id, read_uncommitted=False):
    db_conn = dbd.open(mb_context, dbname=str(pos_id), service_name=persistence_service.encode('utf-8'), logger=logger)
    db_conn.query("PRAGMA busy_timeout = 30000;")
    if read_uncommitted:
        db_conn.query("PRAGMA read_uncommitted = 1;")

    db_conn.query("PRAGMA journal_mode = WAL;")
    return db_conn


def _make_json(params):
    return "json:{0}".format(json.dumps(params))


def reset_store_layout_setup():
    db_conn = get_db_connection(1)
    params = []
    for k, v in store_layout.items():
        t = deepcopy(v)
        t["id"] = k
        params.append(t)

    logger.info('Before reset store layout')
    db_conn.pquery("resetStoreLayout", PreparedStringTable=_make_json(params))
    logger.info('After reset store layout')

    store_modified_xml = get_store_picture_xml(0, subject="STORE_MODIFIED", event_type="StartingTableService")
    logger.info('Store Modified Xml: {}'.format(store_modified_xml))

    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "StartService", store_modified_xml)


def get_user_session_id(pos_id, user_id, db_conn=None):
    sql = """
        SELECT SessionId
        FROM posctrl.UserSession
        WHERE CloseTime IS NULL AND OperatorId={0}
        ORDER BY OpenTime DESC
        LIMIT 1
    """.format(user_id)

    session_id = None
    db_conn = get_db_connection(pos_id) if db_conn is None else db_conn
    cursor = db_conn.select(sql)
    for row in cursor:
        session_id = row.get_entry(0)

    if session_id is None:
        raise TableServiceException(NO_SESSION_ID_FOUND)

    return session_id


def send_order_message(params, pos_id, token):
    params = "\0".join(str(p) for p in params)
    msg = mb_context.MB_EasySendMessage(
        dest_name="ORDERMGR%d" % int(pos_id),
        token=token,
        format=FM_PARAM,
        data=params,
        timeout=ORDER_MANAGER_TIMEOUT,
    )

    if msg.token != TK_SYS_ACK:
        params = msg.data.split('\0')
        raise OrderException(params[0], "%s: %s" % (params[0], params[1]))

    return msg.data


def create_order(
    pos_id,                     # type: int
    table,                      # type: Dict[str, str]
    session_id,                 # type: str
    must_store_order=False,     # type: Optional[bool]
    blk_notify=False,           # type: Optional[bool]
    additional_info="",         # type: Optional[str]
    order_type="SALE",          # type: Optional[str]
    sale_type="EAT_IN",         # type: Optional[str]
    order_sub_type="",          # type: Optional[str]
    custom_properties=None,     # type: Optional[Dict[str, str]]
):
    # type: (...) -> str

    order_id = None
    pod_type = "TS"

    link_properties = _build_link_properties(
        pos_id=pos_id,
        table=table,
        sale_type=sale_type,
        pod_type=pod_type,
    )
    if custom_properties is None:
        custom_properties = {}
    custom_properties.update(link_properties)

    for retries in range(1, 3):
        try:
            service_period_id = None
            if order_sub_type == "":
                service_period_id = _get_table_service_period_id(
                    pos_id=pos_id,
                    table=table,
                )
            order_service = order_api_util.create_order_service(pos_id=pos_id)
            order = order_service.create_order(
                session_id=session_id,
                sale_type=sale_type,
                pod_type=pod_type,
                order_type=order_type,
                order_sub_type=order_sub_type,
                service_period_id=service_period_id,
                additional_info=additional_info,
                custom_properties=custom_properties,
                blk_notify=blk_notify,
            )
            order_id = order.get("orderId")
            break
        except order_api.dto.exception.OrderTakerException as ex:
            there_is_an_order_open = ex.error_code == 8
            if there_is_an_order_open and retries < 3:
                store_order(pos_id=pos_id)
                continue
            else:
                logger.exception("Error when retrieving order id")
                raise

    if order_id is None:
        raise TableServiceException("$ORDERMGR_COULD_NOT_CREATE_ORDER")

    logger.debug("Created order with id: {}".format(order_id))

    if must_store_order:
        store_order(pos_id, blk_notify=blk_notify)

    return order_id


def _is_tab(table):
    return table["typeDescr"] == "Tab" and table["id"].startswith('TAB')


def _is_seat(
    table   # type: Dict
):
    # type: (...) -> bool

    table_id = table.get("id") or ""
    return table_id.isdigit() and table.get("typeDescr") == "Seat"


def _has_tabs(
    table   # type: Dict
):
    # type: (...) -> bool

    return _is_seat(table) and table.get("serviceChildren")


def _is_available(
    table   # type: Dict
):
    # type: (...) -> bool

    return table.get("status") == "1"


def _change_table_seat_custom_property(
    properties,     # type: Dict
    pos_id,         # type: Any[Union[str, int]]
    order_id,       # type: str
    line_number,    # type: str
    item_id,        # type: str
    level,          # type: str
    part_code       # type: str
):
    # type: (...) -> None

    if not properties:
        return

    xml = '<Properties>'
    for key, value in properties.items():
        try:
            value = str(value)
        except (Exception,):
            value = value.encode('utf-8')

        xml += '<Property key={0} value={1}/>'.format(
            saxutils.quoteattr(str(key)).encode('utf-8'),
            saxutils.quoteattr(value)
        )

    xml += '</Properties>'
    params = ('0', pos_id, order_id, xml, line_number, item_id, level, part_code)

    request_buffer = '\x00'.join((str(p) for p in params))
    msg = mb_context.MB_EasySendMessage(
        dest_name="ORDERMGR%d" % pos_id,
        token=TK_SLCTRL_OMGR_SETCUSTOMPROPERTIES,
        format=FM_PARAM,
        data=request_buffer
    )

    if msg.token == TK_SYS_NAK:
        raise Exception("Error setting table seat custom property")


def _handle_maximum_seats(
    order,          # type: eTree.XML
    pos_id,         # type: int
    maximum_seats,  # type: int
):
    # type: (...) -> None

    sale_lines = order.findall(".//SaleLine")
    for sale_line in sale_lines:
        custom_properties = sale_line.get("customProperties")
        try:
            seat = int(json.loads(custom_properties)['seat']) if 'seat' in json.loads(custom_properties).keys() else -1
        except (Exception,):
            seat = -1
            sale_line_id = "{}.{}.{}".format(
                sale_line.get("lineNumber"),
                sale_line.get("itemId"),
                sale_line.get("partCode"),
            )
            message = "Error while parsing custom properties json of sale_line: {} / order: {}. CustomProp: {}"
            logger.exception(message.format(sale_line_id, order.get("orderId"), custom_properties))

        if seat > maximum_seats:
            custom_properties = {
                "seat": "0"
            }
            _change_table_seat_custom_property(
                properties=custom_properties,
                pos_id=pos_id,
                order_id=order.get('orderId'),
                line_number=sale_line.get("lineNumber"),
                item_id="1",
                level=sale_line.get('level'),
                part_code=sale_line.get("partCode")
            )


def _build_link_properties(
    pos_id,             # type: Any[Union[int, str]]
    table,              # type: Dict
    sale_type=None,     # type: Optional[str]
    pod_type=None,      # type: Optional[str]
):
    # type: (...) -> Dict[str, str]

    pos_type = POSTypeMapper.get(sale_type, pod_type)
    tab_id = parent_id = "clear"

    if _is_tab(table):
        table_name = _get_service_custom_property(pos_id, table["serviceId"], "TAB_ID")
        table_name = table_name["TAB_ID"] if table_name else table["id"]
        tab_id = table_name
        if "sliceParentTableId" in table and table["sliceParentTableId"] is not None:
            parent_id = table["sliceParentTableId"]
    else:
        table_name = table["name"] if table["name"] else table["id"]

    ret = {
        "TAB_ID": tab_id,
        "PARENT_ID": parent_id,
        "POS_TYPE": pos_type,
        "TABLE_ID": table["id"],
        "TABLE_NAME": table_name,
        "TABLE_SEATS": table["serviceSeats"],
        "SERVICE_ID": table["serviceId"]
    }
    if "sector" in table and table["sector"] is not None:
        ret["SECTOR"] = table["sector"]

    return ret


def _link_order_with_table(
    blk_notify,         # type: bool
    custom_properties,  # type: Dict
    order_id,           # type: Any[Union[int, str]]
    pos_id,             # type: Any[Union[int, str]]
    table,              # type: Dict
    sale_type=None,     # type: Optional[str]
    pod_type=None,      # type: Optional[str]
):
    # type: (...) -> None

    try:
        if not sale_type or not pod_type:
            order_xml = eTree.XML(order_picture(pos_id, order_id))
            order = order_xml.find(".//Order")
            sale_type = sale_type if sale_type else order.get("saleTypeDescr")
            pod_type = pod_type if pod_type else order.get("podType")
            maximum_seats = int(table["serviceSeats"])
            _handle_maximum_seats(
                order=order,
                pos_id=pos_id,
                maximum_seats=maximum_seats,
            )

        pos_type = POSTypeMapper.get(sale_type, pod_type)
        properties = eTree.Element("Properties")
        tab_id = parent_id = "clear"

        if _is_tab(table):
            table_name = _get_service_custom_property(pos_id, table["serviceId"], "TAB_ID")
            table_name = table_name["TAB_ID"] if table_name else table["id"]
            tab_id = table_name
            if "sliceParentTableId" in table and table["sliceParentTableId"] is not None:
                parent_id = table["sliceParentTableId"]
        else:
            table_name = table["name"] if table["name"] else table["id"]

        eTree.SubElement(properties, "Property", {"key": "TAB_ID", "value": tab_id})
        eTree.SubElement(properties, "Property", {"key": "PARENT_ID", "value": parent_id})
        eTree.SubElement(properties, "Property", {"key": "POS_TYPE", "value": pos_type})
        eTree.SubElement(properties, "Property", {"key": "TABLE_ID", "value": table["id"]})
        eTree.SubElement(properties, "Property", {"key": "TABLE_NAME", "value": table_name})
        eTree.SubElement(properties, "Property", {"key": "TABLE_SEATS", "value": table["serviceSeats"]})
        eTree.SubElement(properties, "Property", {"key": "SERVICE_ID", "value": table["serviceId"]})

        if "sector" in table and table["sector"] is not None:
            eTree.SubElement(properties, "Property", {"key": "SECTOR", "value": table["sector"]})

        if custom_properties is not None:
            for k, v in custom_properties.items():
                eTree.SubElement(properties, "Property", {"key": k, "value": str(v)})

        set_order_properties(pos_id, blk_notify, order_id, properties)
    except OrderException:
        logger.exception("Error setting Custom Properties. The order {} will be voided".format(order_id))
        void_order(pos_id, order_id, blk_notify=blk_notify)
        raise


def void_order(
    pos_id,             # type: Union[int, str]
    order_id,           # type: Union[int, str]
    abandon="1",        # type: Optional[str]
    blk_notify=True,    # type: Optional[bool]
    order_status=None,  # type: Optional[int]
):
    # type: (...) -> None

    try:
        order_status = str(order_status) if order_status else ""
        params = ("1" if blk_notify else "0", pos_id, "0", order_id, abandon, order_status)
        send_order_message(params, pos_id, TK_SLCTRL_OMGR_VOIDORDER)
    except OrderException:
        order_xml = eTree.XML(order_picture(pos_id, order_id))
        order = order_xml.find(".//Order")
        voided_status = [OrderStatus.VOIDED.name, OrderStatus.ABANDONED.name, OrderStatus.SYSTEM_VOIDED.name]
        if order.get("state") not in voided_status:
            raise


def tender_order(
    pos_id,                 # type: int
    tender_id,              # type: str
    amount,                 # type: str
    do_not_close,           # type: bool
    order_tender_id="",     # type: Optional[str]
    tip_amount="",          # type: Optional[str]
    tender_detail="",       # type: Optional[str]
    blk_notify=False,       # type: Optional[bool]
):
    # type: (...) -> Optional[eTree.Element]

    blk_notify = "0" if not blk_notify else "1"
    params = (
        blk_notify,
        pos_id,
        tender_id,
        amount,
        1 if do_not_close else 0,
        order_tender_id,
        tip_amount,
        tender_detail,
    )

    response = send_order_message(params, pos_id, TK_SLCTRL_OMGR_DOTENDER)
    data = response.split('\0')
    if data[2]:
        return eTree.XML(data[2])


def clear_tender(
    pos_id,                      # type: str
    order_id,                    # type: str
    blk_notify=False,            # type: bool
    tender_id="",                # type: str
    order_tender_id="",          # type: str
    keep_tender_records=False,   # type: Optional[bool]
):
    # type: (...) -> None

    params = (
        "1" if blk_notify else "0",
        pos_id,
        tender_id,
        order_tender_id,
        "1" if keep_tender_records else "0",
        order_id
    )
    send_order_message(params, pos_id, TK_SLCTRL_OMGR_CLEARTENDERS)


def recall_order(
    pos_id,                 # type: Optional[int]
    order_id,               # type: Optional[int]
    session_id="",          # type: Optional[str]
    src_pos_id="",          # type: Optional[str]
    blk_notify=False,       # type: Optional[bool]
    service_id=None,        # type: Optional[str]
    notify_ndiscount=True,  # type: Optional[bool]
):
    # type: (...) -> None

    logger.debug("Sending recall to POS: {} / Order Id: {} / Source POS ID: {}".format(pos_id, order_id, src_pos_id))
    try:
        params = ("1" if blk_notify else "0", pos_id, order_id, session_id, src_pos_id, "", notify_ndiscount)
        with recall_lock:
            send_order_message(params, pos_id, TK_SLCTRL_OMGR_RECALLORDER)
    except OrderException:
        logger.warning("Error calling TK_SLCTRL_OMGR_RECALLORDER", exc_info=True)

        order_picture_xml = eTree.XML(order_picture(pos_id))
        order = order_picture_xml.find(".//Order")
        if order is None:
            order = order_picture_xml
        if order.get("state") == "IN_PROGRESS":
            if str(order.get("orderId")) == str(order_id):
                return

            void_order(pos_id, "", blk_notify=False)
        raise

    db_conn = get_db_connection(pos_id)
    if service_id is not None:
        db_conn.pquery("registerServiceOrder", serviceId=service_id, orderId=order_id, posId=pos_id)

    logger.debug("Sent recall to ORDERMGR")


def total_order(
    pos_id,                 # type: int
    db_conn,                # type: Connection
    blk_notify=False,       # type: Optional[bool]
    notify_ndiscount=True,  # type: Optional[bool]
):
    # type: (...) -> str

    params = ("1" if blk_notify else "0", pos_id, 0, notify_ndiscount)
    updated_order_picture = send_order_message(params, pos_id, TK_SLCTRL_OMGR_ORDERTOTAL)
    updated_order_picture_str = updated_order_picture.split("\0")[-1]

    totaled_order_picture = eTree.XML(updated_order_picture_str)
    order_id = totaled_order_picture.get("orderId")
    state = totaled_order_picture.get("state")
    db_conn.pquery("updateCurrentOrderState", orderId=order_id, state=state)

    return updated_order_picture_str


def reset_current_order(pos_id, blk_notify=False):
    params = ("1" if blk_notify else "0", pos_id)
    send_order_message(params, pos_id, TK_SLCTRL_OMGR_RESETCURRORDER)


def reopen_order(
    pos_id,             # type: int
    blk_notify=False,   # type: Optional[bool]
):
    # type: (...) -> None

    params = ("1" if blk_notify else "0", pos_id, 0)
    try:
        send_order_message(params, pos_id, TK_SLCTRL_OMGR_ORDERREOPEN)
    except OrderException as ex:
        if ex.get_code() != 6:  # Order already Opened
            raise


def store_order(
    pos_id,                 # type: int
    db_conn=None,           # type: Optional[Connection]
    blk_notify=False        # type: Optional[bool]
):
    # type: (...) -> None

    params = ("1" if blk_notify else "0", pos_id)
    try:
        response = send_order_message(
            params=params,
            pos_id=pos_id,
            token=TK_SLCTRL_OMGR_STOREORDER,
        )

        try:
            order_xml = eTree.XML(response.split('\0')[-1])
            order_id = order_xml.get("orderId")
            state = order_xml.get("state")
            if db_conn is None:
                db_conn = get_db_connection(pos_id=pos_id)
            db_conn.pquery(
                procname="updateCurrentOrderState",
                orderId=order_id,
                state=state,
            )
        except (TypeError, AttributeError):
            pass
    except OrderException as ex:
        if ex.get_code() != 6:  # Order already Exists
            raise


def set_order_properties(pos_id, blk_notify=False, order_id="", properties=None):
    params = ("1" if blk_notify else "0", pos_id, order_id, eTree.tostring(properties, encoding="utf-8", method="xml"))
    send_order_message(params, pos_id, TK_SLCTRL_OMGR_SETCUSTOMPROPERTIES)
    return True


def set_order_property(
    pos_id,              # type: int
    key,                 # type: str
    value,               # type: str
    order_id,            # type: str
    line_number="",      # type: str
    item_id="",          # type: str
    level="",            # type: str
    part_code="",        # type: str
    blk_notify=False,    # type: bool
):
    # type: (...) -> None

    params = (
        "1" if blk_notify else "0",
        pos_id,
        order_id,
        key,
        value,
        line_number,
        item_id,
        level,
        part_code,
    )

    send_order_message(
        params=params,
        pos_id=pos_id,
        token=TK_SLCTRL_OMGR_SETCUSTOMPROPERTY,
    )


def update_order_properties(pos_id, blk_notify=False, order_type="", sale_type="", price_list="", order_id="", tip=""):
    params = (("1" if blk_notify else "0"), pos_id, order_type, sale_type, "", "", "", price_list, "", order_id, tip)
    send_order_message(params, pos_id, TK_SLCTRL_OMGR_UPDPROPERTIES)
    return True


def order_picture(
    pos_id,             # type: int
    order_ids=None,     # type: Optional[Any[Union[int, str, list]]]
    no_cache=False,     # type: Optional[bool]
):

    try:
        logger.info("Order picture: POS ID: {} / ORDER ID: {}".format(pos_id, order_ids))
        if isinstance(order_ids, list):
            order_ids = ";".join([str(i) for i in order_ids])
        logger.info("Order picture: POS ID: {} / ORDER ID: {}".format(pos_id, order_ids))
        params = (
            "" if order_ids is not None else pos_id,
            order_ids or "",
            "true" if no_cache else "false"
        )
        response = send_order_message(params, pos_id, TK_SLCTRL_OMGR_ORDERPICT)

        data = response.split('\0')
        if data[2]:
            return data[2]
    except (Exception,):
        logger.exception("Order picture error: POS ID: {} / ORDER ID: {}".format(pos_id, order_ids))
        raise


def get_parameters(argv, msg, with_table=True):
    pos_id = int(argv[0])
    user_id = int(argv[1]) if len(argv) > 1 and argv[1] else None
    table_id = argv[2] if len(argv) > 2 and argv[2] else None

    parameters = [pos_id, user_id, table_id, msg] if with_table else [pos_id, user_id, msg]
    if not all([x not in [None, ""] for x in parameters]):
        raise TableServiceException(INVALID_PARAMETERS)

    table = None
    if with_table:
        table = _get_table_from_table_id(
            pos_id=pos_id,
            table_id=table_id,
        )

    return pos_id, table, table_id, user_id


def _get_table_from_table_id(
    pos_id,         # type: int
    table_id,       # type: str
    cached=True,    # type: Optional[bool]
):
    # type: (...) -> Dict

    store_tables = handle_list_tables(
        argv=[pos_id, None, None],
        cached=cached,
    )
    pos_table = filter(lambda t: int(t.get("currentPOSId") or "-1") == pos_id, store_tables)
    if len(list(pos_table)) > 1:
        logger.warning("There are more than one table for this POS")
    if pos_table and pos_table[0]['id'] != table_id:
        logger.warning("SERVING_ANOTHER_TABLE : POS TABLE: {} / TABLE_ID: {}".format(pos_table[0]['id'], table_id))
        raise TableServiceException(errmsg="$TABLE_CURRENTLY_SERVING_ANOTHER_TABLE")
    tables = filter(lambda t: t["id"] == table_id, store_tables)
    if not tables:
        raise TableServiceException(errmsg=NO_TABLE_FOUND)
    table = tables[0]
    return table


def get_parameters_extended(argv, msg):
    pos_id = int(argv[0])
    user_id = int(argv[1]) if len(argv) > 1 and argv[1] else None
    from_table_id = argv[2] if len(argv) > 2 and argv[2] else None
    dest_table_id = argv[3] if len(argv) > 3 and argv[3] else None

    parameters = [pos_id, user_id, from_table_id, dest_table_id, msg]
    if not all([x not in [None, ""] for x in parameters]):
        raise TableServiceException(INVALID_PARAMETERS)

    tables = filter(lambda t: (t["id"] in (from_table_id, dest_table_id)), handle_list_tables([pos_id], cached=True))
    if len(list(tables)) != 2:
        raise TableServiceException(NO_TABLE_FOUND)

    from_table = tables[0] if tables[0]["id"] == from_table_id else tables[1]
    dest_table = tables[0] if tables[0]["id"] == dest_table_id else tables[1]

    return pos_id, user_id, from_table, dest_table


def _list_services(
    pos_id,         # type: int
    services_id,    # type: List[int]
):
    # type: (...) -> List[Dict]

    db_conn = get_db_connection(
        pos_id=pos_id,
        read_uncommitted=True,
    )

    services_id_list = []
    for service_id in services_id:
        services_id_list.append({'serviceId': service_id})

    cursor = db_conn.pselect(
        procname="listServices",
        withProps=1,
        PreparedStringTable=_make_json(params=services_id_list),
    )

    services = _parse_table_cols(cursor=cursor)

    return services


def _parse_table_cols(
    cursor,    # type: Cursor
):
    # type: (...) -> List[Dict]

    json_cols = _get_json_cols()
    services = [
        dict(
            [
                (
                    cursor.get_name(i), row.get_entry(i)
                    if cursor.get_name(i) not in json_cols
                    else (json.loads(row.get_entry(i)) if row.get_entry(i) else None)
                ) for i in range(cursor.cols())
            ]
        ) for row in cursor
    ]
    return services


def handle_list_tables(argv, msg=None, db_conn=None, cached=True):
    # type: (...) -> Optional[List[dict]]

    global table_list_cache
    global table_list_lock

    pos_id = int(argv[0])
    user_id = int(argv[1]) if len(argv) > 1 and argv[1] else None
    table_id = argv[2] if len(argv) > 2 and argv[2] else None

    if table_list_cache is None or cached is False:
        table_list = _get_table_list(db_conn, pos_id)
        with table_list_lock:
            _update_cache(pos_id, table_list)
    else:
        with table_list_lock:
            table_list = table_list_cache

    table_list = _filter_table_list(table_list, table_id, user_id)
    if msg:
        msg.token = TK_SYS_ACK
        mb_context.MB_ReplyMessage(msg, FM_STRING, json.dumps(table_list))
        return None

    return cPickle.loads(cPickle.dumps(table_list, -1))


def _update_cache(pos_id, table_list):
    global table_list_cache

    if table_list_cache is not None:
        pos_tables_cache = filter(lambda t: t["currentPOSId"] not in [None, "", " "], table_list_cache)
        for table_cache in pos_tables_cache:
            if int(table_cache["currentPOSId"]) != int(pos_id):
                cur_table = filter(lambda t: t["id"] == table_cache["id"], table_list)[0]
                cur_table["currentPOSOrderID"] = table_cache["currentPOSOrderID"]
                cur_table["currentPOSOrderState"] = table_cache["currentPOSOrderState"]

    table_list_cache = table_list


def _update_table_list_cache(pos_id, db_conn):
    handle_list_tables([pos_id, None, None], db_conn=db_conn, cached=False)


def _filter_table_list(table_list, table_id, user_id):
    if table_id is not None:
        table_list = [x for x in table_list if x.get("id") == str(table_id)]

    if user_id is not None:
        table_list = [x for x in table_list if x.get("userId") == str(user_id) and x.get("serviceId") is not None]

    return table_list


def _get_table_list(db_conn, pos_id):
    if db_conn is None:
        db_conn = get_db_connection(pos_id, True)

    cursor = db_conn.pselect("listTables", withProps=1)
    table_list = _parse_table_cols(cursor=cursor)
    table_list.extend(_get_tables_without_service(db_conn))

    return table_list


def _get_json_cols():
    json_cols = [
        'orders',
        'linkedTables',
        'properties',
        'splitOrders',
        'slicedServices',
        'serviceChildren'
    ]

    return json_cols


def _get_tables_without_service(
        db_conn     # type: Connection
):
    # type: (...) -> List[Dict]

    result_obj = []
    cursor = db_conn.pselect("getTablesWithoutService")
    for row in cursor:
        result_obj.append({
            "sector": row.get_entry("sector"),
            "subSector": row.get_entry("subSector"),
            "splitOrders": None,
            "typeDescr": row.get_entry("typeDescr"),
            "currentPOSId": None,
            "finishTS": None,
            "startTS": None,
            "sliceSourceTableId": None,
            "parentLinkedTableId": row.get_entry("parentLinkedTableId"),
            "lastUpdateTS": None,
            "serviceSeats": None,
            "seats": row.get_entry("seats"),
            "orders": None,
            "calculatedTip": None,
            "parentLinkedTableName": row.get_entry("parentLinkedTableName"),
            "statusDescr": row.get_entry("statusDescr"),
            "id": row.get_entry("id"),
            "currentOrderId": None,
            "linkedTables": None,
            "totalAmount": None,
            "type": row.get_entry("type"),
            "status": row.get_entry("status"),
            "tipAmount": "",
            "currentPOSOrderID": None,
            "userId": None,
            "currentPOSOrderState": None,
            "serviceId": None,
            "totalPerSeat": None,
            "properties": None,
            "tipRate": "",
            "slicedServices": None,
            "name": row.get_entry("name"),
            "businessPeriod": None,
            "serviceChildren": None
        })

    return result_obj


def _add_services_children(
        table,              # type: eTree.XML
        services_children,  # type: Dict[str, str]
        closed_tabs=None    # type: Optional[List[str]]
):
    # type: (...) -> None

    if not services_children:
        return

    temp_children = eTree.SubElement(table, "ServiceChildren")
    for child in services_children:
        dict_child = dict(filter(lambda i: i[1] is not None, child.items()))
        if closed_tabs and dict_child["tableId"] in closed_tabs:
            continue

        filtered_child_dict = {str(key): str(value) for key, value in dict_child.items()}
        child_properties = json.loads(filtered_child_dict.pop("properties") or "{}")
        dict_properties = dict(filter(lambda i: i[1] is not None, child_properties.items()))
        filtered_child_dict.update(
            {snake_case_to_camel_case(key): str(value) for key, value in dict_properties.items()}
        )
        eTree.SubElement(temp_children, "ServiceChild", filtered_child_dict)


def get_store_picture_xml(pos_id, subject=None, event_type=None, cached=False):
    table_list = handle_list_tables([pos_id, None, None], cached=cached)

    closed_tabs = _get_closed_tabs(table_list=table_list)
    table_list = filter(lambda tb: (int(tb["type"]) == 1 or int(tb["status"]) not in (5, 7)), table_list)

    root = eTree.Element("Event", {"subject": subject, "type": (event_type or subject)})
    tables = eTree.SubElement(root, "Tables")
    for t in table_list:
        orders = t.pop("orders", None)
        linked_tables = t.pop("linkedTables", None)
        t.pop("splitOrders", None)
        sliced_services = t.pop("slicedServices", None)
        properties = t.pop("properties", None)
        services_children = t.pop("serviceChildren", None)
        if orders is not None:
            t["orders"] = ",".join([str(order["orderId"]) for order in orders])
        if linked_tables is not None:
            linked_tables = filter(lambda x: (x not in closed_tabs), linked_tables)
            t["linkedTables"] = ",".join(linked_tables)
        if sliced_services is not None:
            t["slicedTabs"] = ",".join([ss["tableId"] for ss in sliced_services])
        if services_children is not None:
            t["childrenTabs"] = ",".join([ss["tableId"] for ss in services_children])
        table = eTree.SubElement(tables, "Table", dict(filter(lambda i: i[1] is not None, t.items())))
        if properties is not None:
            eTree.SubElement(table, "Properties", dict(filter(lambda i: i[1] is not None, properties.items())))

        _add_services_children(
            table=table,
            services_children=services_children,
            closed_tabs=closed_tabs
        )

    if subject is not None:
        return eTree.tostring(root, encoding="utf-8", method="xml")

    return eTree.tostring(tables, encoding="utf-8", method="xml")


def _get_closed_tabs(
    table_list,     # type: Optional[List[dict]]
):
    # type: (...) -> List[str]

    return [t["id"] for t in filter(lambda tb: (int(tb["type"]) == 2 and int(tb["status"]) in [5, 7]), table_list)]


def _build_custom_properties_dict(
    properties,     # type: dict
):
    # type: (...) -> List[dict]

    parsed_props = []
    for key, value in properties.items():
        if '{' in value and '}' in value:
            value = json.loads(value)
        parsed_props.append({'propKey': key, 'propValue': value})
    return parsed_props


def handle_set_custom_properties(
    argv,                   # type: List[Union[str, int]]
    msg=None,               # type: Optional[MBEasyContext]
    notify_update=True,     # type: Optional[bool]
    db_conn=None,           # type: Optional[Connection]
):
    # type: (...) -> None

    pos_id = int(argv[0])
    properties = json.loads(argv[1]) if len(argv) > 1 and argv[1] else None
    table_id = argv[2] if len(argv) > 2 and argv[2] else None
    service_id = int(argv[3]) if len(argv) > 3 and argv[3] else None
    if not all([pos_id, properties, any([table_id, service_id]), msg]):
        raise TableServiceException(INVALID_PARAMETERS)

    table = []
    if service_id is None:
        table = handle_list_tables([pos_id, None, table_id], cached=True)
        if not table:
            raise TableServiceException(NO_TABLE_FOUND)
        table = table[0]

    custom_props = _build_custom_properties_dict(properties=properties)
    db_conn = get_db_connection(pos_id=pos_id) if db_conn is None else db_conn
    if custom_props:
        db_conn.pquery(
            procname="setServiceCustomProperty",
            serviceId=(service_id or table["serviceId"]),
            PreparedStringTable=_make_json(params=custom_props),
        )

    if not notify_update:
        return

    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)
    store_modified_xml = get_store_picture_xml(
        pos_id=pos_id,
        subject="STORE_MODIFIED",
        event_type="ChangedTableCustomProperties",
    )
    mb_context.MB_EasyEvtSend(
        subject="STORE_MODIFIED",
        type="ChangedTableCustomProperties",
        xml=store_modified_xml,
    )


def handle_set_custom_property(
    argv,                   # type: List[Union[str, int]]
    msg=None,               # type: Optional[MBEasyContext]
    notify_update=True,     # type: Optional[bool]
    db_conn=None            # type: Optional[Connection]
):
    # type: (...) -> None

    pos_id = int(argv[0])
    prop_key = argv[1] if len(argv) > 1 and argv[1] else None
    prop_value = argv[2].decode("utf-8") if len(argv) > 2 and argv[2] else None
    table_id = argv[3] if len(argv) > 3 and argv[3] else None
    service_id = int(argv[4]) if len(argv) > 4 and argv[4] else None
    if not all([pos_id, prop_key, any([table_id, service_id]), msg]):
        raise TableServiceException(INVALID_PARAMETERS)

    table = []
    if service_id is None:
        table = handle_list_tables([pos_id, None, table_id], cached=True)
        if not table:
            raise TableServiceException(NO_TABLE_FOUND)
        table = table[0]

    db_conn = get_db_connection(pos_id) if db_conn is None else db_conn
    db_conn.pquery(
        "setServiceCustomProperty",
        serviceId=(service_id or table["serviceId"]),
        propertyKey=prop_key,
        propertyValue=prop_value,
        PreparedStringTable="json:[]"
    )

    if not notify_update:
        return

    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)
    store_modified_xml = get_store_picture_xml(
        pos_id=pos_id,
        subject="STORE_MODIFIED",
        event_type="ChangedTableCustomProperty",
    )
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "ChangedTableCustomProperty", store_modified_xml)


def _get_service_custom_property(pos_id, service_id, key):
    db_conn = get_db_connection(pos_id)
    cursor = db_conn.pselect(
        "getServiceCustomProperty",
        serviceId=service_id,
        propertyKey=key
    )

    return dict([(row.get_entry(0), row.get_entry(1)) for row in cursor])


def handle_get_custom_property(argv, msg=None):
    pos_id = int(argv[0])
    prop_key = argv[1] if len(argv) > 1 and argv[1] else None
    table_id = argv[2] if len(argv) > 2 and argv[2] else None
    service_id = int(argv[3]) if len(argv) > 3 and argv[3] else None
    if not all([pos_id, any([table_id, service_id])]):
        raise TableServiceException(INVALID_PARAMETERS)

    table = []
    if service_id is None:
        table = handle_list_tables([pos_id, None, table_id], cached=True)
        if not table:
            raise TableServiceException(NO_TABLE_FOUND)
        table = table[0]

    result_obj = _get_service_custom_property(pos_id, service_id or table["serviceId"], prop_key)
    if msg is not None:
        msg.token = TK_SYS_ACK
        mb_context.MB_ReplyMessage(msg, FM_STRING, json.dumps(result_obj))
        return None

    return result_obj


def snake_case_to_camel_case(snake_case):
    # type: (str) -> str

    temp = snake_case.lower().split('_')
    camel_case = temp[0] + ''.join(ele.title() for ele in temp[1:])
    return camel_case


def handle_service_picture(
    argv,   # type: List[Any[Union[str, int]]]
    msg,    # type: MBMessage
):
    # type: (...) -> None

    pos_id = int(argv[0]) if len(argv) > 0 and argv[0] else None
    services_id = parse_string_into_int_list(argv[1]) if len(argv) > 1 and argv[1] else None

    if not services_id or len(services_id) == 0 or pos_id is None:
        raise TableServiceException(INVALID_PARAMETERS)

    service_list = _list_services(
        pos_id=pos_id,
        services_id=services_id,
    )

    services_xml = eTree.Element("TableServices")
    for table in service_list:
        try:
            result_xml = _build_table_picture(
                pos_id=pos_id,
                table=table,
                full=True,
            )
            services_xml.append(result_xml)
        except (Exception,):
            message = "Failed to get service picture for service: {}".format(table["serviceId"])
            logger.exception(message)
            raise TableServiceException(message)

    services_xml.attrib["posId"] = str(pos_id)

    result_xml = eTree.tostring(services_xml, encoding="utf-8", method="xml")
    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg, FM_XML, result_xml)


def handle_table_info(argv, msg=None, subject=None, event_type=None):
    return handle_table_picture(argv, msg, subject, event_type, info_only=True)


def handle_table_picture(
    argv,                   # type: List[Any[Union[str, int]]]
    msg=None,               # type: Optional[MBMessage]
    subject=None,           # type: Optional[str]
    event_type=None,        # type: Optional[str]
    full=False,             # type: Optional[bool]
    info_only=False,        # type: Optional[bool]
    user_id='',             # type: Optional[str]
    no_orders_cache=False,  # type: Optional[bool]
):
    # type: (...) -> Optional[Tuple[Optional[str], Optional[str]]]

    pos_id = int(argv[0])
    table_id = argv[1] if len(argv) > 1 and argv[1] else None
    if pos_id < 0 or table_id is None:
        raise TableServiceException(INVALID_PARAMETERS)

    db_conn = get_db_connection(pos_id, True) if not info_only else None
    table_list = handle_list_tables([pos_id, None, None], db_conn=db_conn, cached=info_only)

    table = _find_table_by_table_id(
        table_id=table_id,
        table_list=table_list,
    )

    result_xml = _build_table_picture(
        pos_id=pos_id,
        table=table,
        db_conn=db_conn,
        full=full,
        info_only=info_only,
        no_orders_cache=no_orders_cache,
    )

    event_xml = None
    if subject is not None:
        session_id = ""
        try:
            if user_id:
                session_id = get_user_session_id(pos_id, user_id, db_conn)
        except (Exception,):
            pass

        params = {
            "subject": subject,
            "type": (event_type or subject),
            "posId": str(pos_id),
            "sessionId": session_id
        }
        event = eTree.Element("Table", params)
        event.append(result_xml)
        event_xml = eTree.tostring(event, encoding="utf-8", method="xml")

    result_xml = eTree.tostring(result_xml, encoding="utf-8", method="xml")
    if msg:
        msg.token = TK_SYS_ACK
        mb_context.MB_ReplyMessage(msg, FM_XML, result_xml)
        return None

    return result_xml, event_xml


def _build_table_picture(
    pos_id,                     # type: int
    table,                      # type: Dict[str, Any]
    db_conn=None,               # type: Optional[Connection]
    full=False,                 # type: Optional[bool]
    info_only=False,            # type: Optional[bool]
    no_orders_cache=False,      # type: Optional[bool]
):
    # type: (...) -> Optional[eTree.Element]

    table_list = handle_list_tables([pos_id, None, None], db_conn=db_conn, cached=info_only)
    closed_tabs = _get_closed_tabs(table_list=table_list)

    linked_tables = table.pop("linkedTables", None)
    if linked_tables is not None:
        linked_tables = filter(lambda x: (x not in closed_tabs), linked_tables)
        table["linkedTables"] = ",".join(linked_tables)

    sliced_services = table.pop("slicedServices", None)
    if sliced_services is not None:
        table["slicedTabs"] = ",".join([ss["tableId"] for ss in sliced_services])

    services_children = table.pop("serviceChildren", None)
    if services_children is not None:
        table["childrenTabs"] = ",".join(
            [ss["tableId"] for ss in services_children if ss["tableId"] not in closed_tabs]
        )

    payment_orders = table.pop("splitOrders", [])
    payment_oder_id = None
    if len(payment_orders or []) > 0:
        payment_oder_id = payment_orders[0]["orderId"]

    all_orders = table["orders"]
    table["serviceOrders"] = sorted(all_orders or [], key=lambda x: int(x["orderId"]))
    table["serviceOrders"] = filter(lambda x: x["orderId"] != payment_oder_id, table["serviceOrders"])

    service_orders = table.pop("serviceOrders", [])

    orders = None
    if _table_is_totaled(table):
        try:
            db_conn = get_db_connection(pos_id) if db_conn is None else db_conn
            if info_only:
                _fill_table_total_info_from_database(db_conn, table)
            else:
                orders = _fill_table_total_info(db_conn, pos_id, table, payment_orders)

            table["orders"] = ",".join([str(o.get("orderId")) for o in payment_orders if "orderId" in o])
        except TableServiceException:
            payment_orders = []
            _reopen_service(pos_id, table)
            table["status"] = "4"

    if full or orders is None:
        if table["statusDescr"] == TableStatusDescr.CLOSED:
            table_orders_ids = [str(o["orderId"]) for o in all_orders if o["orderId"]]
        else:
            table_orders_ids = _filter_service_orders(
                services=services_children,
                service_orders=service_orders
            )

        table["orders"] = ",".join(table_orders_ids)

        if not info_only and table_orders_ids:
            db_conn = get_db_connection(pos_id) if db_conn is None else db_conn
            orders = _fill_table_service_orders(
                db_connection=db_conn,
                pos_id=pos_id,
                service_orders=all_orders,
                table_orders_ids=table_orders_ids,
                no_orders_cache=no_orders_cache
            )

    tenders = []
    if not _is_available(table) and not info_only and payment_orders:
        tenders = _get_services_tenders_from_database(
            pos_id=pos_id,
            service_id=table["serviceId"],
            order_id=payment_orders[-1]["orderId"],
        )

    service_period = _get_table_service_period(
        pos_id=pos_id,
        table=table,
    )

    properties = table.pop("properties", None)
    ret_xml_table = eTree.Element("Table", dict(filter(lambda i: i[1] is not None, table.items())))

    if properties:
        eTree.SubElement(ret_xml_table, "Properties", dict(filter(lambda i: i[1] is not None, properties.items())))

    _add_services_children(
        table=ret_xml_table,
        services_children=services_children,
        closed_tabs=closed_tabs
    )

    if service_period is not None:
        attr = {}
        for key, value in service_period.items():
            attr[key] = str(value)
        eTree.SubElement(ret_xml_table, "ServicePeriod", attr)

    if service_orders:
        temp_orders = eTree.SubElement(ret_xml_table, "ServiceOrders")
        for order in service_orders:
            dict_orders = dict(filter(lambda i: i[1] is not None, order.items()))
            filtered_orders_dict = {str(key): str(value) for key, value in dict_orders.items() if key != "posId"}
            eTree.SubElement(temp_orders, "ServiceOrder", filtered_orders_dict)

    if payment_orders:
        temp_orders = eTree.SubElement(ret_xml_table, "PaymentOrders")
        for order in payment_orders:
            dict_orders = dict(filter(lambda i: i[1] is not None, order.items()))
            filtered_orders_dict = {str(key): str(value) for key, value in dict_orders.items() if key != "posId"}
            eTree.SubElement(temp_orders, "PaymentOrder", filtered_orders_dict)

    if tenders:
        temp_tender = eTree.SubElement(ret_xml_table, "ServiceTenders")
        for tender in tenders:
            eTree.SubElement(temp_tender, "ServiceTender", dict(filter(lambda i: i[1] is not None, tender.items())))

    if orders is not None and not info_only:
        ret_xml_table.append(orders)

    return ret_xml_table


def _find_table_by_table_id(
    table_id,       # type: str
    table_list,     # type: List[Dict[str, Any[Union[str, int]]]]
):
    # type: (...) -> Dict[str, Any[Union[str, int]]]

    current_tables = list(filter(lambda tb: (tb["id"] == table_id), table_list))
    if not current_tables:
        raise TableServiceException(NO_TABLE_FOUND)
    return current_tables[0]


def _filter_service_orders(
    services,       # type: List[Dict[str, Any[Union[str, Dict]]]]
    service_orders  # type: List[Dict[str, Any[Union[str, int]]]]
):
    # type: (...) -> List[str]

    service_dict = {}
    for service in services or []:
        service_dict[service.get("serviceId")] = service

    order_ids = []
    for so in service_orders:
        order_id = so.get("orderId", False)
        if not order_id:
            continue

        if so.get("tableServiceId") in service_dict:
            service = service_dict[so.get("tableServiceId")]
            if service.get("status", '0') == '6':
                if int(service.get("totaledOrderId") or "0") != order_id:
                    continue
            elif int(service.get("totaledOrderId") or "0") == order_id:
                continue

        order_ids.append(str(order_id))

    return order_ids


def _fill_table_total_info_from_database(db_conn, table):
    cursor = db_conn.pselect("getServiceTotals", serviceId=table["serviceId"])

    for row in cursor:
        table["discountAmount"] = row.get_entry(2)
        table["taxTotal"] = row.get_entry(3)
        table["dueAmount"] = row.get_entry(4)
        table["change"] = row.get_entry(5)
        table["totalTender"] = row.get_entry(6)
        table["tip"] = row.get_entry(7)

    return None


def _fill_table_service_orders(
        db_connection,          # type: Connection
        pos_id,                 # type: int
        service_orders,         # type: Dict[str, Any[Union[str, int]]]
        table_orders_ids,       # type: List[str]
        no_orders_cache=False   # type: Optional[bool]
):
    orders = '<Orders>'
    temp_picture_list = ";".join(table_orders_ids)
    orders_pictures = order_picture(
        pos_id=pos_id,
        order_ids=temp_picture_list,
        no_cache=no_orders_cache
    )
    orders_pictures_xml = eTree.XML(orders_pictures).findall('.//Order') if orders_pictures is not None else []
    for order in orders_pictures_xml:
        order_id = order.get("orderId")
        state = order.get("state")
        service_order = [o for o in service_orders if o.get("orderId") == int(order_id)][0]
        if state != str(service_order.get("state")):
            db_connection.pquery("updateCurrentOrderState", orderId=order_id, state=state)
            service_order["state"] = state
        service_order["createdAt"] = order.get("createdAtGMT")

        orders += eTree.tostring(order)

    orders += '</Orders>'
    orders = eTree.XML(orders)
    return orders


def _fill_table_total_info(db_connection, pos_id, table, table_orders):
    def _get_value(xml_object, string):
        return D(xml_object.get(string) or "0.00")

    orders = eTree.Element("Orders")
    due_amount = D(0)
    change_amount = D(0)
    tender_amount = D(0)
    total_tax = D(0)
    total_discount = D(0)
    total_tip = D(0)

    for split_order in table_orders:
        order = _get_totalled_order_picture(db_connection, pos_id, split_order, table)

        order_state = order.get("state")
        order_id = order.get("orderId")
        if order_state != split_order["state"]:
            db_connection.pquery("updateCurrentOrderState", orderId=order_id, state=order_state)
            split_order["state"] = order_state

        total_discount += _get_value(order, "discountAmount")
        total_tax += _get_value(order, "taxTotal")
        total_tip += _get_value(order, "tip")
        order_due_amount = _get_value(order, "dueAmount")

        if order_due_amount < D(0):
            order.set("dueAmount", "0.00")
            order.set("change", str((order_due_amount * D(-1)).quantize(D("0.01"))))
        else:
            due_amount += order_due_amount

        orders.append(order)
        for tender in order.findall("TenderHistory/Tender"):
            change_amount += _get_value(tender, "change")
            tender_amount += _get_value(tender, "tenderAmount")

        if split_order["orderId"] == int(order_id):
            split_order["createdAt"] = order.get("createdAtGMT")

    table["discountAmount"] = str(total_discount.quantize(D("0.01")))
    table["taxTotal"] = str(total_tax.quantize(D("0.01")))
    table["dueAmount"] = str(due_amount.quantize(D("0.01")))
    table["change"] = str(change_amount.quantize(D("0.01")))
    table["totalTender"] = str(tender_amount.quantize(D("0.01")))
    table["tip"] = str(total_tip.quantize(D("0.01")))

    return orders


def _get_totalled_order_picture(db_connection, pos_id, split_order, table):
    order = eTree.XML(order_picture(pos_id, split_order['orderId']))
    order = order.findall(".//Order")[0]
    if split_order['state'] != order.get('state'):
        db_connection.pquery("updateCurrentOrderState", orderId=split_order['orderId'], state=order.get('state'))
        split_order['state'] = order.get('state')

    if split_order['state'] in ('VOIDED', 'ABANDONED'):
        logger.warning("Totaled Order in Invalid State {}".format(split_order['state']))
        raise TableServiceException("Totaled Order in Invalid State")

    if split_order['state'] not in ('TOTALED', 'PAID'):
        recalled = False
        try:
            recall_order(pos_id, split_order['orderId'], blk_notify=True)
            recalled = True
        except (Exception,):
            order = eTree.XML(order_picture(pos_id))
            order_id = int(order.get('orderId'))
            if order_id != split_order['orderId'] or order.get('state') not in ('TOTALED', 'PAID'):
                raise TableServiceException("Totaled Order in Invalid State")

        if recalled:
            total_order(pos_id, db_conn=db_connection, blk_notify=False)
            order = eTree.XML(order_picture(pos_id))
            store_order(
                pos_id=pos_id,
                db_conn=db_connection,
            )
    else:
        current_pos_order_id = table.get("currentPOSOrderID")
        if current_pos_order_id != str(split_order['orderId']):
            db_connection.pquery("updateCurrentServiceOrder", posId=pos_id, orderId=split_order['orderId'])
            update_order_properties(pos_id=pos_id, order_id=split_order['orderId'])

    return order


def _table_is_totaled(table):
    return int(table["status"]) == 6


def handle_start_service(argv, msg=None):
    pos_id, _, table_id, user_id = get_parameters(
        argv=argv,
        msg=msg,
        with_table=False,
    )
    b_period = argv[3] if len(argv) > 3 and argv[3] else None
    num_seats = int(argv[4]) if len(argv) > 4 and argv[4] else None
    custom_props = json.loads(argv[5]) if len(argv) > 5 and argv[5] else {}
    source_table_id = int(argv[6]) if len(argv) > 6 and argv[6] else None

    if not isinstance(custom_props, dict):
        raise TableServiceException(INVALID_PARAMETERS)

    service_status = TableStatusDescr.IN_PROGRESS if custom_props.get("MODE", None) == "tabs" else None
    custom_props = [{'propKey': k, 'propValue': v} for k, v in custom_props.items()]
    typeid = 2 if table_id is None else 1
    if not all([pos_id, user_id, b_period, msg]):
        raise TableServiceException(INVALID_PARAMETERS)

    table = None
    if table_id:
        table = handle_list_tables([pos_id, None, table_id], cached=True)
        table = table[0] if table else None
    if table is not None and int(table["status"]) != 1:
        logger.warning(TABLE_EXPECTED_STATUS.format(table["status"], 'AVAILABLE'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    if table is not None:
        _fix_table_link_if_needed(
            pos_id=pos_id,
            table=table,
        )

    parent_service_id = None
    if source_table_id:
        source_table = handle_list_tables([pos_id, None, source_table_id], cached=True)
        if not source_table:
            raise TableServiceException(NO_TABLE_FOUND)
        parent_service_id = source_table[0]["serviceId"]

    db_conn = get_db_connection(pos_id)
    cursor = db_conn.pselect(
        "startService",
        posId=pos_id,
        userId=user_id,
        businessPeriod=b_period,
        typeId=typeid,
        tableId=table_id,
        numberOfSeats=num_seats,
        srcServiceId=parent_service_id,
        statusDescr=service_status
    )
    result_obj = None
    for row in cursor:
        result_obj = dict([(cursor.get_name(i), row.get_entry(i)) for i in range(cursor.cols())])
        break

    if result_obj and custom_props:
        db_conn.pquery(
            "setServiceCustomProperty",
            serviceId=(result_obj["serviceId"]),
            PreparedStringTable=_make_json(custom_props)
        )

    opened_table_id = result_obj["tableId"]

    table_picture_xml, _ = handle_table_picture(argv=[pos_id, opened_table_id])
    order_id = DiscountService.on_service_open(
        mbcontext=mb_context,
        pos_id=pos_id,
        table_id=opened_table_id,
        is_tab=opened_table_id.startswith("TAB-"),
    )
    if order_id:
        table_picture_xml = _move_order_to_service(
            pos_id=pos_id,
            table_id=opened_table_id,
            order_id=order_id,
        )

    msg.token = TK_SYS_ACK if result_obj else TK_SYS_NAK
    mb_context.MB_ReplyMessage(
        message=msg,
        format=FM_STRING,
        data=table_picture_xml,
    )
    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="StartService")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "StartService", store_modified_xml)


def handle_slice_service(argv, msg=None):
    pos_id = int(argv[0])
    userid = int(argv[1]) if len(argv) > 1 and argv[1] else None
    new_setup = json.loads(argv[2]) if len(argv) > 2 and argv[2] else None
    abandon_table = argv[6].lower() == 'true' if len(argv) > 6 and argv[6] else None

    temp_setup = []
    for s in new_setup:
        order_id, lines = map(s.get, ("originalOrderId", "lineNumbers"))
        temp_setup.extend([dict(zip(("originalOrderId", "lineNumber"), t)) for t in [(order_id, x) for x in lines]])
    if not temp_setup:
        raise TableServiceException(INVALID_PARAMETERS)

    for i, s in enumerate(temp_setup):
        s["sliceLineNumber"] = (i + 1)
    src_table_id = argv[3] if len(argv) > 3 and argv[3] else None
    if not all([pos_id, userid, src_table_id, msg]):
        raise TableServiceException(INVALID_PARAMETERS)

    dst_table_id = argv[4] if len(argv) > 4 and argv[4] else None
    dst_table = None
    typeid = 2
    if dst_table_id is not None:
        dst_table = handle_list_tables([pos_id, None, dst_table_id], cached=True)
        dst_table = dst_table[0] if dst_table else None
        typeid = 1
    if dst_table is not None and int(dst_table["status"]) != 1:
        logger.warning(TABLE_EXPECTED_STATUS.format(dst_table["status"], 'AVAILABLE'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    src_table = handle_list_tables([pos_id, None, src_table_id], cached=True)
    if not src_table:
        raise TableServiceException(NO_TABLE_FOUND)
    src_table = src_table[0]
    if int(src_table["status"]) != 4:
        logger.warning(TABLE_EXPECTED_STATUS.format(src_table["status"], 'INPROGRESS'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    db_conn = get_db_connection(pos_id)
    session_id = get_user_session_id(pos_id, userid, db_conn)
    _recall_orders_from_pos(pos_id, session_id, src_table, db_conn)

    new_order_id = None
    new_service = None
    intrans = False
    has_conn = False
    success = False
    try:
        db_conn.transaction_start()
        has_conn = True
        cursor = db_conn.pselect(
            "startService",
            posId=pos_id,
            userId=userid,
            businessPeriod=src_table["businessPeriod"],
            srcServiceId=src_table["serviceId"],
            typeId=typeid,
            tableId=dst_table_id,
            numberOfSeats="1"
        )
        for row in cursor:
            new_service = dict([(cursor.get_name(i), row.get_entry(i)) for i in range(cursor.cols())])
            break
        if not new_service:
            raise TableServiceException("$UNABLE_TO_SLICE_SERVICE")
        if dst_table is None:
            dst_table = handle_list_tables([pos_id, None, new_service["tableId"]], db_conn=db_conn, cached=False)
            if not dst_table:
                raise TableServiceException("$UNABLE_TO_SLICE_SERVICE")
            dst_table = dst_table[0]
        # create a new order
        new_order_custom_properties = {
            "SOURCE_SERVICE_ID": src_table["serviceId"],
        }
        if dst_table.get("sector"):
            new_order_custom_properties["SECTOR"] = dst_table["sector"]

        new_order_id = create_order(
            pos_id=pos_id,
            table=dst_table,
            session_id=session_id,
            blk_notify=True,
            must_store_order=True,
            order_sub_type="SLICED_SERVICE",
            custom_properties=new_order_custom_properties
        )
        db_conn.query("BEGIN TRANSACTION;")
        intrans = True
        db_conn.pquery("registerServiceOrder", serviceId=dst_table["serviceId"], orderId=new_order_id, posId=pos_id)
        db_conn.pquery(
            "updateTableServiceStatus",
            serviceId=dst_table["serviceId"],
            status=TableStatusDescr.IN_PROGRESS
        )
        db_conn.pquery(
            "sliceServiceOrders",
            serviceId=dst_table["serviceId"],
            orderId=new_order_id,
            PreparedStringTable=_make_json(temp_setup)
        )
        db_conn.pquery("storeService", serviceId=dst_table["serviceId"], posId=pos_id)

        success = True
    finally:
        if intrans:
            db_conn.query("COMMIT TRANSACTION;" if success else "ROLLBACK TRANSACTION;")
        if has_conn:
            db_conn.transaction_end()
        if new_order_id and not success:
            void_order(pos_id, new_order_id)
        if new_service is not None and not success:
            db_conn.pquery(
                "updateTableServiceStatus",
                serviceId=new_service["serviceId"],
                status=TableStatusDescr.CLOSED
            )
        if success:
            _void_empty_orders_from_slice_service(
                pos_id=pos_id,
                db_conn=db_conn,
                service_id=dst_table["serviceId"],
            )

    seat = argv[5] if len(argv) > 5 and argv[5] else None
    msg.token = TK_SYS_ACK if success else TK_SYS_NAK

    src_table_picture = eTree.XML(handle_table_picture(
        argv=[pos_id, src_table_id],
        no_orders_cache=True
    )[0])

    handle_table_picture(
        argv=[pos_id, dst_table["id"]],
        no_orders_cache=True
    )

    _slice_loyalty_properties(
        pos_id=pos_id,
        source_table_picture=src_table_picture,
        destination_table_dict=dst_table,
        seat=seat,
        msg=msg,
    )

    if abandon_table:
        _abandon_table_from_slice(
            pos_id=pos_id,
            user_id=userid,
            table=src_table,
        )

    mb_context.MB_ReplyMessage(
        message=msg,
        format=FM_STRING,
        data=json.dumps(new_service),
    )
    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="SliceService")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "SliceService", store_modified_xml)


def _abandon_table_from_slice(
    pos_id,     # type: int
    user_id,    # type: int
    table,      # type: Dict[str, Any]
):
    # type: (...) -> None

    try:
        _abandon_service(
            pos_id=pos_id,
            table=table,
            user_id=user_id,
        )
    except (Exception,):
        logger.exception(ERROR_ABANDON_TABLE.format(
            table.get("id"),
            pos_id,
        ))


def _slice_loyalty_properties(
    pos_id,                     # type: int
    source_table_picture,       # type: eTree.XML
    destination_table_dict,     # type: Dict
    seat,                       # type: int
    msg,                        # type: MBEasyContext
):
    # type: (...) -> None

    if source_table_picture is None or not destination_table_dict:
        return

    source_table_id = source_table_picture.get("id")
    destination_service_id = destination_table_dict["serviceId"]
    customer_info = source_table_picture.find("Properties").get(LOYALTY_TABLE_CUSTOMER_INFO) or "{}"
    available_benefits = source_table_picture.find("Properties").get(LOYALTY_TABLE_CUSTOMER_BENEFITS) or "{}"
    benefits_to_apply = source_table_picture.find("Properties").get(LOYALTY_TABLE_COUPONS_TO_APPLY_ON_TOTAL) or "{}"

    properties = [
        (LOYALTY_TABLE_CUSTOMER_INFO, customer_info),
        (LOYALTY_TABLE_CUSTOMER_BENEFITS, available_benefits),
        (LOYALTY_TABLE_COUPONS_TO_APPLY_ON_TOTAL, benefits_to_apply),
    ]

    for label, property_str in properties:
        source_property = json.loads(s=property_str)

        if not source_property or seat not in source_property:
            continue

        seat_property = dict(table=source_property.pop(seat))

        if label == LOYALTY_TABLE_CUSTOMER_INFO:
            customer = seat_property.get("table", {}).get("customerInfo", {}).get("customer", {})

            name = customer.get("name", "")
            handle_set_custom_property(
                argv=[pos_id, "CUSTOMER_NAME", name, "", destination_service_id],
                msg=msg,
                notify_update=False,
            )

        seat_property["from_seat"] = seat
        handle_set_custom_property(
            argv=[pos_id, label, json.dumps(obj=seat_property), "", destination_service_id],
            msg=msg,
            notify_update=False,
        )

        handle_set_custom_property(
            argv=[pos_id, label, json.dumps(obj=source_property), source_table_id],
            msg=msg,
            notify_update=False,
        )


def handle_abandon_service(argv, msg=None):
    pos_id, table, _, user_id = get_parameters(
        argv=argv,
        msg=msg,
    )

    _abandon_service(
        pos_id=pos_id,
        table=table,
        user_id=user_id,
    )
    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(message=msg)

    store_modified_xml = get_store_picture_xml(
        pos_id=pos_id,
        subject="STORE_MODIFIED",
        event_type="AbandonService",
        cached=True,
    )
    mb_context.MB_EasyEvtSend(
        subject="STORE_MODIFIED",
        type="AbandonService",
        xml=store_modified_xml,
    )


def _abandon_service(
    pos_id,     # type: int
    table,      # type: Dict[str, Any]
    user_id,    # type: int
):
    # type: (...) -> None

    db_conn = get_db_connection(pos_id=pos_id)

    for order in (table.get("orders") or []):
        if order["state"] not in ('VOIDED', 'ABANDONED'):
            void_order(pos_id, order["orderId"])
            db_conn.pquery("updateCurrentOrderState", orderId=order["orderId"], state="ABANDONED")

    if table.get("serviceChildren") is not None:
        for tab in table.get("serviceChildren"):
            _generate_table_closed_event(
                db_conn=db_conn,
                pos_id=pos_id,
                service_id=tab.get("serviceId"),
                table_id=tab.get("tableId"),
                user_id=user_id,
                abandon=True,
            )

    service_id = table["serviceId"]
    _generate_table_closed_event(
        db_conn=db_conn,
        pos_id=pos_id,
        service_id=service_id,
        table_id=table.get("id"),
        user_id=user_id,
        abandon=True,
    )


def handle_create_order(argv, msg=None):
    pos_id, table, _, user_id = get_parameters(
        argv=argv,
        msg=msg,
    )
    custom_properties = argv[4] if len(argv) > 4 and argv[4] else None
    if custom_properties is not None:
        custom_properties = json.loads(custom_properties)

    db_conn = get_db_connection(pos_id)
    session_id = get_user_session_id(pos_id, user_id, db_conn)

    invalid_status = [
        TableStatusDescr.AVAILABLE,
        TableStatusDescr.LINKED,
        TableStatusDescr.TOTALED,
        TableStatusDescr.CLOSED,
        TableStatusDescr.ABANDONED,
    ]
    if table["statusDescr"] in invalid_status:
        raise TableServiceException(errmsg=INVALID_TABLE_STATUS)

    order_id = create_order(
        pos_id=pos_id,
        table=table,
        session_id=session_id,
        custom_properties=custom_properties,
    )

    _register_order_on_service(
        order_id=order_id,
        pos_id=pos_id,
        table=table,
        db_conn=db_conn,
        user_id=user_id,
    )

    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg, FM_XML, '<Order orderId="{0}"/>'.format(order_id))
    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="CreateOrder")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "CreateOrder", store_modified_xml)


def _register_order_on_service(
    order_id,                       # type: str
    pos_id,                         # type: int
    table,                          # type: Dict[str, Any]
    db_conn=None,                   # type: Optional[Connection]
    unregister_from_table=None,     # type: Optional[Dict[str, Any]]
    user_id=None,                   # type: Optional[int]
):
    # type: (...) -> None

    intrans = False
    success = False
    has_conn = False
    try:
        if not db_conn:
            db_conn = get_db_connection(pos_id)
        has_conn = True

        db_conn.transaction_start()
        intrans = True

        db_conn.query("BEGIN TRANSACTION;")

        order_pos_id = pos_id
        if unregister_from_table is not None:
            orders = filter(lambda o: o.get("orderId") == order_id, unregister_from_table.get("orders"))
            order_pos_id = orders[0].get("posId") if orders else pos_id
            db_conn.pquery(
                procname="unregisterServiceOrder",
                serviceId=unregister_from_table["serviceId"],
                orderId=order_id,
                posId=order_pos_id,
            )
        db_conn.pquery(
            procname="registerServiceOrder",
            serviceId=table["serviceId"],
            orderId=order_id,
            posId=order_pos_id,
            userId=user_id,
        )

        if unregister_from_table is not None:
            db_conn.pquery("updateCurrentOrderState", orderId=order_id, state="STORED")
        else:
            db_conn.pquery("updateCurrentOrderId", posId=pos_id, orderId=order_id)
            db_conn.pquery(
                "updateTableServiceStatus",
                serviceId=table["serviceId"],
                status=TableStatusDescr.IN_PROGRESS
            )
        success = True
    except (Exception,):
        logger.exception(UNEXPECTED_EXCEPTION)
    finally:
        if intrans and db_conn:
            db_conn.query("COMMIT TRANSACTION;" if success else "ROLLBACK TRANSACTION;")
        if has_conn and db_conn:
            db_conn.transaction_end()


def handle_set_parent_table(
        argv,       # type: List[str]
        msg=None    # type: MBEasyContext
):
    # type: (...) -> None

    pos_id = argv[0]
    table_id = argv[2]
    table = handle_list_tables([pos_id, None, table_id], cached=True)[0]
    table_picture = eTree.XML(handle_table_picture([pos_id, table["id"]])[0])
    service_orders = table_picture.findall(".//ServiceOrders/ServiceOrder")
    orders_ids = [order.get('orderId') for order in service_orders]
    property_value = None

    if not table or not table["startTS"] or table["finishTS"]:
        raise TableServiceException(NO_TABLE_FOUND)

    service_id = table["serviceId"]
    parent_table_id = argv[3] if len(argv) > 3 and argv[3] and argv[3] != "None" else None
    from_table_is_tab = argv[4] if len(argv) > 4 and argv[4] and argv[4] != "None" else None

    db_conn = get_db_connection(pos_id)
    if parent_table_id:
        parent_table = handle_list_tables([pos_id, None, parent_table_id], cached=True)[0]
        if not parent_table:
            raise TableServiceException(NO_TABLE_FOUND)
        parent_service_id = parent_table["serviceId"]
        db_conn.pquery(
            "updateParentServiceId",
            serviceId=service_id,
            parentServiceId=parent_service_id
        )
        property_value = parent_table_id

    elif not from_table_is_tab:
        db_conn.pquery(
            "removeParentServiceId",
            serviceId=service_id,
            parentServiceId=None
        )
        property_value = 'clear'

    if property_value:
        _update_orders_custom_property(
            int(pos_id),
            'PARENT_ID',
            property_value,
            orders_ids
        )

    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)


def _update_orders_custom_property(
        pos_id,             # type: int
        property_key,       # type: str
        property_value,     # type: str
        orders_ids          # type: List[str]
):
    # type: (...) -> None

    for order_id in orders_ids:
        properties = eTree.Element("Properties")
        eTree.SubElement(
            properties,
            "Property",
            {"key": property_key, "value": property_value}
        )

        set_order_properties(
            pos_id,
            False,
            order_id,
            properties
        )


def handle_set_current_order(argv, msg=None):
    pos_id, table, table_id, user_id = get_parameters(argv, msg)
    order_id = int(argv[3]) if len(argv) > 3 and argv[3] else None

    if not order_id:
        raise TableServiceException(INVALID_PARAMETERS)

    orders = filter(lambda o: o["orderId"] == order_id, table.pop('orders', []))
    if not orders:
        raise TableServiceException("$TABLE_ORDER_NOT_FOUND")

    db_conn = get_db_connection(pos_id)
    session_id = get_user_session_id(pos_id, user_id, db_conn)
    recall_order(pos_id, order_id, session_id, orders[0]["posId"], service_id=table["serviceId"])

    db_conn.pquery("updateCurrentOrderId", posId=pos_id, orderId=order_id)
    _update_table_list_cache(pos_id, db_conn)

    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)


def handle_void_service_order(argv, msg=None):
    pos_id, table, table_id, user_id = get_parameters(argv, msg)
    order_id = int(argv[3]) if len(argv) > 3 and argv[3] else None

    if not order_id:
        raise TableServiceException(INVALID_PARAMETERS)

    for order in table["orders"]:
        if order["orderId"] == order_id:
            break
    else:
        raise TableServiceException("$TABLE_ORDER_NOT_FOUND")

    db_conn = get_db_connection(pos_id)
    try:
        void_order(
            pos_id=pos_id,
            order_id=order_id,
            abandon="0",
            blk_notify=False,
        )
        db_conn.pquery("updateCurrentOrderState", orderId=order_id, state="VOIDED")
    except (Exception,):
        store_order(
            pos_id=pos_id,
            db_conn=db_conn,
            blk_notify=True,
        )

    _update_table_list_cache(
        pos_id=pos_id,
        db_conn=db_conn,
    )
    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg)


def handle_total_service(argv, msg=None):
    pos_id, table, table_id, user_id = get_parameters(argv, msg)
    tip_rate = argv[3] if len(argv) > 3 and argv[3] else None
    tip_amount = argv[4] if len(argv) > 4 and argv[4] else None
    sale_type = argv[5] if len(argv) > 5 and argv[5] else "EAT_IN"

    if int(table["status"]) != 4 and len(table.get('serviceChildren') or []) == 0:
        logger.warning(TABLE_EXPECTED_STATUS.format(table["status"], 'AVAILABLE'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    if int(table.get("currentPOSId") or "-1") != int(pos_id):
        raise TableServiceException("$INVALID_CURRENT_TABLE_POS")

    db_conn = get_db_connection(pos_id)

    service_id = table["serviceId"]
    split_services = _get_split_services_from_database(
        service_id=service_id,
        db_conn=db_conn,
    )

    table_split_order_id = split_services[0].get("splitOrderId") if split_services else ""
    table_orders = _recall_orders_from_pos(
        pos_id=pos_id,
        session_id="",
        table=table,
        db_conn=db_conn,
    )

    split_order_id = _verify_and_fix_split_order_id(
        pos_id=pos_id,
        orders=table_orders,
        split_order_id=table_split_order_id,
        db_conn=db_conn,
    )

    benefits = _get_merged_benefit_list(
        db_conn=db_conn,
        split_order_id=split_order_id,
        service_id=service_id,
    )

    service_ids = _get_table_service_ids(table)
    service_order_items = []

    # retrieves a list of all orders and its line numbers
    for service_id in service_ids:
        cursor = db_conn.pselect("getServiceOrderItem", serviceId=service_id)
        service_order_items.extend([
            dict([
                (
                    cursor.get_name(i),
                    json.loads(row.get_entry(i)) if cursor.get_name(i) == "lineNumbers" else row.get_entry(i)
                ) for i in range(cursor.cols())
            ]) for row in cursor
        ])

    if len(service_order_items) == 0:
        raise TableServiceException("service_order_items cannot be empty when an order is being totaled")

    properties = {"SPLIT_KEY": DEFAULT_SPLIT_KEY}
    fixed_benefits = _fix_benefits_line_number(benefits, service_order_items)
    if len(fixed_benefits) > 0:
        properties["BENEFIT_LIST"] = json.dumps(fixed_benefits)

    customer_doc = (table.get("properties", {}) or {}).get("CUSTOMER_DOC", "")
    customer_name = (table.get("properties", {}) or {}).get("CUSTOMER_NAME", "")
    if customer_doc:
        properties["CUSTOMER_DOC"] = customer_doc
    if customer_name:
        properties["CUSTOMER_NAME"] = customer_name

    set_table_loyalty_to_order(
        table=table,
        properties=properties,
    )

    session_id = get_user_session_id(
        pos_id=pos_id,
        user_id=user_id,
        db_conn=db_conn,
    )

    try:
        has_split_services = False
        if split_order_id is not None:
            recall_order(
                pos_id=pos_id,
                order_id=int(split_order_id),
                session_id=session_id,
                blk_notify=True,
            )
            has_split_services = True
        else:
            split_order_id = create_order(
                pos_id=pos_id,
                table=table,
                session_id=session_id,
                must_store_order=False,
                blk_notify=True,
                sale_type=sale_type,
                order_sub_type="SPLIT_SERVICE",
                custom_properties=properties,
            )
    except Exception:
        raise TableServiceException("$FAILED_TOTAL_SERVICE")

    _update_persisted_benefits(
        db_conn=db_conn,
        order_id=split_order_id,
        properties=properties,
    )

    split_obj = {
        "splitKey": DEFAULT_SPLIT_KEY,
        "splitOrderId": split_order_id
    }

    split_list = []
    new_order_id = split_obj["splitOrderId"]
    for item in service_order_items:
        if has_split_services and item["originalOrderId"] == new_order_id:
            continue
        for line in item["lineNumbers"]:
            split_obj.update({
                "originalOrderId": item["originalOrderId"],
                "lineNumber": line
            })
            split_list.append(deepcopy(split_obj))

    success = False
    intrans = False
    has_conn = False
    try:
        # initiate DB transaction
        db_conn.transaction_start()
        has_conn = True

        db_conn.query("BEGIN TRANSACTION;")
        intrans = True
        service_id = table["serviceId"]
        db_conn.pquery("registerServiceOrder", serviceId=service_id, orderId=new_order_id, posId=pos_id, userId=user_id)
        db_conn.pquery("unregisterServiceOrderChildren", serviceId=service_id, posId=pos_id)
        db_conn.pquery("updateCurrentOrderId", posId=pos_id, orderId=new_order_id)

        if has_split_services:
            order_benefit = properties.get("BENEFIT_LIST")
            benefit_ids = []
            if order_benefit:
                _get_on_payment_benefits(order_benefit=order_benefit, benefit_ids=benefit_ids)

            db_conn.pquery(
                "resetSplitServiceOrderInfo",
                SplitOrderId=new_order_id,
                serviceId=table["serviceId"],
                onPaymentIds=",".join(map(str, benefit_ids))
            )

        # split original table orders in new orders for payment
        register_split_service_on_database(db_conn, table, split_list)
        split_service_orders_on_database(db_conn, table)

        db_conn.query("COMMIT TRANSACTION;")
        intrans = False

        if has_split_services:
            _link_order_with_table(
                blk_notify=False,
                custom_properties=properties,
                order_id=new_order_id,
                pos_id=pos_id,
                table=table,
                sale_type=sale_type
            )

        _clean_order_cache(new_order_id, pos_id)

        logger.debug("Totaling order")
        updated_order_picture = total_order(pos_id, db_conn=db_conn, blk_notify=(any([tip_rate, tip_amount])))
        logger.debug("Totaled order")

        table_total = D("0.00")
        if updated_order_picture:
            order = eTree.XML(updated_order_picture)
            order_state = order.get("state")
            db_conn.pquery("updateCurrentOrderState", orderId=new_order_id, state=order_state)

            table_total = D(order.get("totalGross") or "0.00")
            _register_service_tenders(db_conn, order, session_id, table)

        # determine tip amount and update totaled order and table service status
        db_conn.query("BEGIN TRANSACTION;")
        intrans = True
        total_amount = _get_total_amount_removing_products_without_tip(pos_id, updated_order_picture)

        custom_property = _get_service_custom_property(pos_id, table["serviceId"], "TIP_TYPE")
        if custom_property:
            if custom_property.get("TIP_TYPE", None) == TipType.AMOUNT:
                tip_amount = _is_valid_float(table["tipAmount"], tip_amount)
                tip_rate = None
                tip_type = TipType.AMOUNT
            else:
                tip_rate = _is_valid_float(table["tipRate"], tip_rate)
                tip_amount = None
                tip_type = TipType.PERCENTAGE
        else:
            tip_type = TipType.PERCENTAGE if tip_rate is not None else TipType.AMOUNT

        params = [db_conn, new_order_id, pos_id, session_id, table, total_amount, tip_type, tip_rate, tip_amount]
        service_tip = _calculate_service_tip(*params)
        table_total = str(D(table_total) + D(service_tip["tip"]))
        tip_type = TipType.PERCENTAGE if tip_rate is not None else TipType.AMOUNT

        # update table service status, reply and send event
        service_id = table["serviceId"]
        handle_set_custom_property(
            argv=[pos_id, "TIP_TYPE", tip_type, "", service_id],
            msg=msg,
            notify_update=False,
            db_conn=db_conn
        )

        _update_service_totals(
            db_conn=db_conn,
            pos_id=pos_id,
            table=table,
            order=eTree.XML(updated_order_picture),
        )
        db_conn.pquery(
            "updateTableServiceStatus",
            serviceId=service_id,
            status=TableStatusDescr.TOTALED,
            totalAmount=str(table_total)
        )

        success = True
    except (Exception,):
        logger.exception("Error totaling table")
    finally:
        if intrans:
            db_conn.query("COMMIT TRANSACTION;" if success else "ROLLBACK TRANSACTION;")
        if not success and new_order_id:
            try:
                store_order(
                    pos_id=pos_id,
                    db_conn=db_conn,
                )
                reset_current_order(pos_id)
            except (Exception,):
                logger.exception("Error in Store SplitService Order")
                db_conn.query("ROLLBACK TRANSACTION;")
        if has_conn:
            db_conn.transaction_end()
        if not success:
            raise TableServiceException("$FAILED_TOTAL_SERVICE")

    table_picture_xml, event_xml = handle_table_picture([pos_id, table_id], "", "TABLE_TOTALED", "TableService")
    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg, FM_XML, table_picture_xml)
    mb_context.MB_EasyEvtSend("TABLE_TOTALED", "TableService", event_xml, sourceid=int(pos_id))
    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="TableTotaled", cached=True)
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "TableTotaled", store_modified_xml)


def _get_on_payment_benefits(
    order_benefit,      # type: Dict
    benefit_ids         # type: List
):
    # type: (...) -> None

    benefit_json = json.loads(order_benefit)
    benefit_list = json.loads(benefit_json.get("benefitList"))
    for benefit in benefit_list:
        if benefit.get('benefit_applied') and benefit.get("benefit").get("automatic") == "ON_PAYMENT":
            benefit_ids.extend(benefit.get("used_discount_ids"))


def _is_valid_float(value, default):
    try:
        float_value = float(value)
        return str(float_value)
    except ValueError:
        pass

    return default


def _clean_order_cache(
    new_order_id,   # type: str
    pos_id,         # type: int
):
    # type: (...) -> None

    order_picture(
        pos_id=pos_id,
        order_ids=new_order_id,
        no_cache=True,
    )


def split_service_orders_on_database(db_conn, table):
    db_conn.pselect(
        "splitServiceOrders",
        serviceId=table["serviceId"]
    )


def register_split_service_on_database(db_conn, table, split_list):
    db_conn.pquery(
        "registerSplitService",
        serviceId=table["serviceId"],
        PreparedStringTable=_make_json(split_list)
    )


def _fix_benefits_line_number(benefits, service_order_items):
    all_benefits = {}

    for original_order_id in benefits.keys():
        current_benefit = benefits[original_order_id]
        new_benefits = []
        for benefit in json.loads(current_benefit.get("benefitList")):
            _fix_added_sale_lines(benefit, original_order_id, service_order_items)
            _fix_burnt_items(benefit, original_order_id, service_order_items)

            new_benefits.append(benefit)
            current_benefit["benefitList"] = json.dumps(new_benefits)

        all_benefits[original_order_id] = current_benefit

    merged_benefits = []
    for benefit in all_benefits.values():
        merged_benefits = _merge_benefits(merged_benefits, benefit)

    return merged_benefits


def _get_new_line_number(order_id, line_number, service_order_items):
    count = 0
    for order_line_dict in service_order_items:
        if order_line_dict['originalOrderId'] != order_id:
            count += len(order_line_dict['lineNumbers'])
        else:
            return count + order_line_dict['lineNumbers'].index(line_number) + 1

    raise ValueError("New LineNumber not Found")


def _fix_added_sale_lines(benefit, original_order_id, service_order_items):
    if "added_sale_lines" not in benefit:
        return

    temp_added_sale_lines = []
    for line_number in benefit["added_sale_lines"]:
        try:
            new_line_number = _get_new_line_number(original_order_id, line_number, service_order_items)
            temp_added_sale_lines.append(new_line_number)
        except ValueError:
            continue
    benefit["added_sale_lines"] = temp_added_sale_lines


def _fix_burnt_items(benefit, original_order_id, service_order_items):
    if "burnt_items" not in benefit:
        return

    temp_burnt_items = {}
    for burnt_item in benefit["burnt_items"]:
        try:
            burnt_item_split = burnt_item.split('.')
            new_line_number = _get_new_line_number(original_order_id, int(burnt_item_split[0]), service_order_items)
            burnt_item_split[0] = str(new_line_number)
            temp_burnt_items[".".join(burnt_item_split)] = benefit["burnt_items"][burnt_item]
        except ValueError:
            continue
    benefit["burnt_items"] = temp_burnt_items


def handle_update_table_tenders(argv, msg=None, skip_message=False):
    pos_id, table, table_id, user_id = get_parameters(argv, msg)

    _validate_table_is_totaled(table)

    db_conn = get_db_connection(pos_id)
    current_order_picture = order_picture(pos_id)
    try:
        if current_order_picture:
            order = eTree.XML(current_order_picture)
            session_id = get_user_session_id(pos_id, user_id, db_conn)
            _register_service_tenders(db_conn, order, session_id, table)
            _update_service_totals(
                db_conn=db_conn,
                pos_id=pos_id,
                table=table,
                order=order,
            )
    except (Exception,):
        logger.exception("Error on UpdateTableTenders")

    if skip_message:
        return

    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)
    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="UpdateTableTenders")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "UpdateTableTenders", store_modified_xml)


def _validate_table_is_totaled(table):
    if not _table_is_totaled(table):
        logger.warning(TABLE_EXPECTED_STATUS.format(table["status"], 'TOTALED'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    current_pos_order_state = table.get("currentPOSOrderState")
    current_pos_order_id = table.get("currentPOSOrderID")
    if not current_pos_order_id or current_pos_order_state != "TOTALED":
        logger.warning("Invalid Order Status: {} / Expecting: {}".format(current_pos_order_state, 'TOTALED'))
        raise TableServiceException("$INVALID_CURRENT_ORDER_STATUS")


def _register_service_tenders(db_conn, order, session_id, table):
    tenders = order.findall(".//Tender")
    for tender in tenders:
        db_conn.pquery(
            "registerServiceTenders",
            serviceId=table["serviceId"],
            totalTenderAmount=str(tender.get("tenderAmount")),
            PreparedIntegerTable=order.get("orderId"),
            sessionId=session_id,
            orderTenderId=tender.get("tenderId"),
        )


def _update_service_totals(
    db_conn,    # type: Connection
    pos_id,     # type: int
    table,      # type: Dict[str, Any]
    order,      # type: eTree.Element
):
    # type: (...) -> None

    def _get_value(xml_object, string):
        return xml_object.get(string) or "0.00"

    split_order_id = order.get("orderId")
    current_pos_order_id = table.get("currentPOSOrderID")
    valid_status = [OrderStatus.IN_PROGRESS.name, OrderStatus.TOTALED.name]
    is_not_current_order = split_order_id is not None and current_pos_order_id != str(split_order_id)
    if is_not_current_order and order.get("status") in valid_status:
        db_conn.pquery("updateCurrentServiceOrder", posId=pos_id, orderId=split_order_id)

    total_amount = D(_get_value(order, "totalAmount"))
    change_amount = D(0)
    tender_amount = D(0)
    for tender in order.findall("TenderHistory/Tender"):
        change_amount += D(_get_value(tender, "change"))
        tender_amount += D(_get_value(tender, "tenderAmount"))

    db_conn.pquery(
        "updateServiceTotals",
        serviceId=table["serviceId"],
        serviceTotal=str((total_amount + D(_get_value(order, "taxTotal"))).quantize(D("0.01"))),
        discountAmount=_get_value(order, "discountAmount"),
        taxTotal=_get_value(order, "taxTotal"),
        dueAmount=str((D(_get_value(order, "dueAmount")) + change_amount).quantize(D("0.01"))),
        change=str(change_amount.quantize(D("0.01"))),
        tenderAmount=str(tender_amount.quantize(D("0.01"))),
        tipAmount=_get_value(order, "tip"),
        totalAmount=str((total_amount + D(_get_value(order, "tip"))).quantize(D("0.01"))),
    )


def _calculate_service_tip(
    db_conn,                 # type: persistence.Connection
    order_id,                # type: int
    pos_id,                  # type: int
    session_id,              # type: str
    table,                   # type: Dict
    total_amount,            # type: int
    tip_type,                # type: str
    tip_rate=None,           # type: Optional[str]
    tip_amount=None,         # type: Optional[str]
):
    # type: (...) -> Dict

    service_tip_amount = _get_tip_amount(
        total_amount=total_amount,
        tip_rate=tip_rate,
        tip_amount=tip_amount,
    )

    excess_amount = D(0)
    due_amount = _calculate_due_amount(
        pos_id,
        tip_amount=D(service_tip_amount)
    )
    if due_amount < 0:
        excess_amount = abs(due_amount)
        service_tip_amount = D(service_tip_amount) + excess_amount

    if tip_rate is None:
        tip_rate = _calc_tip_rate(
            tip_amount=service_tip_amount,
            total_amount=total_amount,
            current_tip_rate=tip_rate,
        )
    tip_object = {
        "splitKey": DEFAULT_SPLIT_KEY,
        "splitOrderId": str(order_id),
        "totalAmount": str(total_amount),
        "excessAmount": str(excess_amount),
        "tip": str(service_tip_amount),
        "rate": str(D(tip_rate).quantize(D("0.01"))),
        "amount": str(D(service_tip_amount).quantize(D("0.01"))),
        "tip_type": tip_type,
    }

    _register_service_tip(
        db_conn,
        pos_id,
        order_id,
        session_id,
        table,
        tip_object,
    )

    return tip_object


def _get_tip_amount(
    total_amount,       # type: int
    tip_rate=None,      # type: Optional[str]
    tip_amount=None,    # type: Optional[str]
):
    # type: (...) -> D

    if tip_rate is not None:
        return (total_amount * (D(tip_rate) / D("100"))).quantize(D("0.01"))

    if tip_amount is not None:
        return D(tip_amount).quantize(D("0.01"))

    return D(0)


def _calc_tip_rate(
    tip_amount,         # type: Union[str, float, Decimal]
    total_amount,       # type: int
    current_tip_rate,   # type: Optional[Union[float, Decimal]]
):
    # type: (...) -> str

    if not total_amount and current_tip_rate is not None:
        tip_rate = D(current_tip_rate)
    else:
        tip_rate = (D(tip_amount) / total_amount * 100) if total_amount else D(0)

    return str(tip_rate.quantize(D("0.01")))


def _calculate_due_amount(pos_id, tip_amount=D(0)):
    order = eTree.XML(order_picture(pos_id))
    total_gross = D(order.get("totalGross"))
    total_change = D(order.get("change") or 0)
    total_tender = sum(
        [
            D(tender.get("tenderAmount"))
            for tender in order.findall("TenderHistory/Tender")
        ],
        D(0)
    )
    due_amount = (total_gross - total_tender + tip_amount + total_change)
    return due_amount


def _register_service_tip(
    db_conn,        # persistence.Connection
    pos_id,         # type: int
    order_id,       # type: int
    session_id,     # type: str
    table,          # type: Dict
    tip_object,     # type: Dict
):
    # type: (...) -> None

    properties = eTree.Element("Properties")
    eTree.SubElement(
        properties,
        "Property",
        {
            "key": "TIP_RATE",
            "value": tip_object.get("rate")
        }
    )

    eTree.SubElement(
        properties,
        "Property",
        {
            "key": "TIP_AMOUNT",
            "value": tip_object.get("amount")
        }
    )

    if tip_object.get("tip_type") is not None:
        eTree.SubElement(
            properties,
            "Property",
            {
                "key": "TIP_TYPE",
                "value": tip_object.get("tip_type")
            }
        )

    excess_amount = tip_object.get("excessAmount", 0)
    eTree.SubElement(
        properties,
        "Property",
        {
            "key": "TIP_EXCESS_AMOUNT",
            "value": excess_amount if float(excess_amount) > 0 else 'clear'
        }
    )
    set_order_properties(pos_id, True, str(order_id), properties)
    update_order_properties(pos_id, blk_notify=False, tip=tip_object["tip"])
    db_conn.pquery(
        "registerServiceTip",
        serviceId=table["serviceId"],
        sessionId=session_id,
        PreparedStringTable=_make_json([tip_object])
    )


def _set_change_tip_custom_properties(
    msg,            # type: MBEasyContext
    db_conn,        # type: Connection
    pos_id,         # type: str
    service_id,     # type: str
    tip_amount,     # type: Dict
    user_id,        # type: str
    manager_id,     # type: str
):
    # type: (...) -> None

    date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    current_properties = _get_service_custom_property(pos_id, service_id, "TIP_CHANGED")
    new_properties = [{'amount': tip_amount, 'date': date, 'user': user_id, 'manager': manager_id}]
    if current_properties.get("TIP_CHANGED"):
        current_properties = json.loads(current_properties.get("TIP_CHANGED"))
        new_properties = current_properties + new_properties

    handle_set_custom_property(
        argv=[pos_id, "TIP_CHANGED", json.dumps(new_properties), "", service_id],
        msg=msg,
        notify_update=False,
        db_conn=db_conn
    )


def _verify_and_fix_split_order_id(
    pos_id,             # type: int
    orders,             # type: eTree.XML
    split_order_id,     # type: str
    db_conn,            # type: Connection
):
    # type: (...) -> Optional[str]

    if not split_order_id:
        for current_order in orders:
            order_sub_type = current_order.get("subType")
            if order_sub_type == 'SPLIT_SERVICE':
                split_order_id = current_order.get("orderId")
                break
        else:
            return None

    split_order_state = ""
    for current_order in orders:
        if current_order.get("orderId") == split_order_id:
            split_order_state = current_order.get("state")
            break

    if not split_order_state:
        split_order = eTree.XML(order_picture(pos_id, split_order_id))
        split_order_state = split_order.find("Order").get("state")

    skip_states = ('VOIDED', 'ABANDONED', 'SYSTEM_VOIDED')
    if split_order_state in skip_states:
        db_conn.pquery(
            "updateCurrentOrderState",
            orderId=split_order_id,
            state=split_order_state,
        )
        return None

    return split_order_id


def _recall_orders_from_pos(
    pos_id,         # type: int
    session_id,     # type: str
    table,          # type: Dict
    db_conn=None,   # type: Optional[persistence.Connection]
):
    # type: (...) -> (Dict, bool)

    skip_states = ('VOIDED', 'ABANDONED', 'SYSTEM_VOIDED')
    table_orders_ids = [str(o.get("orderId")) for o in table.get("orders") or [] if o['state'] not in skip_states]

    if not table_orders_ids:
        return []

    temp_picture_list = ";".join(table_orders_ids)

    if len(temp_picture_list) > 0:
        logger.debug("Recalling order")
        orders_xml = _multiple_recall_order(pos_id, temp_picture_list, session_id, "")
        table_orders = eTree.XML(orders_xml[1:]).findall("Order") if orders_xml else []
        logger.debug("Recalled order")

        table_service_id = table.get('serviceId')
        for temp_order in table_orders:
            order_id = str(temp_order.get('orderId'))
            order_service_id = temp_order.find(".//CustomOrderProperties/OrderProperty[@key='SERVICE_ID']").get("value")
            if temp_order.get('state') not in skip_states:
                db_conn.pquery(
                    procname="registerServiceOrder",
                    serviceId=order_service_id or table_service_id,
                    orderId=order_id,
                    posId=pos_id,
                )

        return table_orders

    return []


def _get_table_service_ids(
        table  # type: dict
):
    # type: (...) -> List[str]

    if not _table_mode_is_tabs(table=table):
        return [table['serviceId']]

    service_ids = []
    for order in table.get("orders"):
        service_id = str(order['tableServiceId'])
        service_ids.append(service_id) if service_id not in service_ids else None

    return service_ids


def _table_mode_is_tabs(
    table,  # type: dict
):
    # type: (...) -> bool

    return table["properties"] and table["properties"].get("MODE") == "tabs"


def _get_merged_benefit_list(
    db_conn,           # type: persistence.Connection
    split_order_id,    # type: Optional[str]
    service_id         # type: str
):
    # type: (...) -> Dict

    merged_benefits = {}
    cursor = db_conn.pselect("getTableOrderBenefits", serviceId=service_id)

    try:
        totaled_order_id = int(split_order_id)
    except (Exception,):
        totaled_order_id = 0

    for row in cursor:
        order_id = row.get_entry(1)
        if int(order_id) == totaled_order_id:
            continue

        merged_benefits[order_id] = json.loads(row.get_entry(0))

    return merged_benefits


def _update_persisted_benefits(
    db_conn,        # type: persistence.Connection
    order_id,       # type: str
    properties,     # type: Dict
):
    # type: (...) -> None

    if not order_id:
        return

    cursor = db_conn.pselect("getOrderBenefits", orderId=order_id)
    for row in cursor:
        if not properties.get("BENEFIT_LIST", ""):
            properties["BENEFIT_LIST"] = row.get_entry(0)
        else:
            benefit_list = json.loads(row.get_entry(0) or "{}")
            properties_benefits_list = json.loads(s=properties["BENEFIT_LIST"] or "{}")
            properties_benefits_list["persistedBenefits"] = benefit_list.get("persistedBenefits", {})
            properties["BENEFIT_LIST"] = json.dumps(obj=properties_benefits_list)


def _multiple_recall_order(pos_id, order_id, session_id="", src_pos_id=""):
    logger.debug("Sending recall to POS: {} / Order Id: {} / Source POS ID: {}".format(pos_id, order_id, src_pos_id))
    # TRY TO RECALL ORDER IN KNOW POS
    params = ("1", pos_id, order_id, session_id, src_pos_id, "1")
    with recall_lock:
        response = send_order_message(params, pos_id, TK_SLCTRL_OMGR_RECALLORDER)
        data = response.split('\0')
        orders_xml = data[2] if len(data) > 2 else ""

        return orders_xml


def _get_total_amount_removing_products_without_tip(pos_id, order_total_picture=None):
    total_items_tip = 0
    if order_total_picture is None:
        order_total_picture = order_picture(pos_id)

    order = eTree.XML(order_total_picture)
    discounts = {}
    for sale_line in order.findall("SaleLine"):
        if _product_generates_tip(sale_line):
            total_items_tip += D(sale_line.get("itemPrice") or 0)

        _add_discounts_applied(sale_line.get("discountsApplied"), discounts)

    tips_removed_by_discounts = _get_total_tips_removed_by_discounts(discounts, order)
    return total_items_tip - tips_removed_by_discounts


def _add_discounts_applied(discounts_applied, discounts):
    if not discounts_applied:
        return

    for discount in discounts_applied.split(','):
        discount_id, amount = discount.split(':')
        if discount_id not in discounts.keys():
            discounts[discount_id] = 0

        discounts[discount_id] += float(amount)


def _get_total_tips_removed_by_discounts(discounts, order):
    tips_removed_by_discounts = 0
    benefits = get_benefits_from_order(order) or []
    for benefit in benefits:
        coupon = benefit["benefit"]
        if "tipIncluded" in coupon.keys() and coupon["tipIncluded"] is False:
            for discount in benefit["used_discount_ids"]:
                if str(discount) not in discounts:
                    continue

                tips_removed_by_discounts += D(discounts[str(discount)])

    return tips_removed_by_discounts


def get_benefits_from_order(order):
    benefit_list_custom_property = order.find(".//OrderProperty[@key='BENEFIT_LIST']")
    if benefit_list_custom_property is None:
        return []

    benefit_list_json = json.loads(benefit_list_custom_property.get("value"))
    benefit_json = benefit_list_json.get("benefitList")
    benefit_list = json.loads(benefit_json)
    benefit_list = [b for b in benefit_list if b['benefit_applied']]

    return benefit_list


def _product_generates_tip(sale_line):
    return int(sale_line.get("partCode")) not in products_list_not_charge_tip


def handle_change_service_tip(argv, msg=None):
    pos_id, table, table_id, user_id = get_parameters(argv, msg)

    tip_rate = argv[3] if len(argv) > 3 and argv[3] else None
    tip_amount = argv[4] if len(argv) > 4 and argv[4] else None
    manager_id = argv[6] if len(argv) > 6 and argv[6] else ""

    current_tip_amount = (table.get("calculatedTip") or "0.00")

    if not all([any([tip_amount, tip_rate]), msg]):
        raise TableServiceException(INVALID_PARAMETERS)

    _validate_table_is_totaled(table)
    _validate_table_tip_amount(
        pos_id=pos_id,
        current_tip_amount=current_tip_amount,
        tip_amount=tip_amount,
        tip_rate=tip_rate,
    )

    table_total = (table.get("totalAmount") or "0.00")
    total = D(table_total) - D(current_tip_amount)
    db_conn = get_db_connection(pos_id)
    intrans = False
    has_conn = False
    success = False
    try:
        db_conn.transaction_start()
        has_conn = True

        db_conn.query("BEGIN TRANSACTION;")
        intrans = True

        total_order_id = table.get("currentPOSOrderID")
        total_amount = _get_total_amount_removing_products_without_tip(pos_id)
        session_id = get_user_session_id(pos_id, user_id, db_conn)
        tip_type = TipType.PERCENTAGE if tip_rate is not None else TipType.AMOUNT
        params = [db_conn, total_order_id, pos_id, session_id, table, total_amount, tip_type, tip_rate, tip_amount]
        service_tip = _calculate_service_tip(*params)

        _set_change_tip_custom_properties(
            msg=msg,
            db_conn=db_conn,
            pos_id=str(pos_id),
            service_id=table["serviceId"],
            tip_amount=service_tip["amount"],
            user_id=str(user_id),
            manager_id=manager_id,
        )

        total = str(total + D(service_tip["tip"]))
        db_conn.pquery(
            "updateTableServiceStatus",
            serviceId=table["serviceId"],
            status=TableStatusDescr.TOTALED,
            totalAmount=total
        )

        handle_set_custom_property(
            argv=[pos_id, "TIP_TYPE", tip_type, "", table.get("serviceId")],
            msg=msg,
            notify_update=False,
            db_conn=db_conn
        )

        _update_service_totals(
            db_conn=db_conn,
            pos_id=pos_id,
            table=table,
            order=eTree.XML(order_picture(pos_id=pos_id)),
        )

        success = True
    finally:
        if intrans:
            db_conn.query("COMMIT TRANSACTION;" if success else "ROLLBACK TRANSACTION;")
        if has_conn:
            db_conn.transaction_end()

    table_picture_xml, _ = handle_table_picture([pos_id, table_id])
    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg, FM_XML, table_picture_xml)


def _validate_table_tip_amount(
    pos_id,                 # type: int
    current_tip_amount,     # type: str
    tip_amount,             # type: str
    tip_rate,               # type: str
):
    # type: (...) -> None

    if tip_rate is None and tip_amount is None:
        return

    service_tip_amount = _get_tip_amount(
        total_amount=_get_total_amount_removing_products_without_tip(pos_id),
        tip_rate=tip_rate,
        tip_amount=tip_amount,
    )

    order = eTree.XML(order_picture(pos_id))
    order_due_amount = D(order.get("dueAmount") or "0.00")
    new_due_amount = order_due_amount - D(current_tip_amount) + D(service_tip_amount or "0.00")
    if new_due_amount < 0:
        raise TableServiceException("$TIP_AMOUNT_NEED_TO_BE_LESS_THAN_DUE_AMOUNT")


def handle_close_service(argv, msg=None):
    pos_id, table, table_id, user_id = get_parameters(argv, msg)

    if not _table_is_totaled(table):
        logger.warning(TABLE_EXPECTED_STATUS.format(table["status"], 'TOTALED'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    order = eTree.XML(order_picture(pos_id))
    if D(order.get("dueAmount") or "0") > D("0"):
        raise TableServiceException("$NOT_ALL_ORDERS_ARE_PAID")

    db_conn = get_db_connection(pos_id)
    total_order_ids = [o['orderId'] for o in table.get('splitOrders') or []]
    try:
        _pay_order(
            pos_id=pos_id,
            db_conn=db_conn,
            order=order,
        )
        _update_service_totals(
            db_conn=db_conn,
            pos_id=pos_id,
            table=table,
            order=order,
        )
    except (Exception,):
        for order in total_order_ids:
            order_xml = eTree.XML(order_picture(pos_id, order))
            if order_xml.find(".//Order").get('state') != 'PAID':
                raise

    service_id = table["serviceId"]
    for order in (table.get("orders") or []):
        order_id = order['orderId']
        if order_id in total_order_ids:
            continue

        if order['state'] not in ('VOIDED', 'ABANDONED', 'PAID'):
            for retries in range(1, 3):
                try:
                    void_order(pos_id, order_id, blk_notify=False)
                    db_conn.pquery("updateCurrentOrderState", orderId=order_id, state="ABANDONED")
                    break
                except (Exception,):
                    sys_log_exception("Error voiding Order {} on POS {}".format(order_id, pos_id))
                    if retries == 3:
                        raise

    table_picture_xml = _generate_table_closed_event(db_conn, pos_id, service_id, table_id, user_id)

    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg, FM_XML, table_picture_xml)

    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="TableTotaled", cached=True)
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "TableClosed", store_modified_xml)


def _pay_order(
    pos_id,     # type: int
    db_conn,    # type: Connection
    order,      # type: eTree.Element
):
    # type: (...) -> eTree.Element

    fiscal_xml_property = order.findall(".//OrderProperty/[@key='FISCAL_XML']")
    if not fiscal_xml_property:
        order_id = int(order.get("orderId"))
        raise MissingFiscalPropertyException(order_id)

    last_tender = _get_last_tender_with_amount(order=order)
    order = tender_order(
        pos_id=pos_id,
        tender_id=last_tender.get("tenderType"),
        amount=last_tender.get("tenderAmount"),
        do_not_close=False,
        order_tender_id=last_tender.get("tenderId"),
        tip_amount=last_tender.get("tip", 0),
        blk_notify=False
    )
    db_conn.pquery("updateCurrentOrderState", orderId=order.get('orderId'), state="PAID")

    return order


def _get_last_tender_with_amount(
    order,  # type: eTree.Element
):
    # type: (...) -> eTree.Element

    tenders = sorted(order.findall("TenderHistory/Tender"), reverse=True)
    return next(tender for tender in tenders if float(tender.get("tenderAmount")) > 0)


def _generate_table_closed_event(db_conn, pos_id, service_id, table_id, user_id, abandon=False):
    table_picture_xml, event_xml = handle_table_picture(
        argv=[pos_id, table_id],
        msg="",
        subject="TABLE_CLOSED",
        event_type="TableService",
        full=True,
        user_id=user_id,
    )

    status = TableStatusDescr.CLOSED if abandon is False else TableStatusDescr.ABANDONED
    db_conn.pquery(
        "updateTableServiceStatus",
        serviceId=service_id,
        status=status
    )

    selected_to_total = _get_service_custom_property(pos_id, service_id or table_id, "SELECTED_TO_TOTAL")
    if selected_to_total and 'all_services' in selected_to_total.get("SELECTED_TO_TOTAL"):
        db_conn.pquery(
            "closeServicesChildren",
            serviceId=service_id,
            status=status
        )

    table_picture_xml, event_xml = _fix_closed_xml_properties(event_xml, pos_id, table_id, table_picture_xml, abandon)
    mb_context.MB_EasyEvtSend("TABLE_CLOSED", "TableService", event_xml, sourceid=int(pos_id))

    return table_picture_xml


def _fix_closed_xml_properties(event_xml, pos_id, table_id, table_picture_xml, abandon=False):
    closed_table = handle_list_tables([pos_id, None, table_id], cached=False)

    close_status = TableStatusDescr.CLOSED if abandon is False else TableStatusDescr.ABANDONED
    status_int = 7 if abandon is False else 8
    finish_ts = closed_table[0]["finishTS"] if closed_table else datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S')
    to_string = 'status="{}" statusDescr="{}" finishTS="{}"'.format(status_int, close_status, finish_ts)

    from_string = ''
    if event_xml.find('status="2" statusDescr=') != -1:
        from_string = 'status="2" statusDescr="{}"'.format(TableStatusDescr.WAITING_TO_BE_SEATED)
    elif event_xml.find('status="3" statusDescr=') != -1:
        from_string = 'status="3" statusDescr="{}"'.format(TableStatusDescr.SEATED)
    elif event_xml.find('status="4" statusDescr=') != -1:
        from_string = 'status="4" statusDescr="{}"'.format(TableStatusDescr.IN_PROGRESS)
    elif event_xml.find('status="6" statusDescr=') != -1:
        from_string = 'status="6" statusDescr="{}"'.format(TableStatusDescr.TOTALED)

    if from_string != '':
        event_xml = event_xml.replace(from_string, to_string)
        table_picture_xml = table_picture_xml.replace(from_string, to_string)

    return table_picture_xml, event_xml


def handle_reopen_service(argv, msg=None):
    pos_id, table, table_id, user_id = get_parameters(argv, msg)

    if not _table_is_totaled(table):
        logger.warning(TABLE_EXPECTED_STATUS.format(table["status"], 'TOTALED'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    _reopen_service(
        pos_id=pos_id,
        table=table,
        user_id=user_id,
    )

    table_picture_xml, event_xml = handle_table_picture(
        [pos_id, table_id],
        None,
        "TABLE_REOPENED",
        "TableService"
    )

    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg, FM_XML, table_picture_xml)
    mb_context.MB_EasyEvtSend("TABLE_REOPENED", "TableService", event_xml, sourceid=int(pos_id))
    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="TableTotaled", cached=True)
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "TableReopened", store_modified_xml)


def _reopen_service(
    pos_id,        # type: int
    table,         # type: Dict
    user_id=None,  # type: int
):
    # type: (...) -> None

    current_pos_order_state = table.get("currentPOSOrderState")
    current_pos_order_id = table.get("currentPOSOrderID")
    db_conn = get_db_connection(pos_id)
    intrans = False
    has_conn = False
    success = False
    try:
        db_conn.transaction_start()
        has_conn = True
        db_conn.query("BEGIN TRANSACTION;")
        intrans = True

        if current_pos_order_id and current_pos_order_state in ("IN_PROGRESS", "TOTALED"):
            clear_excess_amount(
                db_conn=db_conn,
                pos_id=pos_id,
                order_id=current_pos_order_id,
                table=table,
                user_id=user_id,
            )

            if current_pos_order_state == "TOTALED":
                reopen_order(
                    pos_id=pos_id,
                    blk_notify=True,
                )

            DiscountService.on_reopen_service(
                mbcontext=mb_context,
                pos_id=pos_id,
                table_id=table["id"],
            )

            store_order(
                pos_id=pos_id,
                db_conn=db_conn,
                blk_notify=False,
            )

        _unregister_temp_orders_from_table_in_tabs_mode(
            pos_id=pos_id,
            db_conn=db_conn,
            table=table,
            current_pos_order_id=current_pos_order_id,
        )

        db_conn.pquery(
            procname="updateTableServiceStatus",
            serviceId=table["serviceId"],
            status=TableStatusDescr.IN_PROGRESS,
        )

        db_conn.pquery("updateCurrentOrderId", posId=pos_id)

        success = True
    finally:
        if intrans:
            db_conn.query("COMMIT TRANSACTION;" if success else "ROLLBACK TRANSACTION;")
        if has_conn:
            db_conn.transaction_end()


def _unregister_temp_orders_from_table_in_tabs_mode(
    pos_id,                 # type: int
    db_conn,                # type: Connection
    table,                  # type: Dict
    current_pos_order_id,   # type: int
):
    # type (...) -> None

    if not _table_mode_is_tabs(table=table):
        return

    order_ids = []
    service_id = int(table['serviceId'])
    for order in table.get('orders'):
        order_id = int(order['orderId'])
        table_service_id = int(order['tableServiceId'])

        if order_id != current_pos_order_id and table_service_id == service_id:
            order_ids.append({'orderId': order_id})

    db_conn.pquery(
        procname="unregisterMultipleServiceOrders",
        PreparedStringTable=_make_json(params=order_ids),
        serviceId=service_id,
        posId=pos_id,
    )


def clear_excess_amount(
    db_conn,    # type: Connection
    pos_id,     # type: int
    order_id,   # type: int
    table,      # type: eTree
    user_id,    # type: int
):
    order = eTree.XML(order_picture(pos_id))
    has_excess_amount = order.find(".//CustomOrderProperties/OrderProperty[@key='TIP_EXCESS_AMOUNT']")

    if has_excess_amount is not None:
        session_id = get_user_session_id(
            pos_id=pos_id,
            user_id=user_id,
            db_conn=db_conn,
        )

        tip_object = {
            "splitKey": DEFAULT_SPLIT_KEY,
            "splitOrderId": str(order_id),
            "totalAmount": str(table.get("totalAmount")),
            "excessAmount": "0",
            "tip": "0",
            "rate": "clear",
            "amount": "0",
        }

        _register_service_tip(
            db_conn=db_conn,
            pos_id=pos_id,
            order_id=order_id,
            session_id=session_id,
            table=table,
            tip_object=tip_object,
        )


def handle_recall_service(argv, msg=None):
    pos_id, table, table_id, user_id = get_parameters(argv, msg)

    current_pos_id = table.get("currentPOSId")
    if current_pos_id is not None and int(current_pos_id) != pos_id:
        raise TableServiceException("$TABLE_RECALLED_IN_ANOTHER_STATION")

    if int(table["status"]) not in (2, 3, 4, 6):
        ok_states = 'WAITING / SEATED / IN_PROGRESS / TOTALED'
        logger.warning(TABLE_EXPECTED_STATUS.format(table["status"], ok_states))
        raise TableServiceException(INVALID_TABLE_STATUS)

    db_conn = get_db_connection(pos_id)
    session_id = get_user_session_id(pos_id, user_id, db_conn)
    db_conn.pquery("recallService", serviceId=table["serviceId"], posId=pos_id, sessionId=session_id)
    notify_ndiscount = table["statusDescr"] != TableStatusDescr.TOTALED

    if _table_is_totaled(table):
        totaled_orders = table.pop("splitOrders", None)
        if totaled_orders is not None:
            for split_order in totaled_orders:
                __last_order = (split_order == totaled_orders[-1])
                if not __last_order:
                    store_order(
                        pos_id=pos_id,
                        db_conn=db_conn,
                    )
                    continue

                current_pos_order_id = table.get("currentPOSOrderID")
                if current_pos_order_id is not None and current_pos_order_id != str(split_order['orderId']):
                    current_pos_order_state = table.get("currentPOSOrderState")
                    if current_pos_order_state != 'STORED':
                        store_order(
                            pos_id=pos_id,
                            db_conn=db_conn,
                        )

                try:
                    recall_order(
                        pos_id=pos_id,
                        order_id=split_order['orderId'],
                        session_id=session_id,
                        src_pos_id="",
                        blk_notify=__last_order,
                        service_id=table["serviceId"],
                        notify_ndiscount=notify_ndiscount,
                    )
                    db_conn.pquery("updateCurrentOrderId", posId=pos_id, orderId=split_order['orderId'])
                    split_order['state'] = 'IN_PROGRESS'
                except Exception as ex:  # noqa
                    logger.warning("Error calling recall_order and updating current order", exc_info=True)
                    if not _try_to_fix_totaled_order(db_conn, pos_id, split_order, table):
                        raise

                if split_order.get('state') not in ('PAID', 'TOTALED'):
                    try:
                        total_order(
                            pos_id=pos_id,
                            db_conn=db_conn,
                            blk_notify=False,
                            notify_ndiscount=notify_ndiscount,
                        )
                    except Exception as ex:  # noqa
                        logger.warning("Error calling total_order", exc_info=True)
                        if not _try_to_fix_totaled_order(db_conn, pos_id, split_order, table):
                            raise

        _update_table_list_cache(pos_id, db_conn)
        handle_update_table_tenders(argv, msg, True)

    table_picture_xml, _ = handle_table_picture([pos_id, table_id])
    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg, FM_XML, table_picture_xml)
    store_modified_xml = get_store_picture_xml(
        pos_id=pos_id,
        subject="STORE_MODIFIED",
        event_type="RecallService",
        cached=True,
    )
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "RecallService", store_modified_xml)


def _try_to_fix_totaled_order(db_conn, pos_id, split_order, table):
    logger.debug("_try_to_fix_totaled_order STARTED")
    order = eTree.XML(order_picture(pos_id, split_order['orderId']))
    order = order.findall(".//Order")[0]
    split_order['state'] = order.get('state')
    if order.get('state') != "IN_PROGRESS":
        logger.debug("_try_to_fix_totaled_order ORDER IN_PROGRESS")
        if order.get('state') in ('PAID', 'TOTALED'):
            db_conn.pquery("updateCurrentOrderId", posId=pos_id, orderId=order.get('orderId'))
        elif order.get('state') in ('VOIDED', 'SYSTEM_VOIDED'):
            _reset_split_service(
                pos_id=pos_id,
                db_conn=db_conn,
                service_id=table["serviceId"],
            )
            db_conn.pquery(
                "updateTableServiceStatus",
                serviceId=table["serviceId"],
                status=TableStatusDescr.IN_PROGRESS
            )
        else:
            db_conn.pquery("storeService", serviceId=table["serviceId"], posId=pos_id)
            logger.debug("_try_to_fix_totaled_order NOT FIXED")
            return False

    order = eTree.XML(order_picture(pos_id))
    if order.get('state') == "IN_PROGRESS":
        try:
            notify_ndiscount = table["statusDescr"] != TableStatusDescr.TOTALED
            total_order(
                pos_id=pos_id,
                db_conn=db_conn,
                blk_notify=False,
                notify_ndiscount=notify_ndiscount,
            )
        except (Exception,):
            logger.debug("_try_to_fix_totaled_order VOID_ORDER")
            void_order(
                pos_id=pos_id,
                order_id="",
                blk_notify=False,
            )

    logger.debug("_try_to_fix_totaled_order FIXED")
    return True


def handle_store_service(
    argv,     # type: List[str]
    msg=None  # type: MBEasyContext
):
    # type: (...)  -> None
    pos_id, table, table_id, user_id = get_parameters(argv, msg)
    order_id = argv[3] if len(argv) > 3 and argv[3] else None

    current_pos_id = table.get("currentPOSId")
    table_not_in_pos_id = not current_pos_id or int(current_pos_id) != pos_id
    table_is_seat_mode = table.get("properties", {}).get("MODE") == "seats"
    if table_not_in_pos_id and table_is_seat_mode:
        table_orders = [table_order.get("orderId") for table_order in table.get("orders")]
        if int(order_id) not in table_orders:
            logger.error("ERROR - Order {0} is not linked to table {1}".format(order_id, table_id))
            raise TableServiceException("$TABLE_ORDER_NOT_FOUND")

    db_conn = get_db_connection(pos_id=pos_id)
    if _table_is_totaled(table):
        totaled_orders = table.pop("splitOrders", None)
        if totaled_orders is not None:
            for split_order in totaled_orders:
                if split_order["state"] == "PAID":
                    sys_log_exception("Table is already PAID")
                    raise TableServiceException("$COULD_NOT_DESELECT_TABLE")

        _update_service_totals(
            db_conn=db_conn,
            pos_id=pos_id,
            table=table,
            order=eTree.XML(order_picture(pos_id=pos_id)),
        )

    if order_id is not None:
        _register_order_on_service(order_id, pos_id, table, db_conn)

    current_order_id = table.get("currentOrderId")
    if current_order_id is not None:
        try:
            db_conn.pquery("registerServiceOrder", serviceId=table["serviceId"], orderId=current_order_id, posId=pos_id)
        except (Exception,):
            logger.exception(UNEXPECTED_EXCEPTION)
            raise TableServiceException("$COULD_NOT_DESELECT_TABLE")

    if order_id is not None or current_order_id is not None:
        order_picture_xml = eTree.XML(order_picture(pos_id))
        if order_picture_xml.get("state") != "VOIDED":
            store_order(
                pos_id=pos_id,
                db_conn=db_conn,
            )

    try:
        db_conn.pquery("storeService", serviceId=table["serviceId"], posId=pos_id)
    except (Exception,):
        logger.exception(UNEXPECTED_EXCEPTION)
        raise TableServiceException("$COULD_NOT_DESELECT_TABLE")

    table_picture_xml, event_xml = handle_table_picture([pos_id, table_id], None, "TABLE_STORED", "TableService")
    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg, FM_XML, table_picture_xml)
    mb_context.MB_EasyEvtSend("TABLE_STORED", "TableService", event_xml, sourceid=int(pos_id))
    store_modified_xml = get_store_picture_xml(
        pos_id=pos_id,
        subject="STORE_MODIFIED",
        event_type="ServiceStored",
        cached=True,
    )
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "ServiceStored", store_modified_xml)


def handle_register_service_tender(argv, msg=None):
    pos_id, table, table_id, user_id = get_parameters(argv, msg)
    tender_id = argv[3] if len(argv) > 3 and argv[3] else None
    amount = argv[4] if len(argv) > 4 and argv[4] else None
    tender_detail = argv[5] if len(argv) > 5 and argv[5] else None

    if not all([tender_id, msg]):
        raise TableServiceException(INVALID_PARAMETERS)

    _validate_table_is_totaled(table)

    amount = D(amount) if amount is not None else None
    order = eTree.XML(order_picture(pos_id))
    due_amount = D(order.get("dueAmount") or "0.00")
    tip_amount = D(order.get("tip") or "0.00")

    if due_amount <= D(0):
        msg.token = TK_SYS_NAK
        mb_context.MB_EasyReplyMessage(msg)
        return

    if amount is None:
        amount = due_amount

    tender_tip = calculate_tender_tip(
        pos_id=pos_id,
        amount=amount,
        due_amount=due_amount,
        tip_amount=tip_amount,
    )

    order = tender_order(
        pos_id=pos_id,
        do_not_close=True,
        tender_id=tender_id,
        amount=str(amount),
        tip_amount=str(tender_tip),
        tender_detail=(tender_detail or "")
    )

    if amount > D(0):
        db_conn = get_db_connection(pos_id)

        db_conn.pquery(
            "registerServiceTenders",
            serviceId=table["serviceId"],
            totalTenderAmount=str(amount),
            PreparedIntegerTable=(','.join([table.get("currentPOSOrderID")])),
            orderTenderId=order.get("tenderId")
        )
        _update_service_totals(
            db_conn=db_conn,
            pos_id=pos_id,
            table=table,
            order=order,
        )

    table_picture_xml, _ = handle_table_picture([pos_id, table_id])
    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg, FM_XML, table_picture_xml)


def calculate_tender_tip(
    pos_id,         # type: int
    amount,         # type: D
    due_amount,     # type: D
    tip_amount,     # type: D
):
    # type: (...) -> Union[int, D]

    due_amount_without_tip = due_amount - tip_amount
    if not tender_has_tip(amount, due_amount_without_tip):
        return 0

    if tender_pays_products(due_amount_without_tip):
        if tender_has_change(amount, due_amount):
            return due_amount - due_amount_without_tip
        else:
            return amount - due_amount_without_tip

    order = eTree.XML(
        order_picture(
            pos_id=pos_id,
            no_cache=True,
        )
    )
    order_tips_list = [
        float(ot.get('tip'))
        for ot in order.findall("TenderHistory/Tender")
        if float(ot.get('tenderAmount')) != 0 and ot.get('tip') is not None
    ]

    if order_tips_list:
        order_tips = D(sum(order_tips_list))
        tender_tip = tip_amount - order_tips

        if amount > tender_tip:
            return tender_tip

    if tender_has_change(amount, due_amount):
        return due_amount

    return amount


def tender_has_tip(amount, due_amount_without_tip):
    return amount > due_amount_without_tip


def tender_pays_products(due_amount_without_tip):
    return due_amount_without_tip > 0


def tender_has_change(amount, due_amount):
    return amount > due_amount


def handle_clear_service_tenders(
    argv,       # type: List[str]
    msg=None,   # type: MBEasyContext
):
    # type: (...) -> None

    pos_id, table, table_id, _ = get_parameters(
        argv=argv,
        msg=msg,
    )

    service_tender_id = argv[3] if len(argv) > 3 and argv[3] else None
    delete_order_tender = argv[4].lower() == "true" if len(argv) > 4 and argv[4] else True
    db_conn = get_db_connection(pos_id=pos_id)

    order_id = _get_current_table_order_id(
        table=table,
    )
    if delete_order_tender and not order_id:
        message = "Not found order to clear tender posId/tableId/table: {}#{}#{}"
        message = message.format(pos_id, table_id, str(table))
        logger.info(message)
        raise TableServiceException(message)

    log_msg = "Clear service tenders of posId: tableId/orderId/serviceTenderId: {}#{}#{}#{}"
    logger.info(log_msg.format(pos_id, table_id, order_id, service_tender_id))

    if delete_order_tender:
        if not service_tender_id:
            clear_tender(
                pos_id=str(pos_id),
                order_id=order_id,
                blk_notify=False,
                tender_id="0",
                order_tender_id="0",
                keep_tender_records=True,
            )
        else:
            table_picture, _ = handle_table_picture([pos_id, table_id])
            table_picture_xml = eTree.XML(table_picture)
            service_tenders = table_picture_xml.findall(".//ServiceTenders/ServiceTender")
            for tender in service_tenders:
                if tender.get("serviceTenderId") == service_tender_id:
                    clear_tender(
                        pos_id=str(pos_id),
                        order_id=order_id,
                        blk_notify=False,
                        order_tender_id=tender.get("orderTenderId"),
                        keep_tender_records=True,
                    )

        order = eTree.XML(order_picture(
            pos_id=pos_id,
            order_ids=[order_id],
        )).find(".//Order")

        _update_service_totals(
            db_conn=db_conn,
            pos_id=pos_id,
            table=table,
            order=order,
        )

    db_conn.pquery("clearServiceTenders", serviceId=table["serviceId"], serviceTenderId=service_tender_id)

    table_picture_xml, _ = handle_table_picture([pos_id, table_id])
    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg, FM_XML, table_picture_xml)


def _get_current_table_order_id(
    table,      # type: Dict[str, str]
):
    # type: (...) -> str

    service_orders = list(filter(lambda x: x["state"] in ["TOTALED", "STORED"], table.get('splitOrders')))
    order_id = service_orders[0]["orderId"] if service_orders else None
    return order_id


def handle_table_ready4service(argv, msg=None):
    pos_id, _, table_id, _ = get_parameters(
        argv=argv,
        msg=msg,
        with_table=False,
    )
    table = handle_list_tables([pos_id, None, table_id], cached=True)[0]

    if int(table["type"]) != 1:
        raise TableServiceException("$ONLY_SEAT_TABLES_CAN_BE_MADE_AVAILABLE")

    if int(table["status"]) not in [7, 8]:
        logger.warning(TABLE_EXPECTED_STATUS.format(table["status"], 'CLOSED / ABANDONED'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    db_conn = get_db_connection(pos_id)
    if "serviceId" in table and table["serviceId"]:
        db_conn.pquery(
            "updateTableServiceStatus",
            serviceId=table["serviceId"],
            status=TableStatusDescr.AVAILABLE
        )
    else:
        db_conn.pquery(
            "updateTableServiceStatus",
            tableId=table["id"],
            status=TableStatusDescr.AVAILABLE
        )

    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)
    store_modified_xml = get_store_picture_xml(pos_id, subject="TABLE_READY", event_type="TableService")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "TableReady", store_modified_xml)


def handle_move_table(argv, msg=None):
    pos_id, user_id, from_table, dest_table = get_parameters_extended(argv, msg)
    num_seats = int(argv[4]) if len(argv) > 4 and argv[4] else None
    tables_to_set_available = json.loads(argv[5]) if len(argv) > 5 and argv[5] else None
    mode = argv[6] if len(argv) > 6 and argv[6] else dest_table['typeDescr'].lower()
    ignore_totaled_orders = argv[7].lower() == "true" if len(argv) > 7 else False

    from_table_status = int(from_table["status"])
    if from_table_status != 4 and not (_is_tab(from_table) and from_table_status in [4, 6]):     # In-Progress, Totaled
        logger.warning(TABLE_EXPECTED_STATUS.format(from_table["status"], 'INPROGRESS'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    if int(dest_table["status"]) not in [1, 3, 4]:     # Available, Seated, In-Progress
        ok_states = 'AVAILABLE / SEATED / INPROGRESS'
        logger.warning(TABLE_EXPECTED_STATUS.format(dest_table["status"], ok_states))
        raise TableServiceException(INVALID_TABLE_STATUS)

    from_table_is_tab = _is_tab(from_table)
    _fix_table_link_if_needed(
        pos_id=pos_id,
        table=dest_table,
    )

    db_conn = get_db_connection(pos_id)
    service_seats = dest_table['serviceSeats']
    dest_table['serviceSeats'] = str(num_seats) if service_seats != str(num_seats) else service_seats
    new_service = create_new_service(
        db_conn=db_conn,
        dest_table=dest_table,
        from_table=from_table,
        num_seats=num_seats,
        pos_id=pos_id,
        user_id=user_id,
        mode=mode or "seats",
        msg=msg
    )

    if mode == "seats":
        maximum_seats = int(dest_table["serviceSeats"])
        if int(from_table["serviceSeats"]) > maximum_seats and from_table["orders"] is not None:
            orders_ids = [order["orderId"] for order in from_table["orders"]]
            for order in eTree.XML(order_picture(pos_id, orders_ids)).findall(".//Order"):
                _handle_maximum_seats(
                    order=order,
                    pos_id=pos_id,
                    maximum_seats=maximum_seats,
                )

        if 'serviceSeats' in dest_table.keys() and dest_table['serviceSeats']:
            num_seats = int(dest_table['serviceSeats'])

        sliced_services = dest_table.get('slicedServices')
        if sliced_services is not None:
            has_sliced_services = filter(
                lambda service:
                service['tableId'] == from_table.get('id'),
                dest_table.get('slicedServices')
            )
            num_seats = service_seats if has_sliced_services else num_seats

        if ignore_totaled_orders:
            _reset_split_service(
                pos_id=pos_id,
                db_conn=db_conn,
                service_id=from_table["serviceId"],
            )

        dest_service_id = (new_service["serviceId"] if new_service is not None else dest_table["serviceId"])
        db_conn.pquery(
            "moveTable",
            fromServiceId=from_table["serviceId"],
            destServiceId=dest_service_id,
            numberOfSeats=num_seats
        )

        dest_table_picture = eTree.XML(handle_table_picture([pos_id, dest_table["id"]])[0])
        dest_table = handle_list_tables([pos_id, None, dest_table["id"]], cached=False)[0]
        _transfer_custom_properties(
            pos_id=pos_id,
            source_table=from_table,
            destination_table=dest_table,
            msg=msg,
        )
        dest_orders = dest_table_picture.findall(".//Order")

        for dest_order in dest_orders:
            order_id = dest_order.get("orderId")
            src_pos_id = str(int(dest_order.get("originatorId")[3:]))
            session_id = get_user_session_id(pos_id, user_id, db_conn)

            recall_order(pos_id, order_id, session_id, src_pos_id, True, dest_table_picture.get("serviceId"))
            properties = eTree.Element("Properties")
            eTree.SubElement(properties, "Property", {"key": "TABLE_ID", "value": dest_table_picture.get("id")})
            eTree.SubElement(properties, "Property", {"key": "TABLE_NAME", "value": dest_table_picture.get("name")})
            eTree.SubElement(properties, "Property", {"key": "TAB_ID", "value": "clear"})

            set_order_properties(pos_id, False, order_id, properties)
            store_order(
                pos_id=pos_id,
                db_conn=db_conn,
            )

        split_services = _get_split_services_from_database(
            service_id=dest_service_id,
            db_conn=db_conn,
        )

        table_split_order_id = split_services[0].get("splitOrderId") if split_services else None
        if table_split_order_id:
            order = eTree.XML(order_picture(
                pos_id=pos_id,
                order_ids=table_split_order_id,
            ))
            _update_service_totals(
                db_conn=db_conn,
                pos_id=pos_id,
                table=dest_table,
                order=order,
            )
    else:
        dest_table = handle_list_tables([pos_id, None, dest_table["id"]], cached=False)
        dest_table = dest_table[0] if dest_table else None

        if from_table_is_tab:
            handle_set_parent_table(
                argv=[pos_id, user_id, from_table["id"], dest_table["id"], from_table_is_tab],
                msg=msg
            )
        else:
            from_table_picture = eTree.XML(handle_table_picture([pos_id, from_table["id"]])[0])
            service_children = from_table_picture.find("ServiceChildren")
            for service_child in service_children:
                handle_set_parent_table(
                    argv=[pos_id, user_id, service_child.attrib["tableId"], dest_table["id"]],
                    msg=msg
                )

    if tables_to_set_available:
        for table_to_set_available in tables_to_set_available:
            if mode == "tabs" and from_table_is_tab and from_table['id'] == table_to_set_available:
                continue
            db_conn.pquery(
                "updateTableServiceStatus",
                tableId=table_to_set_available,
                status=TableStatusDescr.AVAILABLE
            )

    if int(dest_table["status"]) in [2, 3]:
        db_conn.pquery(
            "updateTableServiceStatus",
            serviceId=dest_table["serviceId"],
            status=TableStatusDescr.IN_PROGRESS
        )

    if _is_tab(from_table) and mode == "seats":
        db_conn.pquery(
            "updateTableServiceStatus",
            serviceId=from_table["serviceId"],
            status=TableStatusDescr.CLOSED
        )

    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)
    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="MoveTable")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "MoveTable", store_modified_xml)


def handle_move_table_order(
        argv,       # type: List[str]
        msg=None    # type: MBEasyContext
):
    # type: (...) -> None

    pos_id, user_id, from_table, dest_table = get_parameters_extended(argv, msg)
    order_id = int(argv[4])
    num_seats = int(argv[5]) if len(argv) > 5 and argv[5] else None

    if int(from_table["status"]) != 4:
        logger.warning(TABLE_EXPECTED_STATUS.format(from_table["status"], 'INPROGRESS'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    if int(dest_table["status"]) not in [1, 2, 3, 4]:
        logger.warning(TABLE_EXPECTED_STATUS.format(dest_table["status"], OK_STATES))
        raise TableServiceException(INVALID_TABLE_STATUS)

    db_conn = get_db_connection(pos_id)
    create_new_service(
        db_conn=db_conn,
        dest_table=dest_table,
        from_table=from_table,
        num_seats=num_seats,
        pos_id=pos_id,
        user_id=user_id,
        mode="seats",
        msg=msg
    )

    dest_table = handle_list_tables([pos_id, None, dest_table["id"]], cached=False)
    dest_table = dest_table[0] if dest_table else None

    _register_order_on_service(order_id, pos_id, dest_table, db_conn, unregister_from_table=from_table)
    _link_order_with_table(False, {}, order_id, pos_id, dest_table)

    if int(dest_table["status"]) in [2, 3]:
        db_conn.pquery(
            "updateTableServiceStatus",
            serviceId=dest_table["serviceId"],
            status=TableStatusDescr.IN_PROGRESS
        )

    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)
    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="MoveTableOrder")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "MoveTableOrder", store_modified_xml)


def handle_move_child_table(
        argv,       # type: List[str]
        msg=None    # type: MBEasyContext
):
    # type: (...) -> None

    pos_id, user_id, from_table, dest_table = get_parameters_extended(argv, msg)
    child_table_id = argv[4]
    custom_props = json.loads(argv[5]) if len(argv) > 5 and argv[5] else {}
    custom_props = [{'propKey': k, 'propValue': v} for k, v in custom_props.items()]

    if int(from_table["status"]) not in [3, 4]:
        logger.warning(TABLE_EXPECTED_STATUS.format(from_table["status"], 'SEATED / INPROGRESS'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    if int(dest_table["status"]) not in [1, 2, 3, 4]:
        logger.warning(TABLE_EXPECTED_STATUS.format(dest_table["status"], OK_STATES))
        raise TableServiceException(INVALID_TABLE_STATUS)

    db_conn = get_db_connection(pos_id)
    create_new_service(db_conn, dest_table, from_table, 1, pos_id, user_id)

    dest_table = handle_list_tables([pos_id, None, dest_table["id"]], cached=False)
    dest_table = dest_table[0] if dest_table else None

    if dest_table and custom_props:
        db_conn.pquery(
            "setServiceCustomProperty",
            serviceId=(dest_table["serviceId"]),
            PreparedStringTable=_make_json(custom_props)
        )

    handle_set_parent_table([pos_id, user_id, child_table_id, dest_table["id"]], msg)

    if int(dest_table["status"]) in [2, 3]:
        db_conn.pquery(
            "updateTableServiceStatus",
            serviceId=dest_table["serviceId"],
            status=TableStatusDescr.IN_PROGRESS
        )

    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)
    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="MoveChildTable")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "MoveChildTable", store_modified_xml)


def create_new_service(
    db_conn,        # type: Connection
    dest_table,     # type: Dict[str, Any]
    from_table,     # type: Dict[str, Any]
    num_seats,      # type: int
    pos_id,         # type: int
    user_id,        # type: int
    mode=None,      # type: Optional[str]
    msg=None        # type: Optional[MBEasyContext]
):
    # type: (...) -> Dict[str, Any]

    new_service = None
    if int(dest_table["status"]) == 1:
        cursor = db_conn.pselect(
            "startService",
            posId=pos_id,
            userId=user_id,
            businessPeriod=from_table["businessPeriod"],
            typeId=1,
            tableId=dest_table["id"],
            numberOfSeats=num_seats,
            statusDescr=TableStatusDescr.IN_PROGRESS
        )
        for row in cursor:
            new_service = dict([(cursor.get_name(i), row.get_entry(i)) for i in range(cursor.cols())])
            break

    if new_service and mode:
        handle_set_custom_property(
            argv=[pos_id, "MODE", mode, "", new_service["serviceId"]],
            msg=msg,
            notify_update=False
        )

    return new_service


def handle_join_tables(argv, msg=None):
    pos_id, user_id, from_table, dest_table = get_parameters_extended(argv, msg)
    num_seats = int(argv[4]) if len(argv) > 4 and argv[4] else None

    if _has_tabs(dest_table) and (_has_tabs(from_table) or _is_available(from_table)):
        _join_tables_with_children_tabs(
            pos_id,
            user_id,
            from_table,
            dest_table,
            num_seats
        )
    elif _is_seat(dest_table) and (_is_seat(from_table) or _is_available(from_table)):
        _join_tables_with_seats(
            pos_id,
            user_id,
            from_table,
            dest_table,
            num_seats
        )
    elif _has_tabs(dest_table) and _is_tab(from_table):
        _join_table_and_tab(
            pos_id,
            user_id,
            from_table,
            dest_table,
            num_seats
        )
    else:
        raise TableServiceException(
            "Incompatible tables, joining: "
            "from_table={!r}, dest_table={!r}".format(
                from_table.get("id"),
                dest_table.get("id")
            )
        )

    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)
    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="JoinTables")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "JoinTables", store_modified_xml)


def _join_tables_with_children_tabs(
    pos_id,      # type: Union[str, int]
    user_id,     # type: Union[str, int]
    from_table,  # type: Dict
    dest_table,  # type: Dict
    num_seats    # type: Optional[int]
):
    # type: (...) -> None

    assert _has_tabs(from_table) or _is_available(from_table)
    assert _has_tabs(dest_table)

    if num_seats is None:
        num_seats = 1

    _join_tables_common(
        pos_id,
        user_id,
        from_table,
        dest_table,
        num_seats
    )

    if from_table["serviceId"] is not None:
        db_conn = get_db_connection(pos_id)
        db_conn.query(
            """
            UPDATE TableService SET ParentServiceId = {0}
            WHERE ParentServiceId = {1} AND TableId LIKE 'TAB-%';
            """.format(
                dest_table["serviceId"],
                from_table["serviceId"]
            )
        )


def _join_tables_with_seats(
    pos_id,      # type: Union[str, int]
    user_id,     # type: Union[str, int]
    from_table,  # type: Dict
    dest_table,  # type: Dict
    num_seats    # type: Optional[int]
):
    # type: (...) -> None

    assert _is_seat(from_table) or _is_available(from_table)
    assert _is_seat(dest_table)

    _join_tables_common(
        pos_id,
        user_id,
        from_table,
        dest_table,
        num_seats
    )


def _join_table_and_tab(
    pos_id,      # type: Union[str, int]
    user_id,     # type: Union[str, int]
    from_table,  # type: Dict
    dest_table,  # type: Dict
    num_seats    # type: Optional[int]
):
    # type: (...) -> None

    assert _is_tab(from_table)
    assert _has_tabs(dest_table)

    _join_tables_common(
        pos_id,
        user_id,
        from_table,
        dest_table,
        num_seats
    )

    db_conn = get_db_connection(pos_id)
    db_conn.pquery("updateTableServiceStatus", serviceId=from_table["serviceId"], status="Closed")


def _join_tables_common(
    pos_id,      # type: Union[str, int]
    user_id,     # type: Union[str, int]
    from_table,  # type: Dict
    dest_table,  # type: Dict
    num_seats    # type: Optional[int]
):
    # type: (...) -> None

    from_table_links = from_table["linkedTables"]
    db_conn = get_db_connection(pos_id)

    if from_table_links:
        db_conn.pquery("unlinkTables", serviceId=from_table["serviceId"])
        for linked_table in from_table_links:
            db_conn.pquery(
                "linkTable",
                fromTableId=linked_table,
                destServiceId=dest_table["serviceId"],
                numberOfSeats=num_seats
            )

    if int(from_table["status"]) not in (1, 2, 3, 4):  # Available, Waiting2BSeated, Seated or InProgress
        logger.warning(TABLE_EXPECTED_STATUS.format(from_table["status"], OK_STATES))
        raise TableServiceException(INVALID_TABLE_STATUS)

    new_service = create_new_service(db_conn, dest_table, from_table, num_seats, pos_id, user_id)

    if int(from_table["status"]) == 1:
        db_conn.pquery(
            "linkTable",
            fromTableId=from_table["id"],
            destServiceId=(new_service["serviceId"] if new_service is not None else dest_table["serviceId"]),
            numberOfSeats=num_seats
        )
    else:
        _reset_split_service(
            pos_id=pos_id,
            db_conn=db_conn,
            service_id=from_table["serviceId"],
        )
        db_conn.pquery(
            "joinTables",
            fromServiceId=from_table["serviceId"],
            destServiceId=(new_service["serviceId"] if new_service is not None else dest_table["serviceId"]),
            numberOfSeats=num_seats
        )

    if int(dest_table["status"]) in [2, 3] and int(from_table["status"]) == 4:
        db_conn.pquery(
            "updateTableServiceStatus",
            serviceId=dest_table["serviceId"],
            status=TableStatusDescr.IN_PROGRESS
        )

    if num_seats:
        orders_ids = [order.get("orderId") for order in from_table.get("orders") or []]
        orders_ids.extend([order.get("orderId") for order in dest_table.get("orders") or []])

        if orders_ids:
            for order in eTree.XML(order_picture(pos_id, orders_ids)).findall(".//Order"):
                _handle_maximum_seats(
                    order=order,
                    pos_id=pos_id,
                    maximum_seats=num_seats,
                )


def _reset_split_service(
    pos_id,         # type: Union[int, str]
    db_conn,        # type: Connection
    service_id,     # type: str
):
    # type: (...) -> None

    cursor = db_conn.pselect(
        procname="getSplitOrdersByServiceId",
        serviceId=service_id,
    )

    split_orders = [row.get_entry("SplitOrderId") for row in cursor]

    for order_id in split_orders:
        void_order(
            pos_id=pos_id,
            order_id=order_id,
            blk_notify=False,
            order_status=OrderStatus.SYSTEM_VOIDED.value,
        )

    db_conn.pquery(
        procname="resetSplitService",
        serviceId=service_id,
    )


def _void_empty_orders_from_slice_service(
    pos_id,         # type: Union[int, str]
    db_conn,        # type: Connection
    service_id,     # type: str
):
    # type: (...) -> None

    cursor = db_conn.pselect(
        procname="getEmptyOrdersFromSliceService",
        serviceId=service_id,
    )

    sliced_orders = [row.get_entry("OrderId") for row in cursor]
    if not sliced_orders:
        return

    for order_id in sliced_orders:
        void_order(
            pos_id=pos_id,
            order_id=order_id,
            blk_notify=False,
            order_status=OrderStatus.ABANDONED.value,
        )

    db_conn.pquery(
        "deleteSystemVoidOrdersFromService",
        serviceId=service_id
    )


def handle_unlink_tables(argv, msg=None):
    pos_id, _, table_id, _ = get_parameters(
        argv=argv,
        msg=msg,
        with_table=False,
    )
    unlink_table_id = argv[3] if len(argv) > 3 and argv[3] else None

    tables = filter(lambda t: (t["id"] in (table_id, unlink_table_id)), handle_list_tables([pos_id], cached=True))
    if not tables:
        raise TableServiceException(NO_TABLE_FOUND)

    major_table = None
    table_unlink = None
    while tables:
        table = tables.pop()
        if table["id"] == table_id:
            major_table = table
            continue
        if table["id"] == unlink_table_id:
            table_unlink = table

    if unlink_table_id is not None and table_unlink is None:
        raise TableServiceException("$UNLINK_TABLE_NOT_FOUND")

    if table_unlink is not None and int(table_unlink["status"]) != 5:
        logger.warning(TABLE_EXPECTED_STATUS.format(table_unlink["status"], 'LINKED'))
        raise TableServiceException(INVALID_TABLE_STATUS)

    db_conn = get_db_connection(pos_id)
    if table_unlink:
        db_conn.pquery("unlinkTables", serviceId=major_table["serviceId"], tableId=table_unlink["id"])
        db_conn.pquery(
            "updateTableServiceStatus",
            serviceId=table_unlink["serviceId"],
            status=TableStatusDescr.CLOSED
        )
    else:
        db_conn.pquery("unlinkTables", serviceId=major_table["serviceId"])

    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)
    store_modified_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="UnlinkTables")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "UnlinkTables", store_modified_xml)


def _fix_table_link_if_needed(
    pos_id,    # type: int
    table,     # type: Dict
):
    # type: (...) -> None

    if table["statusDescr"] != TableStatusDescr.AVAILABLE:
        return

    if table['linkedTables']:
        _fix_linked_available_table(
            pos_id=pos_id,
            table=table,
        )
    elif table['parentLinkedTableId']:
        _fix_available_table_with_parent(
            pos_id=pos_id,
            table=table,
        )


def _fix_linked_available_table(
    pos_id,    # type: int
    table,     # type: Dict
):
    # type: (...) -> None

    db_conn = get_db_connection(pos_id=pos_id)
    db_conn.transaction_start()

    logger.info("Available table with id {} and table service {} have linked tables, fixing it".format(
        table["id"],
        table["serviceId"],
    ))

    try:
        for table_linked_id in table['linkedTables']:
            db_conn.pquery(
                "unlinkTables",
                serviceId=table["serviceId"],
                tableId=table_linked_id,
            )
            db_conn.pquery(
                "updateTableServiceStatus",
                serviceId=table_linked_id,
                status=TableStatusDescr.AVAILABLE
            )
            db_conn.query("COMMIT TRANSACTION;")
    except (Exception,):
        logger.exception("An error occurred while trying to fix linked available table with service id {}".format(
            table["serviceId"],
        ))
        db_conn.query("ROLLBACK TRANSACTION;")
    finally:
        db_conn.transaction_end()


def _fix_available_table_with_parent(
    pos_id,    # type: int
    table,     # type: Dict
):
    # type: (...) -> None

    logger.info("Available table with id {} and table service {} have a parent table, fixing it".format(
        table["id"],
        table["serviceId"],
    ))

    tables = list(filter(
        lambda t: (t["id"] in table['parentLinkedTableId']), handle_list_tables([pos_id], cached=True)
    ))
    if not tables:
        return

    parent_table = tables[0]

    db_conn = get_db_connection(pos_id=pos_id)
    db_conn.pquery(
        "unlinkTables",
        serviceId=parent_table["serviceId"],
        tableId=table["id"],
    )


def handle_store_picture(argv, msg=None):
    pos_id = int(argv[0])
    force = (int(argv[1]) > 0) if len(argv) > 1 and argv[1] else False
    store_picture_xml = get_store_picture_xml(pos_id)
    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(msg, FM_XML, store_picture_xml)
    if force:
        event_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="StorePicture", cached=True)
        mb_context.MB_EasyEvtSend("STORE_MODIFIED", "StorePicture", event_xml)


def handle_change_table_user(argv, msg=None):
    pos_id = int(argv[0])
    user_id = int(argv[1]) if len(argv) > 1 and argv[1] else None
    table_id = argv[2] if len(argv) > 2 and argv[2] else None
    service_id = int(argv[3]) if len(argv) > 3 and argv[3] else None
    if not all([pos_id, user_id, any([table_id, service_id]), msg]):
        raise TableServiceException(INVALID_PARAMETERS)

    if service_id is None:
        table = handle_list_tables([pos_id, None, table_id], cached=True)
        if not table:
            raise TableServiceException(NO_TABLE_FOUND)
        table = table[0]
        service_id = int(table["serviceId"])

    db_conn = get_db_connection(pos_id)
    db_conn.pquery("updateServiceUser", serviceId=service_id, userId=user_id)
    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)
    event_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="TableUserChanged")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "TableUserChanged", event_xml)


def handle_change_number_of_seats(argv, msg=None):
    pos_id = int(argv[0])
    user_id = int(argv[1]) if len(argv) > 1 and argv[1] else None
    num_seats = int(argv[2]) if len(argv) > 2 and argv[2] else None
    table_id = argv[3] if len(argv) > 3 and argv[3] else None
    service_id = int(argv[4]) if len(argv) > 4 and argv[4] else None
    if not all([pos_id, user_id, num_seats, any([table_id, service_id]), msg]):
        raise TableServiceException(INVALID_PARAMETERS)

    if service_id is None:
        table = handle_list_tables([pos_id, None, table_id], cached=True)
        if not table:
            raise TableServiceException(NO_TABLE_FOUND)
        table = table[0]
        service_id = int(table["serviceId"])

        for order in table["orders"] or []:
            set_order_property(
                pos_id=pos_id,
                key="TABLE_SEATS",
                value=str(num_seats),
                order_id=order["orderId"],
            )

    db_conn = get_db_connection(pos_id)
    db_conn.pquery("updateServiceNumberOfSeats", serviceId=service_id, numberOfSeats=num_seats)
    msg.token = TK_SYS_ACK
    mb_context.MB_EasyReplyMessage(msg)
    event_xml = get_store_picture_xml(pos_id, subject="STORE_MODIFIED", event_type="NOSeatsChanged")
    mb_context.MB_EasyEvtSend("STORE_MODIFIED", "NOSeatsChanged", event_xml)


def handle_move_order_to_service(argv, msg=None):
    pos_id, table, table_id, user_id = get_parameters(argv, msg)
    order_id = argv[3] if len(argv) > 3 and argv[3] else None
    if not all([pos_id, order_id, table_id, msg]):
        raise TableServiceException(INVALID_PARAMETERS)

    table_picture_xml = _move_order_to_service(
        pos_id=pos_id,
        table_id=table_id,
        order_id=order_id,
        table=table,
    )

    msg.token = TK_SYS_ACK
    mb_context.MB_ReplyMessage(
        message=msg,
        format=FM_XML,
        data=table_picture_xml,
    )
    store_modified_xml = get_store_picture_xml(
        pos_id=pos_id,
        subject="STORE_MODIFIED",
        event_type="RecallService",
        cached=True,
    )
    mb_context.MB_EasyEvtSend(
        subject="STORE_MODIFIED",
        type="RecallService",
        xml=store_modified_xml,
    )


def _move_order_to_service(
    pos_id,         # type: int
    table_id,       # type: str
    order_id,       # type: int
    table=None,     # type: Dict
):
    # type: (...) -> eTree.Element

    db_conn = get_db_connection(pos_id)
    if table is None:
        table = _get_table_from_table_id(
            pos_id=pos_id,
            table_id=table_id,
            cached=False,
        )

    _register_order_on_service(order_id, pos_id, table, db_conn)
    _link_order_with_table(True, {}, order_id, pos_id, table)

    store_order(
        pos_id=pos_id,
        db_conn=db_conn,
    )
    try:
        db_conn.pquery("storeServiceOrder", serviceId=table["serviceId"])
    except (Exception,):
        logger.exception(UNEXPECTED_EXCEPTION)
        raise TableServiceException("$COULD_NOT_DESELECT_TABLE")

    table_picture_xml, _ = handle_table_picture([pos_id, table_id])
    return table_picture_xml


def handle_get_sectors(
    argv,       # type: List[Any]
    msg=None,   # type: Optional[MBEasyContext]
):
    # type: (...) -> None

    global SECTORS

    if SECTORS:
        msg.token = TK_SYS_ACK
        mb_context.MB_ReplyMessage(
            message=msg,
            format=FM_STRING,
            data=json.dumps(SECTORS),
        )
        return

    pos_id = argv[0]
    db_conn = get_db_connection(pos_id=pos_id)
    try:
        cursor = db_conn.pselect(procname="getSectors")
        SECTORS = [row.get_entry("Sector") for row in cursor if row.get_entry("Sector")]
        SECTORS.sort()
        if "$TABS" not in SECTORS:
            SECTORS.insert(0, "$TABS")
        msg.token = TK_SYS_ACK
        mb_context.MB_ReplyMessage(
            message=msg,
            format=FM_STRING,
            data=json.dumps(SECTORS),
        )
        return
    except Exception as ex:
        logger.exception(msg=ex.message)
        raise TableServiceException(errmsg="$COULD_NOT_GET_SECTORS_FROM_STORE")


def handle_get_operator_sector_info(
    argv,       # type: List[Any]
    msg=None,   # type: Optional[MBEasyContext]
):
    # type: (...) -> None

    global OPERATORS_BY_SECTORS

    pos_id = argv[0]
    force = argv[1].lower() == "true" if len(argv) > 1 and argv[1] else False
    if OPERATORS_BY_SECTORS and not force:
        msg.token = TK_SYS_ACK
        mb_context.MB_ReplyMessage(
            message=msg,
            format=FM_STRING,
            data=json.dumps(OPERATORS_BY_SECTORS),
        )
        return

    db_conn = get_db_connection(pos_id=pos_id)
    try:
        cursor = db_conn.pselect(procname="getOperatorsBySector")
        OPERATORS_BY_SECTORS = dict([
            (row.get_entry("SectorName"), json.loads(row.get_entry("Operators") or "[]"))
            for row in cursor
        ])

        msg.token = TK_SYS_ACK
        mb_context.MB_ReplyMessage(
            message=msg,
            format=FM_STRING,
            data=json.dumps(OPERATORS_BY_SECTORS),
        )
        return
    except Exception as ex:
        logger.exception(msg=ex.message)
        raise TableServiceException(errmsg="$COULD_NOT_GET_OPERATOR_BY_SECTOR_INFO")


def handle_update_operator_sector_info(
    argv,       # type: List[Any]
    msg=None,   # type: Optional[MBEasyContext]
):
    # type: (...) -> None

    global OPERATORS_BY_SECTORS

    pos_id = argv[0]
    db_conn = get_db_connection(pos_id=pos_id)
    update_info = json.loads(argv[1] if len(argv) > 1 and argv[1] else "{}")
    insert_rows = update_info.get("insert") or []
    delete_rows = update_info.get("delete") or []

    try:
        delete_rows = ",".join(["(\"{}\", {})".format(r[0], r[1]) for r in delete_rows])
        if delete_rows:
            query = """
                DELETE FROM tblservice.OperatorsSector
                WHERE (SectorName, OperatorId) IN (VALUES {});
                """.format(delete_rows)
            db_conn.query(sql=query)

        insert_rows = ",".join(["(\"{}\", {})".format(r[0], r[1]) for r in insert_rows])
        if insert_rows:
            query = """
                INSERT OR REPLACE INTO tblservice.OperatorsSector
                VALUES {};
            """.format(insert_rows)
            db_conn.query(sql=query)
    except Exception as ex:
        logger.exception(msg=ex.message)
        raise TableServiceException(errmsg="$COULD_NOT_UPDATE_OPERATOR_BY_SECTOR_INFO")

    return handle_get_operator_sector_info(
        argv=[pos_id, "true"],
        msg=msg,
    )


def _transfer_custom_properties(
    pos_id,             # type: int
    source_table,       # type: Dict
    destination_table,  # type: Dict
    msg,                # type: MBEasyContext
):
    # type: (...) -> None

    customer_info = source_table.get("properties", {}).get(LOYALTY_TABLE_CUSTOMER_INFO) or "{}"
    available_benefits = source_table.get("properties", {}).get(LOYALTY_TABLE_CUSTOMER_BENEFITS) or "{}"
    benefits_to_apply = source_table.get("properties", {}).get(LOYALTY_TABLE_COUPONS_TO_APPLY_ON_TOTAL) or "{}"

    properties = [
        (LOYALTY_TABLE_CUSTOMER_INFO, customer_info),
        (LOYALTY_TABLE_CUSTOMER_BENEFITS, available_benefits),
        (LOYALTY_TABLE_COUPONS_TO_APPLY_ON_TOTAL, benefits_to_apply),
    ]

    destination_table_id = destination_table.get("id")
    transfer_customer_info = False

    for label, property_str in properties:
        source_property = json.loads(s=property_str)
        if not source_property:
            continue

        seat = source_property.pop("from_seat", None)

        destination_property = destination_table.get('properties', {}).get(label, {})
        if destination_property:
            destination_property = json.loads(s=destination_property)
            if seat and not destination_property.get(seat):
                destination_property[seat] = source_property.get('table')
        else:
            destination_property = source_property
            transfer_customer_info = transfer_customer_info or label == LOYALTY_TABLE_CUSTOMER_INFO

        destination_property = json.dumps(obj=destination_property)
        handle_set_custom_property(
            argv=[pos_id, label, destination_property, destination_table_id, ""],
            msg=msg,
            notify_update=False,
        )

    if transfer_customer_info:
        customer_doc = source_table.get('properties', {}).get('CUSTOMER_DOC')
        if customer_doc:
            handle_set_custom_property(
                argv=[pos_id, 'CUSTOMER_DOC', customer_doc, destination_table_id, ""],
                msg=msg,
                notify_update=False,
            )
        customer_name = source_table.get('properties', {}).get('CUSTOMER_NAME')
        if customer_name:
            handle_set_custom_property(
                argv=[pos_id, 'CUSTOMER_NAME', customer_name, destination_table_id, ""],
                msg=msg,
                notify_update=False,
            )


def _merge_benefits(merged_benefits, benefit):
    if not merged_benefits:
        return benefit

    merged_benefits["benefitPreTotalSequence"].extend(benefit["benefitPreTotalSequence"])
    merged_benefits["discountIdTipExcluded"].extend(benefit["discountIdTipExcluded"])
    merged_benefits["tenderIdTipExcluded"].extend(benefit["tenderIdTipExcluded"])

    benefit_list = json.loads(merged_benefits["benefitList"])
    benefit_list.extend(json.loads(benefit["benefitList"]))
    merged_benefits["benefitList"] = json.dumps(benefit_list)

    return merged_benefits


def _get_table_service_period(
    pos_id,     # type: int
    table,      # type: Dict[str, str]
):
    # type: (...) -> Optional[Dict[str, Union[str, int]]]

    if "properties" in table and table["properties"] is not None and "SERVICE_PERIOD_TABLE" in table["properties"]:
        table_service_period = json.loads(table["properties"]["SERVICE_PERIOD_TABLE"])
        if table_service_period:
            return _build_service_period_from_property(prop=table_service_period)

    pos_service = PosService(
        message_bus=MbContextMessageBus(mbcontext=mb_context),
        pos_id=pos_id,
    )
    model = pos_service.get_model()

    service_period_tag = model.find("Custom[@name='SERVICE_PERIOD']")
    if service_period_tag is None:
        return None

    service_period_dict = json.loads(service_period_tag.attrib["value"])
    if "sectors" in service_period_dict:
        table_sector = table["sector"]
        for sector_name, sector_service_period in service_period_dict["sectors"].items():
            if sector_name == table_sector:
                return _build_service_period_from_property(prop=sector_service_period)

    if "servicePeriodId" in service_period_dict:
        return _build_service_period_from_property(prop=service_period_dict)


def _build_service_period_from_property(
    prop,       # type: Dict[str, Union[str, int]]
):
    # type: (...) -> Dict[str, Union[str, int]]

    return {
        "id": prop["servicePeriodId"],
        "name": prop["servicePeriodName"],
        "abbr": prop["servicePeriodAbbr"],
        "priceListId": prop["servicePeriod"],
    }


def _get_table_service_period_id(
    pos_id,     # type: int
    table,      # type: Dict[str, str]
):
    # type: (...) -> Optional[int]

    service_period = _get_table_service_period(
        pos_id=pos_id,
        table=table,
    )
    if service_period is None:
        return None

    return service_period["id"]


def _get_utf8_value(
    value,  # type: str
):
    # type: (...) -> str

    try:
        return value.decode("utf-8")
    except (ValueError, AttributeError,):
        return value


def _get_services_tenders_from_database(
    pos_id,         # type: int
    service_id,     # type: str
    order_id,       # type: eTree.Element
):
    # type: (...) -> Optional[List]

    order = eTree.XML(order_picture(
        pos_id=pos_id,
        order_ids=[order_id]
    )).find(".//Order")
    order_pos_id = order.get("posId")

    db_conn = get_db_connection(pos_id=order_pos_id)
    cursor = db_conn.pselect(
        procname="getServiceTenders",
        serviceId=service_id
    )

    cols = range(cursor.cols())
    return [
        dict([(cursor.get_name(i), _get_utf8_value(value=row.get_entry(i))) for i in cols])
        for row in cursor
    ]


def _get_split_services_from_database(
    service_id,     # type: str
    db_conn,        # type: Connection
):
    # type: (...) -> Optional[List]

    cursor = db_conn.pselect(
        procname="getSplitServiceOrders",
        serviceId=service_id
    )

    cols = range(cursor.cols())
    return [
        dict([(cursor.get_name(i), _get_utf8_value(value=row.get_entry(i))) for i in cols])
        for row in cursor
    ]


def handle_reset_store_layout(
    argv,       # type: List[Any]
    msg=None,   # type: Optional[MBEasyContext]
):
    # type: (...) -> None

    try:
        logger.debug("Called handle reset store layout from pos: %s", argv[0])
        reset_store_layout_setup()
        msg.token = TK_SYS_ACK
    except Exception as ex:
        logger.exception(msg=ex.message)
        msg.token = TK_SYS_NAK

    mb_context.MB_ReplyMessage(
        message=msg,
        format=FM_STRING,
    )


def handle_event(msg, is_message=False):
    if not order_modified_by_message:
        msg.token = TK_SYS_ACK
        mb_context.MB_EasyReplyMessage(msg)

    update_order_state(msg, is_message)


def update_order_state(msg, is_message):
    try:
        params = []
        if msg.data:
            params = msg.data.split('\0')

        if is_message:
            xml = eTree.XML(params[0])
            params.append(xml.get("subject"))
            params.append(xml.get("type"))
        elif len(params) < 4:
            return

        if params[1] == "ORDER_MODIFIED":
            ignored_events = {
                "ORDER_PROPERTIES_CHANGED": True,
                "CUSTOM_PROPERTY": True,
                "DO_TENDER": True,
                "DO_SALE": True,
                "DO_OPTION": True,
                "DISCOUNT_APPLIED": True,
                "DISCOUNT_REMOVED": True,
                "CLEAR_OPTION": True,
                "CLEAR_TENDERS": True,
                "VOID_LINE": True
            }

            if params[2] in ignored_events:
                return

            logger.debug("Handling%s: %s %s", "" if is_message else " Event", params[1], params[2:])
            order = eTree.XML(params[0])
            order = order.findall(".//Order")[0]
            order_id = order.get("orderId")
            state = order.get("state")
            pos_id = order.get("instance") or order.get("posId") or params[4]
            if pos_id is None:
                logger.error("Error Handling event. PosId is None for order {}".format(order_id))
                return

            db_conn = get_db_connection(pos_id)
            for count in range(3):
                try:
                    db_conn.pquery("updateCurrentOrderState", orderId=order_id, state=state)
                    logger.debug("Successfully processed: %s", params[1])
                    break
                except persistence.AprDbdException:
                    if count >= 2:
                        message = "POS {} - Error updating Order {} to Status {} - Event {}"
                        logger.exception(message.format(pos_id, order_id, state, params[2:]))
                    time.sleep(0.100)

    except (Exception,):
        logger.exception(UNEXPECTED_EXCEPTION)


lock_message = threading.Lock()
handling_message = {}


def handle_message(msg):
    global handling_message
    pos_id = None
    error_message = None
    handling = False
    msg_name = None
    is_table_info = False
    try:
        msg.token = TK_SYS_NAK
        msg_params = msg.data.split('\0')
        check_message_parameters(msg, msg_params)

        msg_name = msg_params[0]
        logger.debug("Handling: %s %s", msg_name, msg_params[1:])
        pos_id = msg_params[1]
        func = globals().get("handle_{0}".format(msg_name.lower()))
        is_table_info = msg_params[0].lower() == "table_info"

        if not is_table_info:
            with lock_message:
                if int(pos_id) not in handling_message:
                    handling_message[int(pos_id)] = False

            time_limit = (time.time() + 5.0)
            while time.time() < time_limit:
                with lock_message:
                    if handling_message[int(pos_id)] is False:
                        handling = True
                        handling_message[int(pos_id)] = True
                        break
                logger.debug("Waiting for another Operation on POS {0}".format(pos_id))
                time.sleep(0.100)
            else:
                logger.error("ERROR - Nested Operations POS {0} / {1}".format(pos_id, msg_name))
                raise TableServiceException("$NESTED_TABLE_OPERATIONS")

        if func and callable(func):
            func(msg_params[1:], msg)
            msg = None  # at this point, the message was already replied

        logger.debug("Successfully processed: %s", msg_name)
    except Exception as e:
        sys_log_exception("Error handling message on Table Service: {0}".format(msg_name))
        logger.exception("Error handling message on Table Service: {0}".format(msg_name))
        error_message = str(e.get_description())
    finally:
        if not is_table_info:
            with lock_message:
                if pos_id is not None and handling:
                    handling_message[int(pos_id)] = False

    if msg is not None:
        if error_message is not None:
            mb_context.MB_ReplyMessage(msg, FM_STRING, error_message)
        else:
            mb_context.MB_EasyReplyMessage(msg)


def check_message_parameters(msg, msg_params):
    if msg.format != FM_PARAM or msg.data is None:
        raise TableServiceException(INVALID_PARAMETERS)
    if len(msg_params) < 2:
        raise TableServiceException(INVALID_PARAMETERS)
    if msg_params[0] not in MANAGED_TOKENS:
        raise TableServiceException(INVALID_PARAMETERS)


def main_loop():
    global thread_pool_size
    pool = ThreadPool(thread_pool_size)

    if not order_modified_by_message:
        mb_context.MB_EasyEvtSubscribe("ORDER_MODIFIED")

    while True:
        msg = mb_context.MB_EasyGetMessage()

        if (not msg) or (msg.token == TK_CMP_TERM_NOW):
            mb_context.MB_EasyReplyMessage(msg)
            break
        elif msg.token == TK_EVT_EVENT:
            pool.add_task(handle_event, msg)
        elif msg.token == TK_CMP_MESSAGE:
            pool.add_task(handle_message, msg)
        elif msg.token == TK_SALE_ORDER_MODIFIED:
            if order_modified_by_message:
                pool.add_task(handle_event, msg, True)
            msg.token = TK_SYS_ACK
            mb_context.MB_EasyReplyMessage(msg)
        else:
            msg.token = TK_SYS_NAK
            mb_context.MB_EasyReplyMessage(msg)

    pool.wait_complete()


def main():
    try:
        init_msgbus()
        init_persistence()
        read_config()
        start_cache_objects()
        reset_store_layout_setup()
        main_loop()
    except KeyboardInterrupt:
        sys.exit(SE_USERCANCEL)
