/*!
 * plum.Shop v1.3: A shopping cart for jQuery
 *
 * Copyright 2011 RoboCréatif, LLC
 * <http://robocreatif.com>
 *
 * Date: 24 September, 2011
 */

var plum = plum || {};

String.prototype.plum = Number.prototype.plum = jQuery.fn.plum = function(callback, options)
{
	var action = callback.split('.'), secondary;
	callback = action[0];
	if (action.length > 1) {
		secondary = options;
		options = action[1];
	}
	return typeof plum[callback] === 'function' ? plum[callback].call(this, options, secondary) : this;
};

(function ($) {

	// Microsoft is a world of hurt. plum.Shop needs to listen to DOM changes
	// to allow it to work with AJAX without needing to reinstantiate it, so
	// it binds the DOMNodeInserted event to the body. But, IE8 and earlier
	// don't support that event, so it must be triggered manually within jQuery's
	// .html() method.
	if ($.browser.msie && parseFloat($.browser.version) < 9) {
		(function() {
			var html = $.fn.html;
			$.fn.html = function () {
				var result = html.apply(this, arguments);
				this.trigger('DOMNodeInserted', [ result ]);
				return result;
			}
		}());
	}

	// plum.Shop plugin handler
	plum.shop = function (options) {

		var shop = plum.shop.prototype;
		$.extend(true, shop.options, options);
		options = shop.options

		// Get the cart storage method
		shop.storage = 'cart' + (options.session && options.sessionurl ? 'Session'
			: !!window.sessionStorage && options.localstorage ? 'Storage'
			: 'Cookie'
		);

		// Set up the cart and listen for cart events. Once the cart has
		// finished building, trigger the ready callback.
		shop.getCart();
		shop.createCart($('.' + options.classes.cart));
		shop.listen();
		options.ready.call(shop);

		// Continue jQuery chaining...
		return this;

	};

	// plum.Shop prototype methods
	plum.shop.prototype = {

		cart: false,
		quantity: 0,
		subtotal: 0,
		shipping: 0,
		tax: 0,
		discount: 0,
		total: 0,
		storage: null,

		options: {

			additem: function () { },
			cancelurl: null,
			cartitem: '',
			classes: {
				cart: 'cart',
				cartlist: 'cart-list',
				cartdiscount: 'cart-discount',
				cartquantity: 'cart-quantity',
				cartshipping: 'cart-shipping',
				cartsubtotal: 'cart-subtotal',
				carttax: 'cart-tax',
				carttotal: 'cart-total',
				custom: 'custom',
				description: 'description',
				discount: 'discount',
				empty: 'empty',
				id: 'id',
				price: 'price',
				product: 'product',
				purchase: 'purchase',
				quantity: 'quantity',
				shipping: 'shipping',
				remove: 'remove',
				thumb: 'thumb',
				title: 'title'
			},
			cookie: 'plum_shop',
			currency: 'USD',
			currencyafter: '',
			currencybefore: '$',
			currencydecimal: '.',
			currencythousands: ',',
			discount: null,
			discountcodes: { },
			emptycart: function () { },
			headerurl: null,
			limit: 0,
			listen: { },
			localstorage: 'plum_shop',
			nofityurl: null,
			ready: function () { },
			returnurl: null,
			session: 'plum_shop',
			sessionurl: null,
			shipping: function () { },
			shippingexempt: 0,
			shippingexemptover: true,
			shippingname: null,
			shippingrate: 0,
			shippingtype: 'variable', // variable, fixed, flat, function
			statusurl: null,
			taxcountry: '',
			taxexempt: 0,
			taxexemptover: false,
			taxrate: 0,
			updatetotals: function () { }

		},

		/**
		 * Adds a product to the cart
		 *
		 * @since  1.0
		 * @param  object  p     The product details
		 * @param  bool    save  True will save the cart, or retrieve it otherwise
		 */
		addItem: function (p, save) {

			// All products must have an ID in order to be added.
			if (!p.id) {
				return false;
			}

			// Helper variables
			var i = this.getItem(p.id),
			o = this.options,
			c = o.classes,
			listitem = $('li[data-' + c.id + '="' + p.id + '"]');

			// If a direct link to the thumbnail isn't provided, the property
			// is very likely in the form of an HTML <img> object. Thus, the
			// value of the thumbnail is the <img> src attribute.
			if (typeof p.thumb === 'object') {
				p.thumb = p.thumb.src;
			}

			// The price must be converted to a number to allow for calculations
			// within Plum. The .price() method allows for easy cleanup on
			// prices that may contain foreign characters.
			if (typeof p.price === 'string') {
				p.price = this.price(p.price);
			}

			// Clean up extraneous characters (tabs, lines, etc.)
			$.each(p, function (k, v) {
				if (typeof v === 'string') {
					p[k] = v.replace(/\s+/g, ' ').replace(/^\s?(.+)\s?$/, '$1');
				}
			});

			// If the additem callback doesn't return false and the quantity of
			// the item is less than the "limit" option, update the cart
			if (o.additem.call(this, p) === false || (o.limit && p.quantity > o.limit)) {
				return false;
			}

			// If the product already exists, check the quantity. If it's less
			// than 1, the product should be removed from the cart and the item
			// in the cart list also removed. If it's 1 or greater, the item
			// will be updated in the cart, using the new quantity.
			if (typeof i !== 'undefined') {
				if (p.quantity < 1) {
					this.cart.items.splice(i, 1);
					listitem.fadeOut(300, function () { listitem.remove(); });
				} else {
					$.extend(this.cart.items[i], p);
					listitem.html(this.createItem(this.cart.items[i]));
				}

			// When a new product is added to the cart, the "cartitem" HTML is
			// formatted to reflect the product and appended to the cart display.
			} else {
				this.cart.items.push(p);
				$('ul.' + c.cartlist).append(
					'<li data-' + c.id + '="' + p.id + '">' + this.createItem(p) + '</li>'
				);
			}

			// Update and (optionally, depending on passed arguments) save the cart.
			this.updateCart(save);

		},

		/**
		 * Retrieves or saves a cart using a cookie
		 *
		 * @since  1.0
		 * @param  bool  save  True will save the cart, or retrieve it otherwise
		 */
		cartCookie: function (save) {

			var o = this.options, cart, i, cookie;
			if (!save) {
				cookie = document.cookie.split(';');
				for (i in cookie) {
					cart = cookie[i];
					while (cart.charAt(0) === ' ') {
						cart = cart.substring(1);
					}
					if (cart.indexOf(o.cookie + '=') === 0) {
						this.cart = unescape(cart.substring((o.cookie + '=').length));
						this.cart = $.parseJSON(this.cart);
						break;
					}
				}
			} else {
				cart = o.cookie + '=' + (typeof this.cart !== 'object' ? ''
					: escape(this.json({
						items: this.cart.items,
						discount: this.cart.discount,
						shipping: this.cart.shipping
					})))
					+ '; path=/';
				if (cart.length < 4049) {
					document.cookie = cart;
				}
			}

		},

		/**
		 * Retrieves or saves a cart using server sessions
		 *
		 * @since  1.0
		 * @param  bool  save  True will save the cart, or retrieve it otherwise
		 */
		cartSession: function (save) {

			var o = this.options, cart = {}, shop = this;
			if (!save) {
				cart[o.session] = true;
				$.ajax(o.sessionurl, {
					async: false,
					type: 'GET',
					data: cart,
					dataType: 'json',
					success: function (e) { shop.cart = e; }
				});
			} else {
				cart[o.session] = {
					discount: this.cart.discount,
					items: this.cart.items,
					shipping: this.cart.shipping
				};
				$.post(o.sessionurl, cart);
			}

		},

		/**
		 * Retrieves or saves a cart using localStorage
		 *
		 * @since  1.1
		 * @param  bool  save  True will save the cart, or retrieve it otherwise
		 */
		cartStorage: function (save) {

			var o = this.options;
			if (!save) {
				this.cart = window.sessionStorage[o.localstorage]
					? $.parseJSON(unescape(window.sessionStorage[o.localstorage]))
					: false;
			} else {
				window.sessionStorage[o.localstorage] = typeof this.cart !== 'object' ? ''
					: escape(this.json({
						items: this.cart.items,
						discount: this.cart.discount,
						shipping: this.cart.shipping
					}));
			}

		},

		/**
		 * Creates a checkout form for third party checkout (Google, PayPal)
		 *
		 * @since   1.0
		 * @param   string  action  The URL to POST the cart contents
		 * @param   array   vars    An array containing input names & values
		 * @return  object  Returns the form as a jQuery object
		 */
		checkout: function (action, vars) {

			var i, x,
			form = '<form style="display:none" '
				+ 'action="' + action + '" '
				+ 'method="post"'
				+ 'accept-charset="utf-8">';
			for (x in vars) {
				i = vars[x];
				form += '<input type="hidden" name="' + i[0] + '" value="' + i[1] + '">';
			}
			return $(form += '<input type="submit"></form>').appendTo('body').submit();

		},

		/**
		 * Creates an HTML representation of the cart
		 *
		 * @since  1.0
		 * @param  object  cart  The shopping cart object
		 */
		createCart: function (cart) {

			var s = this,
			c = this.options.classes
			html = [];

			// If the cart is populated, the display should be built. This
			// involves looping through each cart product to create items
			// for the list, filling an unordered list with each product,
			// and triggering a change on the input fields within that list
			// to run any custom calculations.
			if (cart.length) {
				this.cart.each(function () {
					html.push(
						'<li data-' + c.id + '="' + this.id + '">'
						+ s.createItem(this)
						+ '</li>'
					);
				});
				cart.html('<ul class="' + c.cartlist + '">' + html.join('') + '</ul>');
				$(':input', cart).trigger('change');
			}

			// Update the cart after creating the HTML display.
			this.updateCart();

		},

		/**
		 * Formats a cart product according to the defined HTML
		 *
		 * @since   1.0
		 * @param   object  product  A single cart item object
		 * @return  string  Returns the cart item HTML
		 */
		createItem: function (product) {

			var x, i = 0,

			// Replace the special price short codes.
			html = this.options.cartitem
				.replace(/{pricesingle}/g, this.format(product.price))
				.replace(/{pricetotal}/g, this.format(this.priceSubtotal(product))),

			// Replace any other attributes if short codes exist for them.
			attrs = html.match(/(\{[^\}\s]+\})/g);
			for (i in attrs) {
				if (/\d+/.test(i) && typeof attrs[i] === 'string') {
					x = attrs[i].substring(1, attrs[i].length - 1);
					html = html.replace(new RegExp(attrs[i], 'g'), product[x] || '');
				}
			}

			return html;

		},

		/**
		 * Converts a number to currency format
		 *
		 * This allows for internationalization of currencies. By using the
		 * currency configuration options of plum.Shop, one can customize the
		 * display beyond the default "$1,000.00" format. For example, this
		 * can instead be displayed as "€1 000,00" by changing "currencybefore,"
		 * "currencythousands" and "currencydecimal."
		 *
		 * @since   1.0
		 * @param   number  price  The number to format as a currency string
		 * @return  string  A string formatted with the cart's currency options
		 */
		format: function (price) {

			var o = this.options,
			p = Math.abs(+price || 0).toFixed(2),
			i = parseInt(p, 10) + '',
			j = (j = i.length) > 3 ? j % 3 : 0;

			return (price < 0 ? '-' : '')
				+ o.currencybefore
				+ (j ? i.substr(0, j) + o.currencythousands : '')
				+ i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + o.currencythousands)
				+ o.currencydecimal + Math.abs(p - i).toFixed(2).slice(2)
				+ o.currencyafter;

		},

		/**
		 * Loops through the products in the cart
		 *
		 * Usage: this.cart.each(function () { });
		 *
		 * @since   1.0
		 * @param   function  callback  A callback function to run on each item
		 * @return  mixed     If the callback function returns anything other
		 *                    than true, the loop will be broken
		 */
		each: function (callback) {

			var c, i = 0, l = this.items.length;
			for (; i < l; i) {
				c = callback.call(this.items[i], i++);
				if (typeof c !== 'undefined' && c !== true) {
					return c;
				}
			}

		},

		/**
		 * Removes all items from the cart
		 *
		 * The cart display is emptied, the cart object reset, and the empty
		 * cart saved.
		 *
		 * @since  1.0
		 */
		emptyCart: function () {

			var c = this.options.classes,
			list = $('ul.' + c.cartlist + ' li').fadeOut(300, function () {
				list.remove();
			});
			$('.' + c.discount).val('');
			this.cart = { items: [], discount: false };
			this.cart.each = this.each;
			this.updateCart(true);

		},

		/**
		 * Confirms emptying the cart
		 *
		 * When clicking an element classed with "empty," this function is
		 * called. If the "emptycart" callback function returns anything
		 * other than false, the cart will be emptied. If the callback returns
		 * false, the cart will not be emptied.
		 *
		 * @since  1.1
		 */
		emptyConfirm: function (event) {

			var list, c = this.options.classes;

			if (event) {
				event.preventDefault();
			}
			if (!this.quantity) {
				return;
			}
			if (this.options.emptycart.call(this) !== false) {
				this.emptyCart();
			}

		},

		/**
		 * Gets the cart from the defined location and populates this.cart
		 * with its contents.
		 *
		 * @since  1.0
		 */
		getCart: function () {

			this[this.storage]();

			this.cart = this.cart || {};
			this.cart.items = this.cart.items || [];
			if (this.cart.discount === 'false') {
				this.cart.discount = false;
			}
			if (this.cart.shipping === 'false') {
				this.cart.shipping = false;
			}
			this.cart.discount = this.cart.discount || false;
			this.cart.shipping = this.cart.shipping || false;
			this.cart.each = this.each;
			this.cart.each(function () {
				this.price = parseFloat(this.price);
				this.quantity = parseInt(this.quantity, 10);
			});

		},

		/**
		 * Looks for and returns a product ID from the cart
		 *
		 * @since   1.0
		 * @param   string  id  A product ID to look for
		 * @return  mixed   If the product is found, its ID is returned.
		 *                  Otherwise, this.getItem() is undefined.
		 */
		getItem: function (id) {

			return this.cart.each(function (i) {
				return this.id === id ? i : true;
			});

		},

		/**
		 * Converts an object to a JSON string
		 *
		 * This is a quick method to allow saving the cart as a JSON
		 * string for localStorage and cookie storage.
		 *
		 * @since   1.0
		 * @param   object  object  An object to encode
		 * @return  string  The JSON-encoded string
		 */
		json: function (object) {

			var json = [], i;
			switch (typeof object) {
				case 'function':
					return 'function';
				case 'number':
				case 'boolean':
					return object;
				case 'string':
					return '"' + object.replace(/(\"|\/|\{|\}|\n|\r|\t)/g, '\\$1') + '"';
				default:
					if (typeof object.length === 'number') {
						for (i in object) {
							if (object.hasOwnProperty(i)) {
								json.push(this.json(object[i]));
							}
						}
						return '[' + json.join(',') + ']';
					}
					for (i in object) {
						if (object.hasOwnProperty(i)) {
							json.push('"' + i + '":' + this.json(object[i]));
						}
					}
					return '{' + json.join(',') + '}';
			}

		},

		/**
		 * Listens for activity on the defined classes
		 *
		 * @since  1.0
		 */
		listen: function () {

			var shop = this,
			o = shop.options,
			c = o.classes,
			time = 0,
			i;

			// Clicking a button to add an item to the cart
			$('.' + c.purchase).live('click', function (e) {

				e.preventDefault();

				// Helper variables
				var x,
				i = 0,
				p = $(this).closest('.' + c.product),
				quan,
				prod = {};

				// Loop through the data attributes of a product container and
				// use any matched attributes as product properties.
				$.each(p.data(), function (k, v) {
					if (k.substring(0, c.custom.length) === c.custom) {
						k = k.substring(6).toLowerCase();
					}
					if (!prod[k]) {
						prod[k] = v;
					}
				});

				// Elements within the product container that have appropriate
				// classes will be used as product properties
				$('[class]', p).each(function () {
					var e = $(this);
					x = new RegExp(
						c.price + '|'
						+ c.description + '|'
						+ c.title + '|'
						+ c.thumb + '|'
						+ c.quantity + '|'
						+ c.custom + '-([^\\s]+)'
					);
					x = e.attr('class').match(x);
					if (x) {
						x = x[1] || x[0];
						if (!prod[x]) {
							prod[x] = e.is(':input') ? e.val()
								: e.is('img') ? e[0].src
								: e.text();
							if (e.is('select')) {
								e = $('option:selected:eq(0)', e);
							}
							prod.id = prod.id || e.attr('id');
						}
					}
				});

				// Make sure the product has an ID and quantity
				prod.id = prod.id || p.attr('id') || null;
				prod.quantity = parseInt(prod.quantity || 1, 10);

				// If the product already exists in the cart, add the quantity to
				// the existing amount.
				quan = shop.getItem(prod.id);
				quan = typeof quan === 'undefined' ? 0 : shop.cart.items[quan].quantity;

				// Add to or update in the cart, which should also be save.
				shop.addItem($.extend(prod, {
					quantity: quan + prod.quantity,
					title: prod.title || p[0].title
				}), true);

			});

			// Clicking the remove item button in each cart item
			$('.' + c.cart + ' .' + c.remove).live('click', function (e) {
				e.preventDefault();
				var product = shop.getItem($(this).closest('li').data(c.id));
				product = shop.cart.items[product];
				product.quantity = 0;
				shop.updateQuantity.call(this, shop, product);
			});

			// Clicking a button to remove all items
			$('.' + c.empty).live('click', function (e) {
				shop.emptyConfirm.call(shop, e);
			});

			// Changes occur on input fields in each cart item (e.g., quantity
			// updates or custom event listeners)
			$('.' + c.cart + ' :input').live('change', function () {
				if (time) {
					return false;
				}
				var elem = $(this),
				product = shop.cart.items[shop.getItem(elem.closest('li').data(c.id))],
				i;
				time = window.setTimeout(function () {
					time = 0;
					if (elem.hasClass(c.quantity)) {
						return shop.updateQuantity.call(elem[0], shop, product);
					}
					for (i in o.listen) {
						if (typeof o.listen[i] === 'function') {
							if (elem.hasClass(i)) {
								o.listen[i].call(elem[0], shop, product);
							}
						}
					}
				}, 0);
			});

			// Changes occur on the discount field
			$('.' + c.discount).live('change', function () {
				if (time) {
					return false;
				}
				var elem = this;
				time = window.setTimeout(function () {
					time = 0;
					shop.cart.discount = elem.value in o.discountcodes ? elem.value : false;
					shop.updateCart(true);
				}, 0);
			});

			// Changes occur on the shipping list
			$('select.' + c.shipping).live('change', function () {
				if (time) {
					return false;
				}
				var elem = this;
				time = window.setTimeout(function () {
					time = 0;
					shop.cart.shipping = elem.value;
					shop.updateCart(true);
				}, 0);
			});

			// Clicking buttons to trigger a checkout method
			for (i in this.checkout) {
				if (this.checkout.hasOwnProperty(i)) {
					$('.'  + (this.options.classes[i] || i))
						.live('click', { checkout: i }, function (e) {
							e.preventDefault();
							shop.checkout[e.data.checkout].call(shop);
						});
				}
			}

			// Fun stuff. Listen for the cart being inserted so items can be displayed.
			// Awesome for AJAX-powered sites. If the browser is IE 8 or earlier, a
			// nifty little hack needs to be implemented. Shame on Microsoft, as usual.
			// We're looking for HTML that has been targeted on the .cart element.
			$('body').bind('DOMNodeInserted', function (e, html) {
				var target = !$.browser.msie || parseInt($.browser.version, 10) > 8
					? $(e.target)
					: $('.' + c.cart, html[0]);
				if (target.is('.' + c.cart)) {
					return setTimeout(function () {
						shop.createCart(target);
					}, 20);
				}
			});

		},

		/**
		 * Creates a float number from a currency string
		 *
		 * All characters other than digits and decimal points are removed
		 * from the provided "price" string.
		 *
		 * @since   1.0
		 * @param   string  price  The string to be converted
		 * @return  number  A number formatted from the given string
		 */
		price: function (price) {

			return parseFloat(price.replace(
				new RegExp('\\' + this.options.currencydecimal, 'g'), '.'
			).replace(/[^\d\.]+/, ''));

		},

		/**
		 * Calculates the total shipping cost
		 *
		 * @since  1.0
		 */
		priceShipping: function () {

			var s = this,
			o = s.options,
			c = o.classes,
			x = 0,
			i,
			shipping = '',
			options = $('select.' + c.shipping);

			// Initialize shipping rate
			this.shipping = 0;

			// Create shipping options if a single rate is defined.
			if (typeof o.shippingrate !== 'object') {
				o.shippingrate = { 'Shipping': o.shippingrate };
			}

			// Loop through the shipping options to create a list of rates.
			for (i in o.shippingrate) {
				shipping += '<option value="' + i + '">' + i + ' (';
				switch (o.shippingtype) {
					case 'variable': // Variable rate (using subtotal)
						shipping += this.format(this.subtotal * o.shippingrate[i]);
						break;
					case 'fixed': // Fixed rate (using quantity)
						shipping += this.format(this.quantity * o.shippingrate[i]);
						break;
					case 'flat': // Flat rate
						shipping += this.format(o.shippingrate[i]);
						break;
					case 'custom': // Using the shipping callback function
						x = 0;
						found = 0;
						this.cart.each(function () {
							pr = o.shipping.call(this, o.shippingrate[i]);
							if(pr == 12.50) found += 1; // Only one shipping per yogurt starter of the same kind
							if(pr != 12.50 || found < 2) {
								x += pr;
							}
						});
						shipping += this.format(x);
						break;
					default: // Using the shipping callback function
						x = 0;
						this.cart.each(function () {
							x += o.shipping.call(this, o.shippingrate[i]);
						});
						shipping += this.format(x);
						break;
				}
				shipping += ')</option>';
			}				
			options.html(shipping);
			shipping = $(shipping);

			// If no shipping option has been saved with the cart, the first
			// option in the list should be selected. Otherwise, use the value
			// that has been saved.
			this.cart.shipping = (s.cart.shipping ? shipping.filter(function () {
				return this.value === s.cart.shipping;
			}) : shipping.eq(0));

			// If shipping is exempt, the cost of shipping and each shipping option
			// are set to 0.
			if (
				o.shippingexempt && (
					(o.shippingexemptover && o.shippingexempt < this.subtotal) ||
					(!o.shippingexemptover && o.shippingexempt > this.subtotal)
				)
			) {
				$('option', options).each(function () {
					$(this).text(this.value + ' (' + s.format(0) + ')');
				});

			// Set the shipping cost for non-exempt rates.
			} else {
				if (!this.cart.shipping.text()) {
					this.cart.shipping = shipping.eq(0);
				}
				this.shipping = this.price(this.cart.shipping.text().match(/ \((.+)\)$/)[1]);
				options
					.find('[value="' + this.cart.shipping[0].value + '"]')
					.attr('selected', true);

			}

			this.cart.shipping = this.cart.shipping[0].value;
			return this.shipping = parseFloat(this.shipping.toFixed(2));

		},

		/**
		 * Calculates the subtotal of a single product
		 *
		 * @since  1.0
		 * @param  object  product  The product properties object
		 */
		priceSubtotal: function (product) {

			return product.price * product.quantity;

		},

		/**
		 * Calculates the total tax
		 *
		 * @since  1.0
		 */
		priceTax: function () {

			var o = this.options, s = this;

			if (!o.taxcountry || (o.taxexempt && (
				(o.taxexemptover && o.taxexempt < this.subtotal) ||
				(!o.taxexemptover && o.taxexempt > this.subtotal)
			))) {
				this.tax = 0;
			} else if (typeof o.tax === 'function') {
				this.cart.each(function () {
					s.tax += o.tax.call(s, this);
				});
			} else {
				this.tax = (this.subtotal - this.discount) * o.taxrate;
			}

			return this.tax = parseFloat(this.tax.toFixed(2));

		},

		/**
		 * Updates the quantity of a single item
		 *
		 * @since   1.1
		 * @param   object  shop     The plum.Shop object
		 * @param   object  product  A single product object
		 * @return  mixed
		 */
		updateQuantity: function (shop, product) {

			var quantity = (this.value ? parseInt(this.value, 10) : 0) - product.quantity;
			product.quantity += isNaN(quantity) ? -product.quantity : quantity;

			// If the quantity hasn't changed or is too high, the cart should not
			// be updated. Otherwise, save the changes
			return (typeof this.value !== 'undefined' && this.value === this.defaultValue)
				|| (shop.options.limit && product.quantity > shop.options.limit)
				? this
				: shop.addItem(product, true);

		},

		/**
		 * Updates and (optionally) saves the cart
		 *
		 * @since  1.0
		 * @param  bool  save  If true, the cart will be saved to its defined location
		 */
		updateCart: function (save) {

			var cart = {},
			s = this,
			o = this.options,
			d = 0;

			// Get the cart quantity and subtotal
			this.quantity = 0;
			this.subtotal = 0;
			this.cart.each(function () {
				s.quantity += parseInt(this.quantity, 10);
				s.subtotal += s.priceSubtotal(this);
			});

			// Calculate the shipping cost
			this.shipping = 0;
			this.priceShipping();

			// Apply discounts
			d = !this.quantity || !this.cart.discount ? 0
				: /%/.test(o.discountcodes[this.cart.discount])
					? this.subtotal * parseFloat(o.discountcodes[this.cart.discount]) / 100
				: o.discountcodes[this.cart.discount];
			d = d || 0;
			d += typeof o.discount === 'function' ? o.discount.call(this) : 0;
			this.discount = this.quantity ? d : 0;

			// Update the price and quantity totals
			this.updateTotals();

			if (save) {
				this[this.storage](true);
			}

		},

		/**
		 * Updates the cart totals
		 *
		 * In addition to updating predefined areas of a shopping cart, this
		 * is also a trigger point for the "updatetotals" callback function.
		 *
		 * @since  1.2
		 */
		updateTotals: function () {

			var c = this.options.classes;

			this.tax = 0;
			this.priceTax();
			this.total = this.subtotal - this.discount + this.tax + this.shipping;
			this.total = parseFloat(this.total.toFixed(2));

			this.options.updatetotals.call(this);
			$('.' + c.cartquantity).html(this.quantity);
			$('.' + c.cartsubtotal).html(this.format(this.subtotal));
			$('.' + c.cartdiscount).html(this.format(-this.discount));
			$('.' + c.carttax).html(this.format(this.tax));
			$('.' + c.cartshipping).html(this.format(this.shipping));
			$('.' + c.carttotal).html(this.format(this.total));
			$('.' + c.discount).val(this.cart.discount || '');

		}

	};

}(jQuery));

/*!
 * Amazon FPS extension for plum.Shop
 *
 * @package  plum.Shop
 * @since    1.3
 * @param    string  id  An optional invoice ID
 */
(function ($) {

	var merchant = null;
	plum.shop.prototype.checkout.amazon = function (id) {

		var i = 0,
		o = this.options,
		vars = [],
		action = 'https://payments.amazon.com/checkout/';

		merchant = o.amazonmerchant || merchant;

		// The Amazon merchant ID must be set.
		if (!/[A-Z0-9]{14}/.test(merchant)) {
			return false;
		}

		// Add the items to the shopping cart.
		this.cart.each(function (i) {
			var v, x = 1, d = [];
			i++;
			vars.push([ 'item_merchant_id_' + i, merchant ]);
			vars.push([ 'item_sku_' + i, this.id ]);
			vars.push([ 'item_title_' + i, this.title ]);
			vars.push([ 'item_price_' + i, this.price ]);
			vars.push([ 'item_quantity_' + i, this.quantity ]);
			vars.push([ 'item_image_url_' + i, this.thumb ]);
			for (v in this) {
				if (!/^(?:description|id|price|quantity|title|thumb)$/.test(v)) {
					vars.push([ 'item_' + i + '.custom_attribute_' + x + '.' + v, this[v] ]);
					d.push(v + ': ' + this[v]);
				}
			}
			if (d.length) {
				vars.push([ 'item_description_' + i, d ]);
			}
		});

		// Add a discount.
		vars.push([ 'cart_promotion_1', this.discount ]);
		vars.push([ 'cart_promotion_type_1', 'fixed_amount_off' ]);

		// Add shipping.
		vars.push([ 'shipping_method_service_level_1', 'standard' ]);
		vars.push([ 'shipping_method_price_per_shipment_amount_1', this.shipping ]);

		// Add tax.
		vars.push([ 'tax_rate', o.taxrate ]);

		// Additional variables.
		vars.push([ 'currency_code', o.currency ]);

		// Send the cart to Amazon for payment.
		this.checkout(action + merchant, vars);

	};

}(jQuery));

