// CartService service NGApp.factory( 'CartService', function ( RestaurantService ) { var service = { uuidInc: 0, items: {}, uuid: function () { var id = 'c-' + service.uuidInc; service.uuidInc++; return id; }, add: function (item) { var id = service.uuid(), dish = App.cache('Dish', item); dish_options = dish.options(), options = []; if (arguments[1]) { options = arguments[1].options; } else { for (var x in dish_options) { if (dish_options[x]['default'] == 1) { options[options.length] = dish_options[x].id_option; } } } service.items[id] = {}; service.items[id].id = item; service.items[id].options = options; /* Template viewer stuff */ service.items[id].details = {}; service.items[id].details.id = id; service.items[id].details.name = dish.name; service.items[id].details.description = dish.description != null ? dish.description : ''; /* Customization stuff */ service.items[id].details.customization = {}; service.items[id].details.customization.customizable = ( dish.options().length > 0 ); service.items[id].details.customization.expanded = ( parseInt(dish.expand_view ) > 0 ); service.items[id].details.customization.options = service._parseCustomOptions( dish_options, options ); service.items[id].details.customization.rawOptions = dish_options; //TODO:: If it is a mobile add the items at the top #1035 service.updateTotal(); App.track('Dish added', { id_dish: dish.id_dish, name: dish.name }); }, clone: function (item) { var cart = service.items[item], newoptions = []; for (var x in cart.options) { newoptions[newoptions.length] = cart.options[x]; } service.add(cart.id, { options: newoptions }); App.track('Dish cloned'); }, remove: function (item) { App.track('Dish removed'); delete service.items[item]; service.updateTotal(); }, customizeItem: function (option, item) { var cartitem = service.items[item.details.id]; if (option) { if ( option.type == 'select' ) { var options = item.details.customization.rawOptions; for (var i in options) { if (options[i].id_option_parent != option.id_option) { continue; } for (var x in cartitem.options) { if ( cartitem.options[x] == options[i].id_option && options[i].id_option_parent == option.id_option ) { cartitem.options.splice(x, 1); break; } } } cartitem.options[cartitem.options.length] = option.selected; } else if ( option.type == 'check' ) { if (option.checked) { cartitem.options[cartitem.options.length] = option.id_option; } else { for (var x in cartitem.options) { if (cartitem.options[x] == option.id_option) { cartitem.options.splice(x, 1); break; } } } } } service.items[item.details.id] = cartitem; service.updateTotal(); }, /** * Gets called after the cart is updarted to refresh the total * * @todo Gets called many times before the cart is updated, on load, and shouldn't * * @return void */ updateTotal: function () { var totalText = (App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + this.charged(), tipText = '', feesText = '', totalItems = 0, credit = 0, hasFees = ((service.restaurant.delivery_fee && App.order.delivery_type == 'delivery') || service.restaurant.fee_customer) ? true : false; if (App.credit.restaurant[service.restaurant.id]) { credit = parseFloat(App.credit.restaurant[service.restaurant.id]); } for (var x in service.items) { totalItems++; } service.autotip(); /* If the user changed the delivery method to takeout and the payment is card * the default tip will be 0%. If the delivery method is delivery and the payment is * card the default tip will be autotip. * If the user had changed the tip value the default value will be the chosen one. */ var wasTipChanged = false; if (App.order.delivery_type == 'takeout' && App.order['pay_type'] == 'card') { if (typeof App.order.tipHasChanged == 'undefined') { App.order.tip = 0; wasTipChanged = true; } } else if (App.order.delivery_type == 'delivery' && App.order['pay_type'] == 'card') { if (typeof App.order.tipHasChanged == 'undefined') { App.order.tip = (App.config.user.last_tip) ? App.config.user.last_tip : 'autotip'; App.order.tip = App.lastTipNormalize(App.order.tip); wasTipChanged = true; } } if (wasTipChanged) { $('[name="pay-tip"]').val(App.order.tip); // Forces the recalculation of total because the tip was changed. totalText = (App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + this.charged(); } var _total = service.restaurant.delivery_min_amt == 'subtotal' ? service.subtotal() : service.total(); if (service.restaurant.meetDeliveryMin(_total) && App.order.delivery_type == 'delivery') { $('.delivery-minimum-error').show(); $('.delivery-min-diff').html(service.restaurant.deliveryDiff(_total)); } else { $('.delivery-minimum-error').hide(); } $('.cart-summary-item-count span').html(totalItems); /* If no items, hide payment line * .payment-total line for new customers * .dp-display-payment is for stored customers */ if (!this.subtotal()) { $('.payment-total, .dp-display-payment').hide(); } else { $('.payment-total, .dp-display-payment').show(); } var breakdown = service.totalbreakdown(); var extraCharges = service.extraChargesText(breakdown); if (extraCharges) { $('.cart-breakdownDescription').html((App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + this.subtotal().toFixed(2) + ' (+' + extraCharges + ')'); } else { $('.cart-breakdownDescription').html((App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + this.subtotal().toFixed(2)); } if (App.order.pay_type == 'card' && credit > 0) { var creditLeft = ''; if (this.total() < credit) { var creditLeft = ' - You\'ll still have ' + (App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + App.ceil((credit - this.total())).toFixed(2) + ' gift card left '; credit = this.total(); } $('.cart-gift').html(' (- ' + (App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + App.ceil(credit).toFixed(2) + ' credit ' + creditLeft + ') '); } else { $('.cart-gift').html(''); } setTimeout(function () { if (App.order.pay_type == 'cash' && credit > 0 /* && App.giftcard.showGiftCardCashMessage */ ) { $('.cart-giftcard-message').html('Pay with a card, NOT CASH, to use your ' + (App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + App.ceil(credit).toFixed(2) + ' gift card!'); } else { $('.cart-giftcard-message').html(''); } }, 1000); $('.cart-total').html(totalText); /** * Crunchbutton doesnt collect the cash, the restaurant will ring up * the order in their register, which may have different prices. The * restaurant collects the cash, so its posible things may be * different. This differs from when its card, crunchbutton collects * the money directly so the price cant vary */ if (App.order['pay_type'] == 'card') { $('.cash-order-aprox').html(''); $('.cart-paymentType').html('by card'); } else { $('.cash-order-aprox').html('approximately'); $('.cart-paymentType').html(''); } if (App.cartHighlightEnabled && $('.cart-summary').css('display') != 'none') { $('.cart-summary').removeClass('cart-summary-detail'); $('.cart-summary').effect('highlight', {}, 500, function () { $('.cart-summary').addClass('cart-summary-detail'); }); } if ($('.cart-total').html() == totalText) { //return; } if (!totalItems) { $('.default-order-check').hide(); } else { $('.default-order-check').show(); } var totalItems = {}, name, text = ''; $('.cart-summary-items').html(''); for (var x in service.items) { name = App.cached['Dish'][service.items[x].id].name; if (totalItems[name]) { totalItems[name]++; } else { totalItems[name] = 1; } } for (x in totalItems) { text = ',  ' + text; if (totalItems[x] > 1) { text = x + ' (' + totalItems[x] + ')' + text; } else { text = x + text; } } $('.cart-summary-items').html(text.substr(0, text.length - 13)); $('.cart-item-customize-price').each(function () { var dish = $(this).closest('.cart-item-customize').attr('data-id_cart_item'), option = $(this).closest('.cart-item-customize-item').attr('data-id_option'), cartitem = service.items[dish], opt = App.cached['Option'][option], price = opt.optionPrice(cartitem.options); $(this).html(service.customizeItemPrice(price)); }); }, customizeItemPrice: function (price, force) { if (price != '0.00' || force) { return ' (' + ( (price < 0) ? 'minus $' : '+ $' ) + parseFloat(Math.abs(price)).toFixed(2) + ')'; } return ''; }, /** * subtotal, delivery, fee, taxes and tip * * @category view */ extraChargesText: function (breakdown) { var elements = []; var text = ''; if (breakdown.delivery) { elements.push((App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + breakdown.delivery.toFixed(2) + ' delivery'); } if (breakdown.fee) { elements.push((App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + breakdown.fee.toFixed(2) + ' fee'); } if (breakdown.taxes) { elements.push((App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + breakdown.taxes.toFixed(2) + ' taxes'); } if (breakdown.tip && breakdown.tip > 0) { elements.push((App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + breakdown.tip + ' tip'); } if (elements.length) { if (elements.length > 2) { var lastOne = elements.pop(); var elements = [elements.join(', ')]; elements.push(lastOne); } var text = elements.join(' & '); } return text; }, getCart: function () { var cart = []; for (x in service.items) { cart[cart.length] = service.items[x]; } return cart; }, /** * Submits the cart order * * @returns void */ submit: function () { if (App.busy.isBusy()) { return; } App.busy.makeBusy(); var read = $('.payment-form').length ? true : false; if (read) { App.config.user.name = $('[name="pay-name"]').val(); App.config.user.phone = $('[name="pay-phone"]').val().replace(/[^\d]*/gi, ''); if (App.order['delivery_type'] == 'delivery') { App.config.user.address = $('[name="pay-address"]').val(); } App.order.tip = $('[name="pay-tip"]').val(); } var order = { cart: service.getCart(), pay_type: App.order['pay_type'], delivery_type: App.order['delivery_type'], restaurant: service.restaurant.id, make_default: $('#default-order-check').is(':checked'), notes: $('[name="notes"]').val(), lat: (App.loc.pos()) ? App.loc.pos().lat : null, lon: (App.loc.pos()) ? App.loc.pos().lon : null }; if (order.pay_type == 'card') { order.tip = App.order.tip || '3'; order.autotip_value = $('[name=pay-autotip-value]').val(); } if (read) { order.address = App.config.user.address; order.phone = App.config.user.phone; order.name = App.config.user.name; if (App.order.cardChanged) { order.card = { number: $('[name="pay-card-number"]').val(), month: $('[name="pay-card-month"]').val(), year: $('[name="pay-card-year"]').val() }; } else { order.card = {}; } } console.log('ORDER:', order); var errors = {}; if (!order.name) { errors['name'] = 'Please enter your name.'; } if (!App.phone.validate(order.phone)) { errors['phone'] = 'Please enter a valid phone #.'; } if (order.delivery_type == 'delivery' && !order.address) { errors['address'] = 'Please enter an address.'; } if (order.pay_type == 'card' && ((App.order.cardChanged && !order.card.number) || (!App.config.user.id_user && !order.card.number))) { errors['card'] = 'Please enter a valid card #.'; } if (!service.hasItems()) { errors['noorder'] = 'Please add something to your order.'; } if (!$.isEmptyObject(errors)) { var error = ''; for (var x in errors) { error += errors[x] + "\n"; } $('body').scrollTop($('.payment-form').position().top - 80); App.alert(error); App.busy.unBusy(); App.track('OrderError', errors); // Log the error App.log.order({ 'errors': errors }, 'validation error'); return; } // Play the crunch audio just once, when the user clicks at the Get Food button if (App.iOS() && !App.crunchSoundAlreadyPlayed) { App.playAudio('get-food-audio'); App.crunchSoundAlreadyPlayed = true; } // if it is a delivery order we need to check the address if (order.delivery_type == 'delivery') { // Correct Legacy Addresses in Database to Avoid Screwing Users #1284 // If the user has already ordered food if (App.config && App.config.user && App.config.user.last_order) { // Check if the order was made at this community if (App.config.user.last_order.communities.indexOf(service.restaurant.id_community) > -1) { // Get the last address the user used at this community var lastAddress = App.config.user.last_order.address; var currentAdress = $('[name=pay-address]').val(); // Make sure the the user address is the same of his last order if ($.trim(lastAddress) != '' && $.trim(lastAddress) == $.trim(currentAdress)) { App.isDeliveryAddressOk = true; // Log the legacy address App.log.order({ 'address': lastAddress, 'restaurant': service.restaurant.name }, 'legacy address'); } } } /* // Check if the user address was already validated if ( !App.isDeliveryAddressOk ) { // Use the aproxLoc to create the bounding box if( App.loc.aproxLoc ){ var latLong = new google.maps.LatLng( App.loc.aproxLoc ? App.loc.aproxLoc.lat : App.loc.pos().lat, App.loc.aproxLoc ? App.loc.aproxLoc.lon : App.loc.pos().lon); } // Use the restautant's position to create the bounding box - just for tests if( App.useRestaurantBoundingBox ){ var latLong = new google.maps.LatLng( service.restaurant.loc_lat, service.restaurant.loc_long ); } if( !latLong ){ //App.alert( 'Could not locate you!' ); //App.busy.unBusy(); //return; } var success = function( results ) { // Get the closest address from that lat/lng var theClosestAddress = App.loc.theClosestAddress( results, latLong ); var isTheAddressOk = App.loc.validateAddressType( theClosestAddress ); if( isTheAddressOk ){ // Now lets check if the restaurant deliveries at the given address var lat = theClosestAddress.geometry.location.lat(); var lon = theClosestAddress.geometry.location.lng(); if( App.useCompleteAddress ){ $( '[name=pay-address]' ).val( App.loc.formatedAddress( theClosestAddress ) ); } if (!service.restaurant.deliveryHere({ lat: lat, lon: lon})) { App.alert( 'Sorry, you are out of delivery range or have an invalid address. \nPlease check your address, or order takeout.' ); // Write the found address at the address field, so the user can check it. $( '[name=pay-address]' ).val( App.loc.formatedAddress( theClosestAddress ) ); // Log the error App.log.order( { 'address' : $( '[name=pay-address]' ).val(), 'restaurant' : service.restaurant.name } , 'address out of delivery range' ); App.busy.unBusy(); return; } else { if( App.completeAddressWithZipCode ){ // Get the address zip code var zipCode = App.loc.zipCode( theClosestAddress ); var typed_address = $( '[name=pay-address]' ).val(); // Check if the typed address already has the zip code if( typed_address.indexOf( zipCode ) < 0 ){ var addressWithZip = typed_address + ' - ' + zipCode; $( '[name=pay-address]' ).val( addressWithZip ); } } App.busy.unBusy(); App.isDeliveryAddressOk = true; service.submit(); } } else { // Address was found but it is not valid (for example it could be a city name) App.alert( 'Oops, it looks like your address is incomplete. \nPlease enter a street name, number and zip code.' ); App.busy.unBusy(); // Make sure that the form will be visible $('.payment-form').show(); $('.delivery-payment-info, .content-padder-before').hide(); $( '[name="pay-address"]' ).focus(); // Log the error App.log.order( { 'address' : $( '[name=pay-address]' ).val(), 'restaurant' : service.restaurant.name } , 'address not found or invalid' ); } } // Address not found! var error = function() { App.alert( 'Oops, it looks like your address is incomplete. \nPlease enter a street name, number and zip code.' ); App.busy.unBusy(); // Log the error App.log.order( { 'address' : $( '[name=pay-address]' ).val(), 'restaurant' : service.restaurant.name } , 'address not found' ); }; // Call the geo method App.loc.doGeocodeWithBound(order.address, latLong, success, error); return; } */ } if (order.delivery_type == 'takeout') { App.isDeliveryAddressOk = true; } App.isDeliveryAddressOk = true; if (!App.isDeliveryAddressOk) { return; } // Play the crunch audio just once, when the user clicks at the Get Food button if (!App.crunchSoundAlreadyPlayed) { App.playAudio('get-food-audio'); App.crunchSoundAlreadyPlayed = true; } $.ajax({ url: App.service + 'order', data: order, dataType: 'html', type: 'POST', complete: function (json) { try { json = $.parseJSON(json.responseText); } catch (e) { // Log the error App.log.order(json.responseText, 'processing error'); json = { status: 'false', errors: ['Sorry! Something went horribly wrong trying to place your order!'] }; } if (json.status == 'false') { var error = ''; for (x in json.errors) { error += json.errors[x] + "\n"; } App.track('OrderError', json.errors); App.alert(error); // Log the error App.log.order({ 'errors': json.errors }, 'validation error - php'); } else { if (json.token) { $.totalStorage('token', json.token); } $('.link-orders').show(); order.cardChanged = false; App.justCompleted = true; App.giftcard.notesCode = false; var totalItems = 0; for (var x in service.items) { totalItems++; } $.getJSON('/api/config', App.processConfig); App.cache('Order', json.uuid, function () { App.track('Ordered', { 'total': this.final_price, 'subtotal': this.price, 'tip': this.tip, 'restaurant': service.restaurant.name, 'paytype': this.pay_type, 'ordertype': this.order_type, 'user': this.user, 'items': totalItems }); App.order.cardChanged = false; App.loc.changeLocationAddressHasChanged = false; delete App.order.tipHasChanged; App.go('/order/' + this.uuid); }); } setTimeout(function () { App.busy.unBusy(); }, 400); } }); }, // end service.submit() subtotal: function () { var total = 0, options; for (var x in service.items) { total += parseFloat(App.cached['Dish'][service.items[x].id].price); options = service.items[x].options; for (var xx in options) { var option = App.cached['Option'][options[xx]]; if (option === undefined) continue; // option does not exist anymore total += parseFloat(option.optionPrice(options)); } } total = App.ceil(total); return total; }, /** * delivery cost * * @return float */ _breackDownDelivery: function () { var delivery = 0; if (service.restaurant.delivery_fee && App.order.delivery_type == 'delivery') { delivery = parseFloat(service.restaurant.delivery_fee); } delivery = App.ceil(delivery); return delivery; }, /** * Crunchbutton service * * @return float */ _breackDownFee: function (feeTotal) { var fee = 0; if (service.restaurant.fee_customer) { fee = (feeTotal * (parseFloat(service.restaurant.fee_customer) / 100)); } fee = App.ceil(fee); return fee; }, _breackDownTaxes: function (feeTotal) { var taxes = (feeTotal * (service.restaurant.tax / 100)); taxes = App.ceil(taxes); return taxes; }, _breakdownTip: function (total) { var tip = 0; if (App.order['pay_type'] == 'card') { if (App.order.tip === 'autotip') { return parseFloat($('[name=pay-autotip-value]').val()); } tip = (total * (App.order.tip / 100)); } tip = App.ceil(tip); return tip; }, total: function () { var total = 0, dish, options, feeTotal = 0, totalItems = 0, finalAmount = 0; var breakdown = this.totalbreakdown(); total = breakdown.subtotal; feeTotal = total; feeTotal += breakdown.delivery; feeTotal += breakdown.fee; finalAmount = feeTotal + breakdown.taxes; finalAmount += this._breakdownTip(total); return App.ceil(finalAmount).toFixed(2); }, charged: function () { var finalAmount = this.total(); if (App.order.pay_type == 'card' && App.credit.restaurant[service.restaurant.id]) { finalAmount = finalAmount - App.ceil(App.credit.restaurant[service.restaurant.id]).toFixed(2); if (finalAmount < 0) { finalAmount = 0; } } return App.ceil(finalAmount).toFixed(2); }, /** * Returns the elements that calculates the total * * breakdown elements are: subtotal, delivery, fee, taxes and tip * * @return array */ totalbreakdown: function () { var elements = {}; var total = this.subtotal(); var feeTotal = total; elements['subtotal'] = this.subtotal(); elements['delivery'] = this._breackDownDelivery(); feeTotal += elements['delivery']; elements['fee'] = this._breackDownFee(feeTotal); feeTotal += elements['fee']; elements['taxes'] = this._breackDownTaxes(feeTotal); elements['tip'] = this._breakdownTip(total); return elements; }, resetOrder: function () { service.items = {}; $('.cart-items-content, .cart-total').html(''); }, reloadOrder: function () { var cart = service.items; service.resetOrder(); service.loadFlatOrder(cart); }, loadFlatOrder: function (cart) { for (var x in cart) { service.add(cart[x].id, { options: cart[x].options ? cart[x].options : [] }); } }, loadOrder: function (order) { // @todo: convert this to preset object try { if (order) { var dishes = order['_dishes']; for (var x in dishes) { var options = []; for (var xx in dishes[x]['_options']) { options[options.length] = dishes[x]['_options'][xx].id_option; } if (App.cached.Dish[dishes[x].id_dish] != undefined) { service.add(dishes[x].id_dish, { options: options }); } } } } catch (e) { console.log(e.stack); // throw e; } service.updateTotal(); }, hasItems: function () { if (!$.isEmptyObject(service.items)) { return true; } return false; }, autotip: function () { var subtotal = service.totalbreakdown().subtotal; var autotipValue if (subtotal === 0) { autotipValue = 0; } else { // autotip formula - see github/#940 autotipValue = Math.ceil(4 * (subtotal * 0.107 + 0.85)) / 4; } $('[name="pay-autotip-value"]').val(autotipValue); var autotipText = autotipValue ? ' (' + (App.config.ab && App.config.ab.dollarSign == 'show' ? '$' : '') + autotipValue + ')' : ''; $('[name=pay-tip] [value=autotip]').html('Autotip' + autotipText); }, _parseCustomOptions: function( options, selectedOptions ){ var parsedOptions = []; for( var x in options ){ var newOption = {}; var rawOption = options[x]; if ( rawOption.id_option_parent ) { continue; } newOption.type = rawOption.type; newOption.id_option = rawOption.id_option; newOption.name = rawOption.name + ( rawOption.description || '' ); if( rawOption.type == 'check' ){ newOption.id_option = rawOption.id_option; newOption.price = rawOption.optionPrice(options); newOption.priceFormated = service.customizeItemPrice(newOption.price); newOption.checked = ( $.inArray(rawOption.id_option, selectedOptions) !== -1); } if( rawOption.type == 'select' ){ newOption.options = []; newOption.selected = false; for( var i in options ){ if (options[i].id_option_parent == rawOption.id_option) { var newSubOption = {}; newSubOption.id_option = options[i].id_option; newSubOption.id_option_parent = options[i].id_option_parent; newSubOption.price = options[i].price; newSubOption.priceFormated = service.customizeItemPrice(newSubOption.price); newSubOption.selected = ($.inArray( options[i].id_option, selectedOptions) !== -1); newSubOption.name = options[i].name + (options[i].description || '') + service.customizeItemPrice(newSubOption.price, (rawOption.price_linked == '1')); newOption.options.push( newSubOption ); if( newSubOption.selected ){ newOption.selected = options[i].id_option; } } } } parsedOptions.push( newOption ); } return parsedOptions; } }; return service; });