====== Custom Shipping Code ====== The Custom Shipping Code option supports adding javascript code to your store's shipping section that is able to add new rates, or modify or remove existing rates that have been returned from other configured shipping rates for your store. The Custom Shipping Code is processed after any other carriers that have been configured have run, including the Custom Shipping Endpoint. This means that those rates can also be modified as part of your custom code. **This custom code runs serverside,** not client-side. Yes, it's javascript, but this javascript doesn't output to the browser. Instead, it's run serverside in Node.js (in a separate environment per store), so you can put private logic (such as coupon codes) in the code. ===== Example Payload ===== The custom shipping code receives the same payload as the custom shipping endpoint. See a [[./payload|sample payload]]. ===== Category Settings ===== You'll need to set each [[https://admin.foxycart.com/admin.php?ThisAction=ManageProductCategories|Category]] where you want the Custom Shipping Code to apply to //Shipped using live shipping rates//. You can do this after applying the custom shipping code to your shipping settings if you're not using live shipping rates already. ===== Enabling Custom Shipping Code ===== ==== Enable custom code ==== To enable Custom Shipping Code for your store, login to your store's Foxy administration and head to the "shipping" section. You'll find the option to "use custom code" as the last option on the page. Enabling the "use custom code" option will reveal a code editor where you can enter your custom JavaScript shipping code. The Custom Shipping Code functionality behaves just like a normal carrier like USPS, FedEx or UPS, so requires that any categories that you'd like to have the shipping calculated with your custom code to have a delivery type of "Shipped using live shipping rates". ==== Write your custom shipping code ==== We have a [[v:2.0:shipping:custom_code:api|Shipping API]] that helps you interact with the rates, adding custom rates, or filtering, modifying or removing existing rates (either from our native live rate carriers, or custom rates you add). You can review the example code further down this page for examples of using the Shipping API to modify rates. Within the shipping code editor, you will also have access to a couple of objects: * ''rates'': The rates object for the cart, which will be prepopulated with any rates returned from other options enabled for the store. This object also provides access to the [[v:2.0:shipping:custom_code:api|Shipping API]]. * ''cart'': An object containing all of the information about the customers cart. For an overview of what this object looks like, [[v:2.0:shipping:payload|see this page]]. The code is processed within a Node.js v14 instance, UTC timezone, with access to the following Node modules: * [[https://github.com/node-fetch/node-fetch|node-fetch]] (available as ''fetch'') * [[https://sdk.foxy.dev/|Foxy SDK]] (available as ''FoxySDK'') * [[https://github.com/goshippo/shippo-node-client|Shippo]] (available as ''shippo'') * [[https://www.npmjs.com/package/xml2js|xml2js]] (available as ''xml2js'') Within the code editor, you can enter any javascript logic you require for calculating your custom shipping rates. As you enter your shipping code, the code editor will provide in-context feedback for any issues it may detect. Hover over any icons or highlighted code for additional details (note that there is a known issue with the code validations relating to lines of code containing ''await'' - you can ignore errors for this) We recommend enabling the "enable shipping rate signing" option on the shipping page settings as well. If you have any javascript snippets in your store's configuration outside of the custom code entered on the shipping page that affects the returned shipping rates, or the weight used to calculate the shipping rate, you will want to leave this unchecked however. ==== Deploy your shipping code ==== Once you have your shipping code entered as desired, click the green "Update" button at the bottom of the page. You will see a message that the shipping code is being deployed, and to refresh the page to get an updated status. Wait for 15-20 seconds and refresh the page - if you see your shipping code displayed again, then your code was successfully deployed! ===== Migrating from existing snippets ===== If you are migrating from one of our existing snippets, you can [[v:2.0:shipping:custom_code:migrating|review notes on switching from the snippets to using custom shipping code here]]. ===== Auto-Select First Rate ===== If you'd like your cart to automatically select the first rate shown, you can use the snippet outlined [[v:2.0:shipping:autoselect_first_shipping_rate|here]]. ===== Example Shipping Code ===== The following are a selection of examples of shipping code to give you an idea of how it can be utilised for your store. For details on the functions available within the custom code, you can [[v:2.0:shipping:custom_code:api|review the Shipping API here]]. Note that HTML is not currently supported in the rate //carrier// or //service// parameters. We do have plans to support it in the future. ==== Adding a fallback shipping rate ==== This example simply checks to see if there were any rates returned from third-party carriers like USPS, UPS and FedEx, and if not, returns a fallback $15 rate to ensure your customers will always see a rate. if (!rates.exists()) { rates.add(10001, 15, '', 'Standard Shipping'); } ==== Flat rates with conditional free shipping ==== Provide a standard and express flat rate shipping option, setting the standard option to be free if at least $40 of products are present in the cart. rates.add(10001, 5, 'FoxyPost', 'Standard'); rates.add(10002, 15, 'FoxyPost', 'Express'); if (cart['_embedded']['fx:shipment']['total_item_price'] >= 40) { rates.filter(10001).price(0).service('Free Shipping'); } ==== Flat rates based on order total ==== Different flat rates based on the product subtotal, with a "pick up" option if a threshold isn't met. const total_item_price = cart['_embedded']['fx:shipment']['total_item_price']; rates.add(10001, 75, 'FoxyPost', 'Standard'); if (total_item_price <= 200) { // Orders $200 and under can only be picked up. rates.filter(10001).price(0).service('Customer Pickup'); } else if (total_item_price <= 500) { // Orders from $200.01 to $500 ship for $75 rates.filter(10001).price(75); } else if (total_item_price <= 1000) { rates.filter(10001).price(150); } else { // Orders over $1000, from the last if statement rates.filter(10001).price(300); } ==== Remove the "1-Day" and "2-Day" text from USPS rates ==== In some instances, USPS adds an estimated timeframe to their returned rates. While they may be valid for the estimated delivery time when shipped - it can create unrealistic expectations for customers if it may take a couple days to ship. This example removes those timeframe strings from any returned rates. rates.removeDayTimeframes(); ==== Adding extra text to shipping rate labels === If you want to modify a returned shipping rate label to have a different service, you can do that using the API like this: rates.filter('FedEx Home Delivery').service('Ground'); That would change the rate ''FedEx Home Delivery'' to instead be ''FedEx Ground''. You can also append to the existing service text though, so it will still use whatever the shipping provider returns. For example, the following would use FedEx Home Delivery again, but would append " (Signature Required)" to the end of it: let home_delivery = rates.filter('FedEx Home Delivery'); if (home_delivery.exists()) { home_delivery.service(home_delivery.service() + ' (Signature Required)'); } If you wanted to make a similar adjustment to a lot of different rates, for example adding " (Signature Required)" to all FedEx rates, you can do that like this: rates.filter('fedex').each(function() { this.service(this.service() + ' (Signature Required)'); }); ==== Providing flat rates based on the shipping country ==== The following example provides three tiers of shipping rates, one if the customer is within the UK (considered domestic in this example), another if the customer is within Europe, and another rate for anywhere else in the world. const tier1 = ['GB']; const tier2 = ['AL', 'AD', 'AM', 'AT', 'BY', 'BE', 'BA', 'BG', 'CH', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FO', 'FI', 'FR', 'GE', 'GI', 'GR', 'HU', 'HR', 'IE', 'IS', 'IT', 'LT', 'LU', 'LV', 'MC', 'MK', 'MT', 'NO', 'NL', 'PL', 'PT', 'RO', 'RU', 'SE', 'SI', 'SK', 'SM', 'TR', 'UA', 'VA']; const country = cart['_embedded']['fx:shipment']['country']; if (tier1.indexOf(country) > -1) { // United Kingdom rates.add(10001, 10, 'FoxyPost', 'Standard'); } else if (tier2.indexOf(country) > -1) { // Europe rates.add(10002, 20, 'FoxyPost', 'International'); } else { // Rest of world rates.add(10003, 30, 'FoxyPost', 'International'); } ==== Returning a flat rate for international orders ==== This example is a simple approach for only showing a flat rate option for international orders. const country = cart['_embedded']['fx:shipment']['country']; if (country != "US") { // The customer is shipping outside the US rates.hide(); rates.add(10001, 25, 'FoxyPost', 'Standard'); } ==== Restricting shipping to only a specific set of postcodes ==== This example can be used to prevent customers from being able to complete the checkout if they don't have a shipping address within a specific set of postcodes. The ''allowed_postcodes'' array can be updated to include as many postcodes as needed. If a customer has a shipping address that doesn't have a matching postcode, they will instead see an error. const allowed_postcodes = [55115, 55116]; const postal_code = Number(cart['_embedded']['fx:shipment']['postal_code']); if (allowed_postcodes.includes(postal_code)) { rates.add(10001, 10, '', 'Standard Shipping'); } else { rates.error('Sorry, we are not currently able to ship to your location'); } ===Example using non-numerical postcodes=== Since some countries may use postcodes that include alphabetic characters, the example needs to be a little different in that case. Here is an example for postcodes in UK. const allowed_postcodes = ['SE100AA', 'SE100AB']; const postal_code = (cart['_embedded']['fx:shipment']['postal_code']).replace(/\s+/g, '').toUpperCase(); if (allowed_postcodes.includes(postal_code)) { rates.add(10001, 10, '', 'Standard Shipping'); } else { rates.error('Sorry, we are not currently able to ship to your location'); } ==== Hiding specific shipping rates depending on the shipping state ==== This option hides rates any express options if the shipping state is outside of the contiguous US const contiguous = ['AL', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY', 'DC']; const country = cart['_embedded']['fx:shipment']['country']; const region = cart['_embedded']['fx:shipment']['region']; if (country == "US" && contiguous.indexOf(region) == -1) { rates.filter('express').hide(); } ==== Adjust rates based on weight and item count ==== Provides three flat rates, increasing the costs if the weight exceeds 10, and removes the express option if there are more than 5 products. rates.add(10001, 5, 'FoxyPost', 'Standard'); rates.add(10002, 9.45, 'FoxyPost', 'Priority'); rates.add(10003, 10, 'FoxyPost', 'Express (Next Day)'); if (cart['_embedded']['fx:shipment']['total_weight'] > 10) { rates.filter(10001).price(6); rates.filter(10002).price(10); rates.filter(10003).price(11.99); } if (cart['_embedded']['fx:shipment']['item_count'] > 5) { rates.filter(10003).hide(); } ==== Free shipping based on a coupon ==== If a specific coupon code (or a code that ends in a specific suffix) is present on the transaction, the following example hides all of the returned rates and instead adds a custom free shipping rate. NOTE that you may want additional logic here, as you may only want to allow free shipping for the contiguous US states, or only within your country, or excluding specific products. This example would allow free shipping if a coupon with a code of ''freeshipping'' was present, or one that had a suffix of ''-fs'', like ''mycode-fs'' or ''h3Sif4yg-fs'' If you don't want to give a discount for the products and only free shipping, your coupon can be set to all the default settings with a ''coupon details'' string of ''0-0''. for (let d in cart['_embedded']['fx:discounts']) { let code = cart['_embedded']['fx:discounts'][d]['code']; if ( code == "freeshipping" // example of a specific code || code.match(/-fs$/) // example of a code suffix ) { rates.hide(); rates.add(11000, 0, '', 'Free Shipping'); } } ==== Free shipping over a certain amount ==== This example provides free shipping if the subtotal for the order (total item price less any coupons) is greater than $50. const total_item_price = cart['_embedded']['fx:shipment']['total_item_price']; let discounts = 0; for (let d in cart['_embedded']['fx:discounts']) { discounts += cart['_embedded']['fx:discounts'][d]['amount']; } rates.add(10001, 8, '', 'Standard Shipping'); if (total_item_price + discounts > 50) { rates.hide(); rates.add(10002, 0, '', 'Free Shipping'); } ==== Modifying Rates by Customer Attributes ==== If you use ''attributes'' on your customers, such as a ''Loyalty_Level'' or ''Wholesaler_Status'', you can key off that to trigger custom rates. The below code is just a partial bit to show how to access customer attributes, which you could combine with other examples on this page. let loyaltyLevel = false; if ( cart._embedded["fx:customer"] && cart._embedded["fx:customer"].id && cart._embedded["fx:customer"].id != 0 && cart._embedded["fx:customer"]._embedded && cart._embedded["fx:customer"]._embedded["fx:attributes"] ) { for ( let i = 0; i < cart._embedded["fx:customer"]._embedded["fx:attributes"].length; i++ ) { const attr = cart._embedded["fx:customer"]._embedded["fx:attributes"][i]; if (attr.name === "Loyalty_Level") { loyaltyLevel = attr.value; } } } if (loyaltyLevel && loyaltyLevel.match(/silver/i)) { rates.hide().add(10003, 0, "", "Free Shipping for Silver Level"); } ==== Restricting available rates based on product categories ==== If the customer has ordered products from the 'food' category, this example will only show overnight delivery options. If the customer has ordered 3 or more food items, update the priority overnight option to be free. let food = 0; for (let p in cart['_embedded']['fx:items']) { let item = cart['_embedded']['fx:items'][p]; switch (item['_embedded']['fx:item_category']['code']) { case "food": food += item['quantity']; break; } } if (food > 0) { // Hide all shipping options, but show overnight rates rates.hide().filter("overnight").show(); // If 3 or more food items, update priority overnight to be free if (food >= 3) rates.filter('priority overnight').price(0); } ==== Restricting available rates based on custom fields ==== This example is similar to the above, but checks the value of a custom field from the session/checkout instead of the category. Review the [[..:shipping#passing_custom_fields_to_the_custom_shipping_endpoint|"Passing custom fields to the custom shipping endpoint" section on the shipping page]] for details on how to pass custom values from the cart/checkout to your shipping code. let my_field = ''; for (let c in cart['_embedded']['fx:custom_fields']) { let custom_field = cart['_embedded']['fx:custom_fields'][c]; if (custom_field.name == 'my_field') { my_field = custom_field.value.toLowerCase(); } } rates.add(10001, 8.99, '', 'Standard Shipping'); rates.add(10002, 15.50, '', 'Express Shipping'); if (my_field == 'my_value') { rates.hide(); rates.filter(10002).price("+3.25").show(); } ==== Restricting to a single carrier for a specific country ==== This example, will only provide FedEx rates to Canada, but allow any other configured live rate carriers for any other countries if (cart['_embedded']['fx:shipment']['country'] == "CA") { rates.hide().filter("fedex").show(); } ==== Calculating shipping fees from custom product options ==== This approach allows for shipping fees to be set per product via a custom product option called ''shipping''. It relies on your product add to carts including an attribute like ''&shipping=12.5''. If there are shipping rates already present (from the native live carrier integrations or the custom shipping endpoint), then the product level shipping fees are added onto the existing rates, otherwise it creates a new rate with a label of "Standard Shipping". If you want to hide the custom product option on the cart, you can do that from the "configuration" page of the Foxy administration, look for the option labelled "Customize Cart Display" and add the product option name you used (''shipping'' in this example). let product_shipping = 0; for (let p in cart['_embedded']['fx:items']) { let item = cart['_embedded']['fx:items'][p]; for (let o in item['_embedded']['fx:item_options']) { let item_option = item['_embedded']['fx:item_options'][o]; if (item_option['name'] == "shipping" && !isNaN(parseFloat(item_option['value']))) { product_shipping += parseFloat(item_option['value']); } } } if (product_shipping > 0) { if (rates.exists()) { rates.price("+" + product_shipping); } else { rates.add(10000, product_shipping, '', 'Standard Shipping'); } } ==== Increase rates by a percentage ==== This example filters rates on USPS and adds a 15% increase to all USPS prices. rates.filter('USPS').price('+15%'); ==== Increase rates by an amount ==== This example filters rates on UPS and adds 4 units of the appropriate currency to all UPS prices. rates.filter('UPS').price('+4'); ==== Getting shipping rates from Third Party Providers ==== If you're needing to get rates from other providers that Foxy doesn't currently support, then using a third-party service like [[https://goshippo.com/|GoShippo]] or [[https://www.easypost.com|EasyPost]] can be a great option. We support fetching their rates through the custom shipping code feature, and you can see details on setting that up on the following pages: * [[v:2.0:shipping:custom_code:goshippo|Fetching shipping rates from GoShippo]] * [[v:2.0:shipping:custom_code:easypost|Fetching shipping rates from EasyPost]] * [[v:2.0:shipping:custom_code:xps|Fetching shipping rates from XPS]] * [[v:2.0:shipping:custom_code:postmen|Fetching shipping rates from Postmen]] ==== Calculate flat rate shipping based on distance ==== For stores that want to calculate a flat rate shipping cost based on the distance, rather than using a live rate provider like USPS, FedEx or UPS, this approach can use the Google Maps API to calculate the distance between the store address and the customers. [[v:2.0:shipping:custom_code:google_maps_distance|Calculating flat rate shipping by distance]] ==== Restricting shipping to a specific map area ==== If you have a specific geographical area that you allow shipping to, you can use Google Maps with the custom shipping code to only show certain options if the customers shipping address is within that area. [[v:2.0:snippets:restrict_shipping_to_specific_map_area|Restrict shipping to a specific map area]]