Advertisement
Guest User

Square API Catalog Export Code for Google Sheets 1.1

a guest
Nov 2nd, 2024
85
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // This function creates a custom menu in the Google Sheets UI
  2. function onOpen() {
  3.   var ui = SpreadsheetApp.getUi();
  4.   ui.createMenu('Square API')
  5.     .addItem('Set API Key', 'setApiKey') // Menu item to set the API key
  6.     .addItem('Set Email Address', 'setEmailAddress') // Menu item to set the email address
  7.     .addItem('Start Processing', 'startProcessing')
  8.     .addItem('Set Daily Timer', 'createDailyTrigger')
  9.     .addToUi();
  10. }
  11.  
  12. // Function to prompt the user to enter their Square API key
  13. function setApiKey() {
  14.   var ui = SpreadsheetApp.getUi();
  15.  
  16.   // Prompt the user for the API key
  17.   var response = ui.prompt('Set Square API Key', 'Please enter your Square API access token:', ui.ButtonSet.OK_CANCEL);
  18.  
  19.   // Process the user's response
  20.   if (response.getSelectedButton() == ui.Button.OK) {
  21.     var apiKey = response.getResponseText().trim();
  22.  
  23.     if (apiKey) {
  24.       // Store the API key in document properties
  25.       var documentProperties = PropertiesService.getDocumentProperties();
  26.       documentProperties.setProperty('SQUARE_ACCESS_TOKEN', apiKey);
  27.  
  28.       // Inform the user that the key has been saved
  29.       ui.alert('Success', 'Your Square API access token has been saved securely.', ui.ButtonSet.OK);
  30.     } else {
  31.       ui.alert('Error', 'No API key entered. Please try again.', ui.ButtonSet.OK);
  32.     }
  33.   } else {
  34.     ui.alert('Operation cancelled.');
  35.   }
  36. }
  37.  
  38. // Function to prompt the user to enter their email address
  39. function setEmailAddress() {
  40.   var ui = SpreadsheetApp.getUi();
  41.  
  42.   // Prompt the user for the email address
  43.   var response = ui.prompt('Set Notification Email', 'Please enter your email address:', ui.ButtonSet.OK_CANCEL);
  44.  
  45.   // Process the user's response
  46.   if (response.getSelectedButton() == ui.Button.OK) {
  47.     var emailAddress = response.getResponseText().trim();
  48.  
  49.     if (emailAddress) {
  50.       // Store the email address in document properties
  51.       var documentProperties = PropertiesService.getDocumentProperties();
  52.       documentProperties.setProperty('NOTIFICATION_EMAIL', emailAddress);
  53.  
  54.       // Inform the user that the email address has been saved
  55.       ui.alert('Success', 'Your email address has been saved.', ui.ButtonSet.OK);
  56.     } else {
  57.       ui.alert('Error', 'No email address entered. Please try again.', ui.ButtonSet.OK);
  58.     }
  59.   } else {
  60.     ui.alert('Operation cancelled.');
  61.   }
  62. }
  63.  
  64. // Main function to start processing
  65. function startProcessing() {
  66.   var ss = SpreadsheetApp.getActiveSpreadsheet();
  67.   var sheetName = 'API-Export';
  68.  
  69.   try {
  70.     // Clear existing sheet if it exists, or create a new one if it doesn't
  71.     var sheet = ss.getSheetByName(sheetName);
  72.     if (sheet) {
  73.       sheet.clear(); // Clear the content but keep the sheet
  74.     } else {
  75.       sheet = ss.insertSheet();
  76.       sheet.setName(sheetName);
  77.     }
  78.  
  79.     // Similar steps for the Progress sheet
  80.     var progressSheetName = 'Processing-Progress';
  81.     var progressSheet = ss.getSheetByName(progressSheetName);
  82.     if (progressSheet) {
  83.       progressSheet.clear(); // Clear the content but keep the sheet
  84.     } else {
  85.       progressSheet = ss.insertSheet();
  86.       progressSheet.setName(progressSheetName);
  87.     }
  88.  
  89.     // Initialize progress indicators
  90.     progressSheet.getRange('A1').setValue('Total Variations:');
  91.     progressSheet.getRange('A2').setValue('Variations Processed:');
  92.     progressSheet.getRange('A3').setValue('Progress (%):');
  93.     progressSheet.getRange('A5').setValue('Type "STOP" in cell B5 to halt processing.');
  94.  
  95.     // Reset stop flag
  96.     progressSheet.getRange('B5').setValue('');
  97.  
  98.     // Fetch all location IDs automatically
  99.     var locationIds = fetchLocationIds();
  100.     if (!locationIds.length) {
  101.       Logger.log("No locations found for the merchant.");
  102.       displayAlert("No locations found for this merchant.");
  103.       return;
  104.     }
  105.  
  106.     // Fetch all catalog items and variations
  107.     var catalogData = fetchAllCatalogItems();
  108.     if (catalogData.items.length === 0) {
  109.       displayAlert("No items found in the catalog.");
  110.       return;
  111.     }
  112.  
  113.     // Fetch all categories
  114.     var categoryMap = fetchAllCategories();
  115.  
  116.     // Update total variations in progress sheet
  117.     progressSheet.getRange('B1').setValue(catalogData.variationCount);
  118.  
  119.     // Create a variation map
  120.     var variationMap = buildVariationMap(catalogData.items, categoryMap);
  121.  
  122.     // Fetch inventory counts for all variations
  123.     var inventoryMap = fetchInventoryCountsForAllVariations(variationMap, locationIds, progressSheet);
  124.  
  125.     // Process variations and write data to the sheet
  126.     processAndWriteData(sheet, variationMap, locationIds, inventoryMap, progressSheet);
  127.  
  128.     // Send success email
  129.     var documentProperties = PropertiesService.getDocumentProperties();
  130.     var emailAddress = documentProperties.getProperty('NOTIFICATION_EMAIL');
  131.  
  132.     if (emailAddress) {
  133.       MailApp.sendEmail({
  134.         to: emailAddress,
  135.         subject: "Square Data Refresh Successful",
  136.         body: "The daily refresh of Square data was completed successfully."
  137.       });
  138.     } else {
  139.       Logger.log('Notification email address is not set. Please use "Set Email Address" in the "Square API" menu.');
  140.     }
  141.  
  142.   } catch (error) {
  143.     // Send failure email
  144.     var documentProperties = PropertiesService.getDocumentProperties();
  145.     var emailAddress = documentProperties.getProperty('NOTIFICATION_EMAIL');
  146.  
  147.     if (emailAddress) {
  148.       MailApp.sendEmail({
  149.         to: emailAddress,
  150.         subject: "Square Data Refresh Failed",
  151.         body: "The daily Square data refresh failed with the following error: " + error.message
  152.       });
  153.     } else {
  154.       Logger.log('Notification email address is not set. Please use "Set Email Address" in the "Square API" menu.');
  155.     }
  156.  
  157.     // Log the error in the Spreadsheet
  158.     Logger.log("Error: " + error.message);
  159.     // Optionally, show an alert if running manually
  160.     displayAlert("An error occurred: " + error.message);
  161.   }
  162. }
  163.  
  164. // Function to create a time-driven trigger to refresh data daily
  165. function createDailyTrigger() {
  166.   // First, delete any existing triggers to avoid duplicates
  167.   deleteExistingTriggers();
  168.  
  169.   // Set a time-driven trigger to run the startProcessing function every day at 8 AM
  170.   ScriptApp.newTrigger('startProcessing')
  171.     .timeBased()
  172.     .atHour(8)  // Set the time here (8 AM in this case)
  173.     .everyDays(1)  // Run every day
  174.     .create();
  175.  
  176.   // Show confirmation to the user
  177.   SpreadsheetApp.getUi().alert("Daily timer has been set for 8 AM.");
  178. }
  179.  
  180. // Function to delete any existing time-based triggers for 'startProcessing' to avoid duplicates
  181. function deleteExistingTriggers() {
  182.   var triggers = ScriptApp.getProjectTriggers();
  183.   for (var i = 0; i < triggers.length; i++) {
  184.     if (triggers[i].getHandlerFunction() == 'startProcessing') {
  185.       ScriptApp.deleteTrigger(triggers[i]);
  186.     }
  187.   }
  188. }
  189.  
  190. // Function to fetch all catalog items and variations
  191. function fetchAllCatalogItems() {
  192.   var allItems = [];
  193.   var variationCount = 0;
  194.   var cursor = null;
  195.   var listCatalogUrl = 'https://connect.squareup.com/v2/catalog/list';
  196.  
  197.   do {
  198.     var response = fetchCatalogPage(listCatalogUrl, cursor);
  199.  
  200.     if (response.getResponseCode() === 200) {
  201.       var jsonData = JSON.parse(response.getContentText());
  202.  
  203.       if (Array.isArray(jsonData.objects) && jsonData.objects.length > 0) {
  204.         jsonData.objects.forEach(function(item) {
  205.           if (item.type === 'ITEM') {
  206.             allItems.push(item);
  207.  
  208.             if (Array.isArray(item.item_data.variations)) {
  209.               variationCount += item.item_data.variations.length;
  210.             }
  211.           }
  212.         });
  213.       }
  214.  
  215.       cursor = jsonData.cursor || null;
  216.     } else {
  217.       Logger.log("Error details from List Catalog: " + response.getContentText());
  218.       displayAlert("Error retrieving catalog. Check logs for details.");
  219.       return { items: [], variationCount: 0 };
  220.     }
  221.  
  222.   } while (cursor);
  223.  
  224.   Logger.log("Total Items Retrieved: " + allItems.length);
  225.   Logger.log("Total Variations Retrieved: " + variationCount);
  226.  
  227.   return { items: allItems, variationCount: variationCount };
  228. }
  229.  
  230. // Function to fetch a catalog page
  231. function fetchCatalogPage(listCatalogUrl, cursor) {
  232.   var headers = {
  233.     "Square-Version": "2023-10-18",
  234.     "Content-Type": "application/json"
  235.   };
  236.  
  237.   var urlWithCursor = cursor ? listCatalogUrl + "?cursor=" + cursor + "&include_related_objects=true" : listCatalogUrl + "?include_related_objects=true";
  238.  
  239.   var listOptions = {
  240.     "method": "GET",
  241.     "headers": headers,
  242.     "muteHttpExceptions": true
  243.   };
  244.  
  245.   return makeApiRequest(urlWithCursor, listOptions);
  246. }
  247.  
  248. // Function to fetch all categories and build a category map
  249. function fetchAllCategories() {
  250.   var allCategories = {};
  251.   var cursor = null;
  252.   var listCatalogUrl = 'https://connect.squareup.com/v2/catalog/list';
  253.  
  254.   do {
  255.     var urlWithCursor = cursor ? listCatalogUrl + "?cursor=" + cursor + "&types=CATEGORY" : listCatalogUrl + "?types=CATEGORY";
  256.     var headers = {
  257.       "Square-Version": "2023-10-18",
  258.       "Content-Type": "application/json"
  259.     };
  260.     var options = {
  261.       "method": "GET",
  262.       "headers": headers,
  263.       "muteHttpExceptions": true
  264.     };
  265.     var response = makeApiRequest(urlWithCursor, options);
  266.  
  267.     if (response.getResponseCode() === 200) {
  268.       var jsonData = JSON.parse(response.getContentText());
  269.  
  270.       if (Array.isArray(jsonData.objects) && jsonData.objects.length > 0) {
  271.         jsonData.objects.forEach(function(obj) {
  272.           if (obj.type === "CATEGORY") {
  273.             allCategories[obj.id] = obj.category_data.name;
  274.           }
  275.         });
  276.       }
  277.  
  278.       cursor = jsonData.cursor || null;
  279.     } else {
  280.       Logger.log("Error fetching categories: " + response.getContentText());
  281.       displayAlert("Error retrieving categories. Check logs for details.");
  282.       return {};
  283.     }
  284.  
  285.   } while (cursor);
  286.  
  287.   Logger.log("Total Categories Retrieved: " + Object.keys(allCategories).length);
  288.  
  289.   return allCategories;
  290. }
  291.  
  292. // Function to build a variation map
  293. function buildVariationMap(items, categoryMap) {
  294.   var variationMap = {};
  295.  
  296.   items.forEach(function(item) {
  297.     if (item.item_data && Array.isArray(item.item_data.variations)) {
  298.       var itemId = item.id || "";
  299.       var itemName = item.item_data.name || "";
  300.       var description = item.item_data.description || "";
  301.       var itemUrl = item.item_data.ecom_uri || "";
  302.  
  303.       // Get Image URLs (primary and additional)
  304.       var imageUrls = [];
  305.       if (Array.isArray(item.item_data.ecom_image_uris) && item.item_data.ecom_image_uris.length > 0) {
  306.         imageUrls = item.item_data.ecom_image_uris; // Use ecom_image_uris first
  307.       } else if (Array.isArray(item.image_ids) && item.image_ids.length > 0) {
  308.         imageUrls = getImageUrls(item.image_ids, item.related_objects || []);  // Fallback to image_ids
  309.       }
  310.  
  311.       // Ensure the columns for image URLs
  312.       var primaryImageUrl = imageUrls[0] || "";
  313.       var secondaryImageUrl = imageUrls[1] || "";
  314.       var tertiaryImageUrl = imageUrls[2] || "";
  315.  
  316.       // Additional fields to capture
  317.       var isDeleted = item.is_deleted || false;
  318.       var catalogV1Ids = Array.isArray(item.catalog_v1_ids) ? item.catalog_v1_ids.map(function(id) {
  319.         return id.catalog_v1_id;
  320.       }).join(", ") : "";
  321.       var itemVisibility = item.item_data.visibility || "";
  322.       var categoryId = item.item_data.category_id || "";
  323.       var categoryName = categoryMap[categoryId] || "";
  324.       var modifierListInfo = item.item_data.modifier_list_info ? JSON.stringify(item.item_data.modifier_list_info) : "";
  325.       var productType = item.item_data.product_type || "";
  326.       var skipModifierScreen = item.item_data.skip_modifier_screen || false;
  327.       var taxIds = Array.isArray(item.item_data.tax_ids) ? item.item_data.tax_ids.join(", ") : "";
  328.       var itemOptions = item.item_data.item_options ? JSON.stringify(item.item_data.item_options) : "";
  329.       var availableOnline = item.item_data.available_online || false;
  330.       var availableForPickup = item.item_data.available_for_pickup || false;
  331.  
  332.       // Item-level availability fields
  333.       var itemPresentAtAllLocations = item.hasOwnProperty('present_at_all_locations') ? item.present_at_all_locations : false;
  334.       var itemPresentAtLocationIds = Array.isArray(item.present_at_location_ids) ? item.present_at_location_ids : [];
  335.       var itemAbsentAtLocationIds = Array.isArray(item.absent_at_location_ids) ? item.absent_at_location_ids : [];
  336.  
  337.       item.item_data.variations.forEach(function(variation) {
  338.         var variationId = variation.id || "";
  339.         var variationName = variation.item_variation_data.name || "";
  340.         var price = variation.item_variation_data.price_money
  341.           ? variation.item_variation_data.price_money.amount / 100
  342.           : "";
  343.  
  344.         // Retrieve the GTIN (UPC/EAN/ISBN)
  345.         var gtin = variation.item_variation_data.upc || "";
  346.  
  347.         // Extract item option values for this variation
  348.         var itemOptionValues = "";
  349.         if (Array.isArray(variation.item_variation_data.item_option_values)) {
  350.           itemOptionValues = JSON.stringify(variation.item_variation_data.item_option_values);
  351.         }
  352.  
  353.         // New fields
  354.         var sku = variation.item_variation_data.sku || "";
  355.         var customAttributes = variation.custom_attribute_values ? JSON.stringify(variation.custom_attribute_values) : "";
  356.         var measurementUnitId = variation.item_variation_data.measurement_unit_id || "";
  357.         var pricingType = variation.item_variation_data.pricing_type || "";
  358.         var updatedAt = variation.updated_at || item.updated_at || "";
  359.  
  360.         // Availability fields
  361.         var presentAtAllLocations = variation.hasOwnProperty('present_at_all_locations') ? variation.present_at_all_locations : null;
  362.         var presentAtLocationIds = Array.isArray(variation.present_at_location_ids) ? variation.present_at_location_ids : null;
  363.         var absentAtLocationIds = Array.isArray(variation.absent_at_location_ids) ? variation.absent_at_location_ids : null;
  364.  
  365.         // If variation-level availability fields are null, use item-level fields
  366.         if (presentAtAllLocations === null) {
  367.           presentAtAllLocations = itemPresentAtAllLocations;
  368.         }
  369.         if (presentAtLocationIds === null) {
  370.           presentAtLocationIds = itemPresentAtLocationIds;
  371.         }
  372.         if (absentAtLocationIds === null) {
  373.           absentAtLocationIds = itemAbsentAtLocationIds;
  374.         }
  375.  
  376.         // Add location-specific overrides
  377.         var locationData = {};
  378.         if (Array.isArray(variation.item_variation_data.location_overrides)) {
  379.           variation.item_variation_data.location_overrides.forEach(function(override) {
  380.             var locId = override.location_id;
  381.             locationData[locId] = {
  382.               track_inventory: override.track_inventory || false,
  383.               inventory_alert_type: override.inventory_alert_type || "",
  384.               inventory_alert_threshold: override.inventory_alert_threshold || ""
  385.             };
  386.           });
  387.         }
  388.  
  389.         variationMap[variationId] = {
  390.           variationId: variationId,
  391.           itemId: itemId,
  392.           itemName: itemName,
  393.           description: description,
  394.           itemUrl: itemUrl,
  395.           variationName: variationName,
  396.           price: price,
  397.           gtin: gtin,
  398.           isDeleted: isDeleted,
  399.           catalogV1Ids: catalogV1Ids,
  400.           presentAtAllLocations: presentAtAllLocations,
  401.           presentAtLocationIds: presentAtLocationIds,
  402.           absentAtLocationIds: absentAtLocationIds,
  403.           itemVisibility: itemVisibility,
  404.           categoryId: categoryId,
  405.           categoryName: categoryName,
  406.           modifierListInfo: modifierListInfo,
  407.           productType: productType,
  408.           skipModifierScreen: skipModifierScreen,
  409.           taxIds: taxIds,
  410.           itemOptions: itemOptions,
  411.           itemOptionValues: itemOptionValues,
  412.           sku: sku,
  413.           customAttributes: customAttributes,
  414.           measurementUnitId: measurementUnitId,
  415.           pricingType: pricingType,
  416.           availableOnline: availableOnline,
  417.           availableForPickup: availableForPickup,
  418.           updatedAt: updatedAt,
  419.           locationData: locationData,
  420.           images: [primaryImageUrl, secondaryImageUrl, tertiaryImageUrl]
  421.         };
  422.       });
  423.     }
  424.   });
  425.  
  426.   return variationMap;
  427. }
  428.  
  429. // Function to fetch inventory counts for all variations using Retrieve Inventory Count endpoint
  430. function fetchInventoryCountsForAllVariations(variationMap, locationIds, progressSheet) {
  431.   var inventoryMap = {};
  432.   var variationIds = Object.keys(variationMap);
  433.   var headers = {
  434.     "Square-Version": "2023-10-18",
  435.     "Content-Type": "application/json"
  436.   };
  437.  
  438.   var variationsProcessed = 0;
  439.  
  440.   for (var i = 0; i < variationIds.length; i++) {
  441.     var variationId = variationIds[i];
  442.  
  443.     // Check if the user requested to stop processing
  444.     var stopFlag = progressSheet.getRange('B5').getValue().toString().toUpperCase();
  445.     if (stopFlag === 'STOP') {
  446.       Logger.log('Processing halted by user during inventory fetching.');
  447.       break;
  448.     }
  449.  
  450.     // Throttle requests to avoid rate limits
  451.     if (i > 0 && i % 100 == 0) {
  452.       Logger.log('Sleeping for 1 second to avoid rate limits...');
  453.       Utilities.sleep(1000); // Sleep for 1 second
  454.     }
  455.  
  456.     var cursor = null;
  457.     do {
  458.       var url = 'https://connect.squareup.com/v2/inventory/' + variationId;
  459.       if (locationIds.length > 0) {
  460.         url += '?location_ids=' + locationIds.join(',');
  461.       }
  462.       if (cursor) {
  463.         url += (locationIds.length > 0 ? '&' : '?') + 'cursor=' + cursor;
  464.       }
  465.  
  466.       var options = {
  467.         "method": "GET",
  468.         "headers": headers,
  469.         "muteHttpExceptions": true
  470.       };
  471.  
  472.       var response = makeApiRequest(url, options);
  473.  
  474.       if (response.getResponseCode() === 200) {
  475.         var data = JSON.parse(response.getContentText());
  476.         if (Array.isArray(data.counts)) {
  477.           data.counts.forEach(function(count) {
  478.             var key = count.catalog_object_id + '_' + count.location_id;
  479.             var quantity = parseInt(count.quantity || "0", 10);
  480.             inventoryMap[key] = {
  481.               quantity: quantity,
  482.               availability: quantity > 0 ? 'Available' : 'Unavailable'
  483.             };
  484.           });
  485.         } else {
  486.           Logger.log("No counts found for variation ID " + variationId);
  487.         }
  488.  
  489.         cursor = data.cursor || null;
  490.       } else {
  491.         Logger.log("Error retrieving inventory count for variation ID " + variationId + ": " + response.getContentText());
  492.         displayAlert("Error retrieving inventory count for variation ID " + variationId + ": " + response.getContentText());
  493.         break; // Exit the loop on error
  494.       }
  495.     } while (cursor);
  496.  
  497.     variationsProcessed++;
  498.  
  499.     // Update progress indicators every 100 variations
  500.     if (variationsProcessed % 100 === 0) {
  501.       var progressPercent = Math.round((variationsProcessed / variationIds.length) * 100);
  502.       progressSheet.getRange('B2').setValue(variationsProcessed);
  503.       progressSheet.getRange('B3').setValue(progressPercent);
  504.       SpreadsheetApp.flush();
  505.     }
  506.   }
  507.  
  508.   Logger.log("Total Inventory Counts Retrieved: " + Object.keys(inventoryMap).length);
  509.   return inventoryMap;
  510. }
  511.  
  512. // Function to process variations and write data to the sheet
  513. function processAndWriteData(sheet, variationMap, locationIds, inventoryMap, progressSheet) {
  514.   // Prepare the header row dynamically to include all the extra fields and inventory columns for each location
  515.   var headerRow = [
  516.     "Variation ID (ID-B)", "Item ID (ID-A)", "Title", "Link", "Description", "Variation Name", "Price (CAD)",
  517.     "GTIN (UPC/EAN/ISBN)", "SKU", "Custom Attributes", "Item Options", "Modifier Lists", "Product Type", "Measurement Unit",
  518.     "Pricing Type", "Visibility", "Available Online", "Available for Pickup", "Updated At", "is_deleted",
  519.     "catalog_v1_ids", "present_at_all_locations", "item_visibility", "category_id", "category_name",
  520.     "modifier_list_info", "product_type", "skip_modifier_screen", "tax_ids", "item_option_values"
  521.   ];
  522.  
  523.   // Add columns for location overrides for each location
  524.   locationIds.forEach(function(locationId) {
  525.     headerRow.push("Track Inventory at " + locationId);
  526.     headerRow.push("Inventory Alert Type at " + locationId);
  527.     headerRow.push("Inventory Alert Threshold at " + locationId);
  528.   });
  529.  
  530.   // Add columns for availability at each location based on catalog data (active/inactive)
  531.   locationIds.forEach(function(locationId) {
  532.     headerRow.push("Is Active at " + locationId);
  533.   });
  534.  
  535.   // Add columns for inventory counts at each location
  536.   locationIds.forEach(function(locationId) {
  537.     headerRow.push("Inventory at " + locationId);
  538.   });
  539.  
  540.   headerRow.push("Image Link", "Additional Image Link 1", "Additional Image Link 2"); // Add extra image columns at the end
  541.  
  542.   // Write header row to the sheet
  543.   sheet.appendRow(headerRow);
  544.  
  545.   var allRows = [];
  546.   var variationsProcessed = 0;
  547.   var stopProcessing = false; // Flag to control processing based on user input
  548.  
  549.   // Iterate over variationMap
  550.   for (var variationId in variationMap) {
  551.     if (variationMap.hasOwnProperty(variationId)) {
  552.       // Check if the user requested to stop processing
  553.       var stopFlag = progressSheet.getRange('B5').getValue().toString().toUpperCase();
  554.       if (stopFlag === 'STOP') {
  555.         Logger.log('Processing halted by user.');
  556.         stopProcessing = true;
  557.         break;
  558.       }
  559.  
  560.       var variationData = variationMap[variationId];
  561.  
  562.       // Retrieve inventory counts for each location
  563.       var inventoryCounts = [];
  564.       var availabilityStatuses = [];
  565.  
  566.       locationIds.forEach(function(locationId) {
  567.         var key = variationId + '_' + locationId;
  568.         var inventoryInfo = inventoryMap.hasOwnProperty(key) ? inventoryMap[key] : { quantity: 0, availability: 'Unavailable' };
  569.         inventoryCounts.push(inventoryInfo.quantity);
  570.         availabilityStatuses.push(inventoryInfo.availability);
  571.       });
  572.  
  573.       // Determine if variation is active at each location based on catalog data
  574.       var activeStatuses = [];
  575.       locationIds.forEach(function(locationId) {
  576.         var isActive = isVariationActiveAtLocation(variationData, locationId);
  577.         activeStatuses.push(isActive ? 'Active' : 'Inactive');
  578.       });
  579.  
  580.       // Add location-specific overrides
  581.       var locationOverrides = [];
  582.       locationIds.forEach(function(locationId) {
  583.         var locData = variationData.locationData[locationId] || {};
  584.         locationOverrides.push(locData.track_inventory || "");
  585.         locationOverrides.push(locData.inventory_alert_type || "");
  586.         locationOverrides.push(locData.inventory_alert_threshold || "");
  587.       });
  588.  
  589.       // Prepare the row data
  590.       var rowData = [
  591.         variationData.variationId, variationData.itemId, variationData.itemName, variationData.itemUrl,
  592.         variationData.description, variationData.variationName, variationData.price, variationData.gtin,
  593.         variationData.sku, variationData.customAttributes, variationData.itemOptions, variationData.modifierListInfo,
  594.         variationData.productType, variationData.measurementUnitId, variationData.pricingType, variationData.itemVisibility,
  595.         variationData.availableOnline, variationData.availableForPickup, variationData.updatedAt, variationData.isDeleted,
  596.         variationData.catalogV1Ids, variationData.presentAtAllLocations, variationData.itemVisibility,
  597.         variationData.categoryId, variationData.categoryName, variationData.modifierListInfo, variationData.productType,
  598.         variationData.skipModifierScreen, variationData.taxIds, variationData.itemOptionValues
  599.       ].concat(locationOverrides, activeStatuses, inventoryCounts, variationData.images);
  600.  
  601.       allRows.push(rowData);
  602.  
  603.       variationsProcessed++;
  604.  
  605.       // Update progress indicators every 100 variations
  606.       if (variationsProcessed % 100 === 0) {
  607.         var progressPercent = Math.round((variationsProcessed / Object.keys(variationMap).length) * 100);
  608.         progressSheet.getRange('B2').setValue(variationsProcessed);
  609.         progressSheet.getRange('B3').setValue(progressPercent);
  610.         SpreadsheetApp.flush();
  611.       }
  612.  
  613.       // Write data to the sheet in batches of 500 rows
  614.       if (allRows.length >= 500) {
  615.         var range = sheet.getRange(sheet.getLastRow() + 1, 1, allRows.length, headerRow.length);
  616.         range.setValues(allRows);
  617.         allRows = [];
  618.       }
  619.     }
  620.   }
  621.  
  622.   // Write any remaining data to the sheet
  623.   if (allRows.length > 0) {
  624.     var range = sheet.getRange(sheet.getLastRow() + 1, 1, allRows.length, headerRow.length);
  625.     range.setValues(allRows);
  626.   }
  627.  
  628.   // Update final progress indicators
  629.   progressSheet.getRange('B2').setValue(variationsProcessed);
  630.   progressSheet.getRange('B3').setValue(100);
  631.   SpreadsheetApp.flush();
  632.  
  633.   if (!stopProcessing) {
  634.     displayAlert("Processing complete.");
  635.   }
  636. }
  637.  
  638. // Helper function to determine if a variation is active at a specific location
  639. function isVariationActiveAtLocation(variationData, locationId) {
  640.   if (variationData.presentAtAllLocations === true) {
  641.     if (variationData.absentAtLocationIds && variationData.absentAtLocationIds.includes(locationId)) {
  642.       return false; // Inactive at this location
  643.     } else {
  644.       return true; // Active at this location
  645.     }
  646.   } else {
  647.     // presentAtAllLocations is false or undefined
  648.     if (variationData.presentAtLocationIds && variationData.presentAtLocationIds.includes(locationId)) {
  649.       return true; // Active at this location
  650.     } else {
  651.       return false; // Inactive at this location
  652.     }
  653.   }
  654. }
  655.  
  656. // Function to get image URLs from image IDs and related objects
  657. function getImageUrls(imageIds, relatedObjects) {
  658.   if (Array.isArray(imageIds) && imageIds.length > 0) {
  659.     return imageIds.map(function(imageId) {
  660.       var imageObject = relatedObjects.find(function(obj) {
  661.         return obj.id === imageId && obj.type === "IMAGE";
  662.       });
  663.       return imageObject ? imageObject.image_data.url : "";
  664.     });
  665.   } else {
  666.     return [];
  667.   }
  668. }
  669.  
  670. // Fetch location IDs for the merchant and return as an array
  671. function fetchLocationIds() {
  672.   var locationApiUrl = 'https://connect.squareup.com/v2/locations';
  673.  
  674.   var headers = {
  675.     "Square-Version": "2023-10-18",
  676.     "Content-Type": "application/json"
  677.   };
  678.  
  679.   var options = {
  680.     "method": "GET",
  681.     "headers": headers,
  682.     "muteHttpExceptions": true
  683.   };
  684.  
  685.   var response = makeApiRequest(locationApiUrl, options);
  686.   var locationIds = [];
  687.  
  688.   if (response.getResponseCode() === 200) {
  689.     var jsonData = JSON.parse(response.getContentText());
  690.     if (Array.isArray(jsonData.locations) && jsonData.locations.length > 0) {
  691.       locationIds = jsonData.locations.map(function(location) {
  692.         return location.id;
  693.       });
  694.     } else {
  695.       Logger.log("No locations found in the API response.");
  696.       displayAlert("No locations found for this merchant.");
  697.     }
  698.   } else {
  699.     Logger.log("Error retrieving locations: " + response.getContentText());
  700.     displayAlert("Error retrieving locations. Check logs.");
  701.   }
  702.  
  703.   return locationIds;
  704. }
  705.  
  706. // Function to handle API requests and token management
  707. function makeApiRequest(url, options) {
  708.   var documentProperties = PropertiesService.getDocumentProperties();
  709.   var accessToken = documentProperties.getProperty('SQUARE_ACCESS_TOKEN');
  710.  
  711.   if (!accessToken) {
  712.     // Prompt the user to set the API key
  713.     displayAlert('Access token is missing. Please use the "Set API Key" option in the "Square API" menu to provide your access token.');
  714.     throw new Error('Access token is required to proceed. Please set it using the "Set API Key" menu option.');
  715.   }
  716.  
  717.   // Ensure options.headers exists
  718.   if (!options.headers) {
  719.     options.headers = {};
  720.   }
  721.  
  722.   // Ensure the Authorization header has the correct token
  723.   options.headers["Authorization"] = "Bearer " + accessToken;
  724.  
  725.   var response = UrlFetchApp.fetch(url, options);
  726.   var statusCode = response.getResponseCode();
  727.  
  728.   if (statusCode === 401) {
  729.     // Unauthorized, token may be invalid or expired
  730.     // Send email notification instead of prompting
  731.     var emailAddress = documentProperties.getProperty('NOTIFICATION_EMAIL');
  732.  
  733.     if (emailAddress) {
  734.       MailApp.sendEmail({
  735.         to: emailAddress,
  736.         subject: "Square Data Refresh Failed - Invalid Access Token",
  737.         body: "The access token used for the Square API is invalid or expired. Please update it using the 'Set API Key' option in the 'Square API' menu."
  738.       });
  739.     } else {
  740.       Logger.log('Notification email address is not set. Please use "Set Email Address" in the "Square API" menu.');
  741.     }
  742.     throw new Error('Access token is invalid or expired.');
  743.   } else if (statusCode >= 200 && statusCode < 300) {
  744.     // Success
  745.     return response;
  746.   } else {
  747.     // Other errors
  748.     Logger.log('API request failed with status code ' + statusCode + ': ' + response.getContentText());
  749.     throw new Error('API request failed with status code ' + statusCode);
  750.   }
  751. }
  752.  
  753. // Helper function to display alerts only when UI is available
  754. function displayAlert(message) {
  755.   try {
  756.     SpreadsheetApp.getUi().alert(message);
  757.   } catch (e) {
  758.     // UI not available (e.g., running via trigger), so we skip showing the alert
  759.     Logger.log("Alert: " + message);
  760.   }
  761. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement