@RequestMapping(value = "/buildOrder.html", method = RequestMethod.GET)
  public ModelAndView get(HttpServletRequest request) {

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Redirecting to current order details page");
    }

    HttpSession session = request.getSession(true);
    String orderrestaurantid = (String) session.getAttribute("orderrestaurantid");
    String restaurantid = (String) session.getAttribute("restaurantid");
    Search search = (Search) session.getAttribute("search");

    if (orderrestaurantid != null) {
      restaurantid = orderrestaurantid;
    }

    if (restaurantid == null) {
      if (search == null) {
        return new ModelAndView("redirect:/home.html", null);
      } else {
        return new ModelAndView("redirect:/search.html" + search.getQueryString());
      }
    } else {
      Restaurant restaurant = restaurantRepository.findByRestaurantId(restaurantid);
      return new ModelAndView("redirect:/" + restaurant.getUrl());
    }
  }
  @ResponseBody
  @RequestMapping(value = "/admin/upload.ajax", method = RequestMethod.POST)
  public ResponseEntity<byte[]> upload(
      @RequestParam("restaurantId") String restaurantId,
      @RequestParam("file") CommonsMultipartFile file)
      throws Exception {

    Map<String, Object> model = new HashMap<String, Object>();

    try {
      S3Object object = new S3Object(basePath + "/" + restaurantId);
      object.setDataInputStream(file.getInputStream());
      object.setContentLength(file.getSize());
      object.setContentType(file.getContentType());
      S3Bucket bucket = s3Service.getBucket(bucketName);
      s3Service.putObject(bucket, object);

      Restaurant restaurant = restaurantRepository.findByRestaurantId(restaurantId);
      restaurant.setHasUploadedImage(true);
      restaurantRepository.saveRestaurant(restaurant);

      model.put("success", true);
    } catch (Exception ex) {
      LOGGER.error("", ex);
      model.put("success", false);
      model.put("message", ex.getMessage());
    }

    String json = jsonUtils.serializeAndEscape(model);
    final HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.TEXT_HTML);
    headers.setCacheControl("no-cache");
    return new ResponseEntity<byte[]>(json.getBytes("utf-8"), headers, HttpStatus.OK);
  }
  @SuppressWarnings("unchecked")
  @ResponseBody
  @RequestMapping(value = "/order/checkSpecialOffer.ajax", method = RequestMethod.POST)
  public ResponseEntity<byte[]> checkSpecialOfferAvailability(
      HttpServletRequest request, @RequestParam(value = "body") String body) throws Exception {

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Checking if special offer is applicable for order");
    }

    Map<String, Object> model = new HashMap<String, Object>();

    try {

      // Extract request parameters
      Map<String, Object> params = (Map<String, Object>) jsonUtils.deserialize(body);
      String restaurantId = (String) params.get("restaurantId");
      String orderId = (String) params.get("orderId");
      String specialOfferId = (String) params.get("specialOfferId");

      // Get the restaurant object
      Restaurant restaurant = restaurantRepository.findByRestaurantId(restaurantId);
      SpecialOffer specialOffer = restaurant.getSpecialOffer(specialOfferId);

      // Get the order object
      if (orderId != null) {
        Order order = orderRepository.findByOrderId(orderId);
        model.put("success", true);
        model.put("applicable", specialOffer.isApplicableTo(order));
      } else {
        model.put("success", true);
        model.put("applicable", specialOffer.isAvailableAt(new DateTime()));
      }
    } catch (Exception ex) {
      LOGGER.error("", ex);
      model.put("success", false);
      model.put("message", ex.getMessage());
    }
    return buildOrderResponse(model);
  }
  @SuppressWarnings("unchecked")
  @ResponseBody
  @RequestMapping(value = "/order/deliveryEdit.ajax", method = RequestMethod.POST)
  public ResponseEntity<byte[]> buildDeliveryEdit(@RequestParam(value = "orderId") String orderId)
      throws Exception {

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Building delivery options for orderId: " + orderId);
    }

    Map<String, Object> model = new HashMap<String, Object>();

    try {
      Order order = orderRepository.findByOrderId(orderId);
      Restaurant restaurant = order.getRestaurant();

      // Get opening times for today and the next three days
      DateTime currentTime = new DateTime();
      DateTime now =
          new DateTime(
              currentTime.getYear(),
              currentTime.getMonthOfYear(),
              currentTime.getDayOfMonth(),
              currentTime.getHourOfDay(),
              currentTime.getMinuteOfHour(),
              0,
              0);

      // Store if the restaurant is currently open
      boolean isOpen = restaurant.isOpen(currentTime);

      List<Integer> days = new ArrayList<Integer>();
      List<Set<LocalTime>> deliveryTimes = new ArrayList<Set<LocalTime>>();
      List<Set<LocalTime>> collectionTimes = new ArrayList<Set<LocalTime>>();

      // Get the remaining options for today first
      days.add(now.getDayOfWeek());
      DateTime[] openingAndClosingTimes = restaurant.getOpeningAndClosingTimes(now);
      DateTime earlyOpeningTime =
          openingAndClosingTimes[0] == null ? now : openingAndClosingTimes[0];
      DateTime lateOpeningTime =
          openingAndClosingTimes[2] == null ? now : openingAndClosingTimes[2];

      // For delivery times push the current time past the delivery time in minutes
      int deliveryTimeMinutes = restaurant.getDeliveryTimeMinutes();
      Pair<Set<LocalTime>, Set<LocalTime>> earlyDeliveryTimesPair =
          getTimeOptions(
              now.isBefore(earlyOpeningTime) ? earlyOpeningTime : now,
              openingAndClosingTimes[1],
              deliveryTimeMinutes);
      Pair<Set<LocalTime>, Set<LocalTime>> lateDeliveryTimesPair =
          getTimeOptions(
              now.isBefore(lateOpeningTime) ? lateOpeningTime : now,
              openingAndClosingTimes[3],
              deliveryTimeMinutes);
      earlyDeliveryTimesPair.first.addAll(lateDeliveryTimesPair.first);
      earlyDeliveryTimesPair.second.addAll(lateDeliveryTimesPair.second);

      // For delivery times push the current time past the collection time in minutes
      int collectionTimeMinutes = restaurant.getCollectionTimeMinutes();
      Pair<Set<LocalTime>, Set<LocalTime>> earlyCollectionTimesPair =
          getTimeOptions(
              now.isBefore(earlyOpeningTime) ? earlyOpeningTime : now,
              openingAndClosingTimes[1],
              collectionTimeMinutes);
      Pair<Set<LocalTime>, Set<LocalTime>> lateCollectionTimesPair =
          getTimeOptions(
              now.isBefore(lateOpeningTime) ? lateOpeningTime : now,
              openingAndClosingTimes[3],
              collectionTimeMinutes);
      earlyCollectionTimesPair.first.addAll(lateCollectionTimesPair.first);
      earlyCollectionTimesPair.second.addAll(lateCollectionTimesPair.second);

      // Add today's opening times
      deliveryTimes.add(earlyDeliveryTimesPair.first);
      collectionTimes.add(earlyCollectionTimesPair.first);

      // Now get the rest of the options for the remaining times
      for (int i = 0; i < 3; i++) {

        // Add any times after midnight from the previous list
        Set<LocalTime> deliveryTimesSet = new TreeSet<LocalTime>();
        Set<LocalTime> collectionTimesSet = new TreeSet<LocalTime>();
        deliveryTimesSet.addAll(earlyDeliveryTimesPair.second);
        collectionTimesSet.addAll(earlyCollectionTimesPair.second);

        // Now get the next set of opening and closing times
        now = now.plusDays(1);
        days.add(now.getDayOfWeek());

        openingAndClosingTimes = restaurant.getOpeningAndClosingTimes(now);
        earlyDeliveryTimesPair =
            getTimeOptions(
                openingAndClosingTimes[0], openingAndClosingTimes[1], deliveryTimeMinutes);
        lateDeliveryTimesPair =
            getTimeOptions(
                openingAndClosingTimes[2], openingAndClosingTimes[3], deliveryTimeMinutes);
        earlyDeliveryTimesPair.first.addAll(lateDeliveryTimesPair.first);
        earlyDeliveryTimesPair.second.addAll(lateDeliveryTimesPair.second);

        earlyCollectionTimesPair =
            getTimeOptions(
                openingAndClosingTimes[0], openingAndClosingTimes[1], collectionTimeMinutes);
        lateCollectionTimesPair =
            getTimeOptions(
                openingAndClosingTimes[2], openingAndClosingTimes[3], collectionTimeMinutes);
        earlyCollectionTimesPair.first.addAll(lateCollectionTimesPair.first);
        earlyCollectionTimesPair.second.addAll(lateCollectionTimesPair.second);

        // Add this day's time options to the list
        deliveryTimesSet.addAll(earlyDeliveryTimesPair.first);
        collectionTimesSet.addAll(earlyCollectionTimesPair.first);

        // Add these lists to the return
        deliveryTimes.add(deliveryTimesSet);
        collectionTimes.add(collectionTimesSet);
      }

      // Add the time options to the model
      model.put("days", days);
      model.put("deliveryTimes", deliveryTimes);
      model.put("collectionTimes", collectionTimes);
      model.put("collectionOnly", restaurant.getCollectionOnly());
      model.put("open", isOpen);
      model.put("success", true);

    } catch (Exception ex) {
      LOGGER.error("", ex);
      model.put("success", false);
      model.put("message", ex.getMessage());
    }
    return buildOrderResponse(model);
  }
  @SuppressWarnings("unchecked")
  @ResponseBody
  @RequestMapping(value = "/order/addSpecialOffer.ajax", method = RequestMethod.POST)
  public ResponseEntity<byte[]> addSpecialOfferToOrder(
      HttpServletRequest request, @RequestParam(value = "body") String body) throws Exception {

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Adding special offer to order: " + body);
    }

    Map<String, Object> model = new HashMap<String, Object>();

    try {

      // Extract request parameters
      Map<String, Object> params = (Map<String, Object>) jsonUtils.deserialize(body);
      String restaurantId = (String) params.get("restaurantId");
      String specialOfferId = (String) params.get("specialOfferId");
      List<String> itemChoices = (List<String>) params.get("itemChoices");
      List<String> itemChoiceCosts = (List<String>) params.get("itemChoiceCosts");
      Integer quantity = Integer.valueOf(params.get("quantity").toString());

      // Get the restaurant object
      Restaurant restaurant = restaurantRepository.findByRestaurantId(restaurantId);
      SpecialOffer specialOffer = restaurant.getSpecialOffer(specialOfferId);

      // Get the order out of the session
      HttpSession session = request.getSession(true);
      String orderId = (String) session.getAttribute("orderid");
      Order order;
      if (orderId == null) {
        order = buildAndRegister(session, restaurantId);
      } else {
        order = orderRepository.findByOrderId(orderId);
        if (order == null) {
          order = buildAndRegister(session, restaurantId);
        }
      }

      // Check if the special offer is applicable to this order
      if (!specialOffer.isApplicableTo(order)) {
        model.put("success", true);
        model.put("applicable", false);
      } else {
        // Wipe existing order if a new restaurant is selected
        if (!restaurantId.equals(order.getRestaurantId())) {
          order.setRestaurantId(restaurantId);
          order.setRestaurant(restaurant);
          order.getOrderItems().clear();
          order.getOrderDiscounts().clear();
        }

        // Build new order item
        OrderItem orderItem = new OrderItem();
        orderItem.setMenuItemNumber(specialOffer.getNumber());
        orderItem.setMenuItemId(specialOfferId);
        orderItem.setMenuItemTitle(specialOffer.getTitle());
        orderItem.setAdditionalItems(itemChoices);
        orderItem.setQuantity(quantity);
        double additionalCost = 0d;
        for (String itemChoiceCost : itemChoiceCosts) {
          additionalCost += Double.valueOf(itemChoiceCost);
        }
        orderItem.setCost(specialOffer.getCost() + additionalCost);

        // Add new order item to order and update
        order.addOrderItem(orderItem);
        order = orderRepository.saveOrder(order);

        // Update can checkout status of order
        session.setAttribute("cancheckout", order.getCanCheckout());

        // Update order restaurant id session attribute if any items present
        if (order.getOrderItems().size() > 0) {
          session.setAttribute("orderrestaurantid", order.getRestaurantId());
          session.setAttribute("orderrestauranturl", order.getRestaurant().getUrl());
        } else {
          session.removeAttribute("orderrestaurantId");
          session.removeAttribute("orderrestauranturl");
        }

        // Return success
        model.put("success", true);
        model.put("applicable", true);
        model.put("order", order);
      }
    } catch (Exception ex) {
      LOGGER.error("", ex);
      model.put("success", false);
      model.put("message", ex.getMessage());
    }
    return buildOrderResponse(model);
  }
  @SuppressWarnings("unchecked")
  @ResponseBody
  @RequestMapping(value = "/order/addItem.ajax", method = RequestMethod.POST)
  public ResponseEntity<byte[]> addToOrder(
      HttpServletRequest request, @RequestParam(value = "body") String body) throws Exception {

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Adding to order: " + body);
    }

    Map<String, Object> model = new HashMap<String, Object>();

    try {
      // Extract request parameters
      Map<String, Object> params = (Map<String, Object>) jsonUtils.deserialize(body);
      String restaurantId = (String) params.get("restaurantId");
      String itemId = (String) params.get("itemId");
      String itemType = (String) params.get("itemType");
      String itemSubType = (String) params.get("itemSubType");
      List<String> additionalItems = (List<String>) params.get("additionalItems");
      Integer quantity = Integer.valueOf(params.get("quantity").toString());

      // Get the restaurant object
      Restaurant restaurant = restaurantRepository.findByRestaurantId(restaurantId);
      MenuItem menuItem = restaurant.getMenuItem(itemId);

      // Build new order item
      OrderItem orderItem = new OrderItem();
      orderItem.setMenuItemNumber(menuItem.getNumber());
      orderItem.setMenuItemId(itemId);
      orderItem.setMenuItemTitle(menuItem.getTitle());
      orderItem.setMenuItemTypeName(itemType);
      orderItem.setMenuItemSubTypeName(itemSubType);
      orderItem.setAdditionalItems(additionalItems);
      orderItem.setQuantity(quantity);

      // Work out the cost of any additional Items
      double additionalItemCost = 0d;
      for (String additionalItemName : additionalItems) {
        if (StringUtils.hasText(itemType)) {
          MenuItemTypeCost menuItemTypeCost = menuItem.getMenuItemTypeCost(itemType);
          additionalItemCost +=
              menuItemTypeCost.getAdditionalItemCost() == null
                  ? 0d
                  : menuItemTypeCost.getAdditionalItemCost();
        } else if (menuItem.getAdditionalItemCost() != null) {
          additionalItemCost += menuItem.getAdditionalItemCost();
        } else {
          MenuItemAdditionalItemChoice additionalItemChoice =
              menuItem.getMenuItemAdditionalItemChoice(additionalItemName);
          additionalItemCost +=
              additionalItemChoice.getCost() == null ? 0d : additionalItemChoice.getCost();
        }
      }

      // Build the cost of the item
      if (StringUtils.hasText(itemType)) {
        MenuItemTypeCost menuItemTypeCost = menuItem.getMenuItemTypeCost(itemType);
        orderItem.setCost(menuItemTypeCost.getCost() + additionalItemCost);
      } else if (StringUtils.hasText(itemSubType)) {
        MenuItemSubType menuItemSubType = menuItem.getMenuItemSubType(itemSubType);
        orderItem.setCost(menuItemSubType.getCost() + additionalItemCost);
      } else {
        orderItem.setCost(menuItem.getCost() + additionalItemCost);
      }

      // Get the order out of the session
      HttpSession session = request.getSession(true);
      String orderId = (String) session.getAttribute("orderid");
      Order order;
      if (orderId == null) {
        order = buildAndRegister(session, restaurantId);
      } else {
        order = orderRepository.findByOrderId(orderId);
        if (order == null) {
          order = buildAndRegister(session, restaurantId);
        } else if (!restaurantId.equals(order.getRestaurantId())) {
          order.setRestaurant(restaurant);
          order.getOrderItems().clear();
          order.getOrderDiscounts().clear();
        }
      }

      // Add new order item to order and update
      order.addOrderItem(orderItem);
      order = orderRepository.saveOrder(order);

      // Update can checkout status of order
      session.setAttribute("cancheckout", order.getCanCheckout());

      // Update order restaurant id session attribute if any items present
      if (order.getOrderItems().size() > 0) {
        session.setAttribute("orderrestaurantid", order.getRestaurantId());
        session.setAttribute("orderrestauranturl", order.getRestaurant().getUrl());
      } else {
        session.removeAttribute("orderrestaurantid");
        session.removeAttribute("orderrestauranturl");
      }

      // Return success
      model.put("success", true);
      model.put("order", order);
    } catch (Exception ex) {
      LOGGER.error("", ex);
      model.put("success", false);
      model.put("message", ex.getMessage());
    }
    return buildOrderResponse(model);
  }
  @Scheduled(cron = "0 0/1 * * * ?")
  public void execute() {

    try {
      if (lock.acquire()) {

        List<Order> orders = orderRepository.findByOrderStatus(ORDER_STATUS_AWAITING_RESTAURANT);
        if (orders.size() > 0) {
          LOGGER.info("Found " + orders.size() + " orders with status 'AWAITING_RESTAURANT'");
        }

        for (Order order : orders) {

          DateTime orderPlacedTime = order.getOrderPlacedTime();
          String orderId = order.getOrderId();
          LOGGER.info("Order id: " + orderId + " was placed at: " + orderPlacedTime);

          Restaurant restaurant = order.getRestaurant();
          DateTime now = new DateTime();
          LOGGER.info("Current time is: " + now);

          String notificationStatus = order.getOrderNotificationStatus();
          LOGGER.info("Order id: " + orderId + " notification status is: " + notificationStatus);

          // Get the time the restaurant opened
          DateTime restaurantOpenedTime = restaurant.getEarlyOpeningTime(now);

          // Don't do anything if the restaurant is not currently open
          LOGGER.info(
              "Restaurant "
                  + restaurant.getName()
                  + " opened time today is: "
                  + restaurantOpenedTime);
          if (restaurantOpenedTime == null || restaurantOpenedTime.isAfter(now)) {
            LOGGER.info(
                "Restaurant "
                    + restaurant.getName()
                    + " has not opened yet, not doing any processing");
            continue;
          }

          // Auto cancel orders which have been awaiting confirmation for too long and the
          // restaurant has been open long enough to respond
          DateTime autoCancelCutoff = new DateTime().minusMinutes(minutesBeforeAutoCancelOrder);
          LOGGER.info("Auo cancel cutoff time is: " + autoCancelCutoff);
          if (orderPlacedTime.isBefore(autoCancelCutoff)
              && restaurantOpenedTime.isBefore(autoCancelCutoff)) {
            try {
              LOGGER.info(
                  "Order id: "
                      + orderId
                      + " has been awaiting confirmation for more than "
                      + minutesBeforeAutoCancelOrder
                      + " minutes, auto-cancelling");
              orderWorkflowEngine.processAction(order, ACTION_AUTO_CANCEL);
            } catch (WorkflowException e) {
              LOGGER.error(
                  "Exception sending auto cancel email for orderId: " + order.getOrderId(), e);
              order.setOrderStatus(ORDER_STATUS_AUTO_CANCELLED);
              orderRepository.saveOrder(order);
            } catch (Exception ex) {
              exceptionHandler.handleException(ex);
            }
            continue;
          }

          // Send email to customer giving them the option to cancel the order if it has been
          // awaiting confirmation for too long
          DateTime cancellationOfferCutoff =
              new DateTime().minusMinutes(minutesBeforeSendCancellationEmail);
          LOGGER.info("Cancellation offer cutoff time is: " + cancellationOfferCutoff);
          if (!order.getCancellationOfferEmailSent()
              && orderPlacedTime.isBefore(cancellationOfferCutoff)
              && restaurantOpenedTime.isBefore(cancellationOfferCutoff)) {
            try {
              LOGGER.info(
                  "Order id: "
                      + order.getOrderId()
                      + " has been awaiting confirmation for more than "
                      + minutesBeforeSendCancellationEmail
                      + " minutes, sending email");
              orderWorkflowEngine.processAction(order, ACTION_SEND_CANCEL_OFFER_TO_CUSTOMER);
            } catch (Exception ex) {
              exceptionHandler.handleException(ex);
            }
          }

          // If there is a call in progress then do not call again
          if (order.isCallInProgress()) {
            LOGGER.info(
                "There is already a call in progress for order id: "
                    + orderId
                    + ", not placing another call");
            continue;
          }

          // If restaurant did not respond at all, do not call again
          if (NOTIFICATION_STATUS_RESTAURANT_FAILED_TO_RESPOND.equals(notificationStatus)) {
            LOGGER.info(
                "Not attempting another call for order id:"
                    + orderId
                    + " as notification status is; "
                    + notificationStatus);
            continue;
          }

          // If call was answered but order not accepted/rejected, retry after 5 minutes otherwise
          // after 1 minute
          DateTime lastCallCutoff =
              new DateTime()
                  .minusSeconds(
                      NOTIFICATION_STATUS_RESTAURANT_ANSWERED.equals(notificationStatus)
                          ? secondsBeforeRetryAnsweredCall
                          : secondsBeforeRetryCall);
          DateTime lastCallTime = order.getLastCallPlacedTime();
          if (lastCallTime == null || lastCallTime.isBefore(lastCallCutoff)) {
            try {
              orderWorkflowEngine.processAction(order, ACTION_CALL_RESTAURANT);
            } catch (Exception ex) {
              LOGGER.error(
                  "Error occurred placing order call to restaurant for order id: "
                      + order.getOrderId());
            }
          }
        }
      }
    } catch (Exception ex) {
      LOGGER.error("Error occurred processing open orders: " + ex.getMessage(), ex);
    } finally {
      lock.release();
    }
  }