/*!
 * Custom checkout extension for plum.Shop
 *
 * @package  plum.Shop
 * @since    1.0
 * @param    string  id  An optional invoice ID
 */
(function ($) {

	var checkout = function () { };
	plum.shop.prototype.checkout.checkout = function (id) {

		checkout = this.options.checkout || checkout;

		if (!this.quantity) {
			return;
		}
		if (checkout.call(this) === true) {
			this.emptyCart();
		}

	};

}(jQuery));

/*!
 * Google Checkout extension for plum.Shop
 *
 * @package  plum.Shop
 * @since    1.0
 * @param    string  id  An optional invoice ID
 */
(function ($) {

	var merchant = null;
	plum.shop.prototype.checkout.google = function (id) {

		var i = this.cart.items.length + 1, 
		o = this.options,
		vars = [],
		action = 'https://checkout.google.com/api/checkout/v2/checkoutForm/Merchant/',
		t;

		merchant = o.googlemerchant || merchant;

		// The Google Checkout merchant ID must be set.
		if (!/^[\d]{10,15}$/.test(merchant)) {
			return false;
		}

		// Add the items to the shopping cart.
		this.cart.each(function (i) {
			var x = [], v;
			i++;
			vars.push([ 'item_merchant_id_' + i, this.id ]);
			vars.push([ 'item_name_' + i, this.title ]);
			vars.push([ 'item_quantity_' + i, this.quantity ]);
			vars.push([ 'item_price_' + i, this.price ]);
			vars.push([ 'item_currency_' + i, o.currency ]);
			for (v in this) {
				if (!/^(?:description|id|price|quantity|title|thumb)$/.test(v)) {
					x.push(v + ': ' + this[v]);
				}
			}
			vars.push([ 'item_description_' + i, x.length ? x.join(', ') : '' ]);
		});

		// Add a discount.
		if (this.discount) {
			vars.push([ 'item_merchant_id_' + i, this.cart.discount || 0 ]);
			vars.push([ 'item_name_' + i, this.cart.discount || this.format(-this.discount) ]);
			vars.push([ 'item_description_' + i, 'Discount' ]);
			vars.push([ 'item_quantity_' + i, 1 ]);
			vars.push([ 'item_price_' + i, -this.discount ]);
			vars.push([ 'item_currency_' + i, o.currency ]);
		}

		// Add tax.
		if (this.tax) {
			t = o.taxcountry.split(/\s*,\s*/);
			vars.push([ 'checkout-flow-support.merchant-checkout-flow-support.tax-tables.default-tax-table.tax-rules.default-tax-rule-1.rate', o.taxrate ]);
			for (i in t) {
				vars.push([ 'checkout-flow-support.merchant-checkout-flow-support.tax-tables.default-tax-table.tax-rules.default-tax-rule-1.tax-area.postal-area.country-code', t[i] ]);
			}
		}

		// Add shipping.
		vars.push([ 'checkout-flow-support.merchant-checkout-flow-support.shipping-methods.flat-rate-shipping-1.name', this.cart.shipping ]);
		vars.push([ 'checkout-flow-support.merchant-checkout-flow-support.shipping-methods.flat-rate-shipping-1.price', this.shipping ]);
		vars.push([ 'checkout-flow-support.merchant-checkout-flow-support.shipping-methods.flat-rate-shipping-1.price.currency', o.currency ]);

		// Add an invoice ID.
		if (id) {
			vars.push([ 'shopping-cart.merchant-private-data', 'InvoiceID:#' + id ]);
		}

		// Send the cart to Google for payment.
		this.checkout(action + merchant, vars);

	};

}(jQuery));

/*!
 * Moneybookers extension for plum.Shop
 *
 * @package  plum.Shop
 * @since    1.1
 * @param    string  id  An optional invoice ID
 */
(function ($) {

	var merchant = { user: null, domain: null };
	plum.shop.prototype.checkout.moneybookers = function (id) {

		var i = 0,
		o = this.options,
		vars = [],
		action = 'https://www.moneybookers.com/app/payment.pl';

		merchant = {
			user: o.moneybookersuser || merchant.user,
			domain: o.moneybookersdomain || merchant.domain
		};

		// A very cursory check to verify that the payment email is a valid
		// email address.
		if (!merchant.user || !/^.+\.[a-z]{2,4}$/.test(merchant.domain)) {
			return false;
		}

		// Add the items to the shopping cart.
		this.cart.each(function () {
			var v, x = [];
			i++;
			vars.push([ 'detail' + i + '_description', this.title ]);
			for (v in this) {
				if (!/^(?:description|price|title|thumb)$/.test(v)) {
					x.push(v + ': ' + this[v]);
				}
			}
			vars.push([ 'detail' + i + '_text', x.join(', ') ]);
		});

		// Add a discount (Moneybookers hack).
		i = 2;
		if (this.discount) {
			vars.push([ 'amount' + i + '_description', this.cart.discount || 'Discount' ]);
			vars.push([ 'amount' + i++, this.format(-this.discount) ]);
		}

		// Add shipping (Moneybookers hack).
		vars.push([ 'amount' + i + '_description', this.cart.shipping ]);
		vars.push([ 'amount' + i++, this.format(this.shipping) ]);

		// Add tax (Moneybookers hack).
		vars.push([ 'amount' + i + '_description', 'Tax' ]);
		vars.push([ 'amount' + i++, this.format(this.tax) ]);
		vars.push([ 'amount', this.total.toFixed(2) ]);

		// Additional variables.
		vars.push([ 'currency', o.currency ]);
		vars.push([ 'pay_to_email', merchant.user + '@' + merchant.domain ]);
		if (o.headerurl) {
			vars.push([ 'logo_url', o.headerurl ]);
		}
		if (o.cancelurl) {
			vars.push([ 'cancel_url', o.cancelurl ]);
		}
		if (o.returnurl) {
			vars.push([ 'return_url', o.returnurl ]);
		}
		if (o.notifyurl) {
			vars.push([ 'status_url', o.notifyurl ]);
		}

		// Add an invoice ID.
		if (id) {
			vars.push([ 'transaction_id', id ]);
		}

		// Send the cart to Moneybookers for payment.
		this.checkout(action, vars);

	};

}(jQuery));

/*!
 * PayPal extension for plum.Shop
 *
 * @package  plum.Shop
 * @since    1.0
 * @param    string  id  An optional invoice ID
 */
(function ($) {

	var merchant = { user: null, domain: null };
	plum.shop.prototype.checkout.paypal = function (id) {

		var i = 0,
		o = this.options,
		vars = [],
		action = 'https://www.paypal.com/cgi-bin/webscr';

		merchant = {
			user: o.paypaluser || merchant.user,
			domain: o.paypaldomain || merchant.domain
		};

		// A very cursory check to verify that the payment email is a valid
		// email address.
		if (!merchant.user || !/^.+\.[a-z]{2,4}$/.test(merchant.domain)) {
			return false;
		}

		// Add the items to the cart.
		this.cart.each(function () {
			var v, x = 0;
			i++;
			vars.push([ 'item_number_' + i, this.id ]);
			vars.push([ 'item_name_' + i, this.title || this.id ]);
			vars.push([ 'quantity_' + i, this.quantity ]);
			vars.push([ 'amount_' + i, this.price ]);
			for (v in this) {
				if (!/^(?:description|id|price|quantity|title|thumb)$/.test(v) && x < 7) {
					vars.push([ 'on' + x + '_' + i, v ]);
					vars.push([ 'os' + x++ + '_' + i, this[v] ]);
				}
			}
		});

		// Add tax.
		vars.push([ 'tax_cart', this.tax ]);

		// Add a discount.
		vars.push([ 'discount_amount_cart', this.discount ]);

		// Add shipping.
		vars.push([ 'custom', 'Shipping type: ' + this.cart.shipping ]);
		vars.push([ 'handling_cart', this.shipping ]);

		// Additional variables.
		vars.push([ 'cmd', '_cart' ]);
		vars.push([ 'upload', '1' ]);
		vars.push([ 'charset', 'utf-8' ]);
		vars.push([ 'currency_code', o.currency ]);
		vars.push([ 'business', o.paypaluser + '@' + o.paypaldomain ]);
		if (o.headerurl) {
			vars.push([ 'cpp_header_image', o.headerurl ]);
		}
		if (o.cancelurl) {
			vars.push([ 'cancel_return', o.cancelurl ]);
		}
		if (o.returnurl) {
			vars.push([ 'return', o.returnurl ]);
		}
		if (o.notifyurl) {
			vars.push([ 'notify_url', o.notifyurl ]);
		}

		// Add an invoice ID.
		if (id) {
			vars.push([ 'invoice', id ]);
		}

		// Send the cart to PayPal for payment.
		this.checkout(action, vars);

	};

}(jQuery));
