{"data":{"logo":{"childImageSharp":{"fixed":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABHklEQVQY00WRvyvGYRTFP6/3tVtkMxmUyWqSP8BmkKQMkvRmNbxJSsmk8xVlU1IU8qMMfhSlDAYDyiSTRUzGe/R8v48Mt+ee7j3nnnsfLPoslix+LD4sVkJ0W+CCugtqZS7aXJRRTzj+cFXrsuhPeQLjIV4triyKELdWRXIBUZRvLTIuQzn+8aHFYsrJytsWNxZrFmMWwyFaFlsWg5k0k/vmLBohmhZnFkOJG2LBYjk1tlvsh7DFXiZfW1xY7FhsWoxmsXWLyzz0IMRUVLXvEE8WA4ncsHi3GLG4CzFpcW/Ra7FhsZrP0ZMHzFocu6BZnkTMW7xZnIaYJn/Kl0VndpbWfrTosDixeLB4Ts5CfFqcWxxZvCTXUYntWkwkQ7/BJu9d4GccFQAAAABJRU5ErkJggg==","width":400,"height":124,"src":"/static/e85f9e27853b1ae2b55ad25e0aa99c71/140ea/knack-logo-orange.png","srcSet":"/static/e85f9e27853b1ae2b55ad25e0aa99c71/140ea/knack-logo-orange.png 1x,\n/static/e85f9e27853b1ae2b55ad25e0aa99c71/26d9e/knack-logo-orange.png 1.5x,\n/static/e85f9e27853b1ae2b55ad25e0aa99c71/a3fa0/knack-logo-orange.png 2x,\n/static/e85f9e27853b1ae2b55ad25e0aa99c71/4cc84/knack-logo-orange.png 3x"}}},"markdownRemark":{"html":"<h2>Why does our Laravel app need a Salesforce integration?</h2>\n<p>Knack Tutoring is a unique peer-tutoring platform used by colleges and universities across the US. Students can find tutors who attend their specific school and excelled in the exact courses they are enrolled in. Dozens of higher ed institutions rely on Knack to handle their tutoring needs, and with that comes the need to share student tutoring data with college administrators in ways that are most convenient for them.</p>\n<p>At Knack we partner with schools all over the US with student populations ranging from the hundreds to tens of thousands. Schools such as University of Florida and Auburn University have already found that students using Knack are 10% more likely to pass courses. When schools deploy Knack, it is important that they retain sight on how often students are using Knack tutoring services.</p>\n<p>Many of our partners use Salesforce. Salesforce has an Education Data Architecture (EDA) extension that schools implement to allow them to securely and privately upload and share course catalogs, student enrollment data, and more. Many schools also utilize the <a href=\"https://appexchange.salesforce.com/appxListingDetail?listingId=a0N3A00000EJjbtUAD\">Student Success Hub for Higher Education (formerly Advisor Link)</a> for advising. We decided to build an integration to allow us to sync tutoring session data nightly with our partners’ Salesforce instances.</p>\n<p><span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 641px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 26.365054602184085%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAABKklEQVQY012Py0rDYBCF8youfAKfw6Uv4FI3Lgq6ced76ELBVTfiQlqk1EhNa1srir3EUOglf5qkzaXVGpL/k0RaxQPDgRnONzMKCxOCPivJPx4EIYZhoOs6nU4nc8/3s7lYRNyN5hj+129WShRZ2of7Q6T7ggwHSK+HdF+he8HUNCjelrgrl8nn8xlwtS6nmmyctdm67JFTxyntB0jtBFk+gLRKe1DcRVaO4WYHR6+gPTZpNOoUCgWGw+H6mqPKhM1zne3rAadtf91XkoUN8xHy00G6b0jvHenpyDANx/xX+vI8DHA9n6u2oG9NST5CgiBguVyimMJmbDmYls14MsNyPMzJjJGwcaczkiQhjuPMoyii1WrxoGlUNY3uc5OnRp1qrYaqqggh+AZZzmtvvY5vgAAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Tutoring Session data synced nightly\"\n        title=\"Tutoring Session data synced nightly\"\n        src=\"/static/cd942f6c86afaff75f07eb3289a808d2/31fd3/knack-arrow-salesforce.png\"\n        srcset=\"/static/cd942f6c86afaff75f07eb3289a808d2/bc34b/knack-arrow-salesforce.png 293w,\n/static/cd942f6c86afaff75f07eb3289a808d2/da9f0/knack-arrow-salesforce.png 585w,\n/static/cd942f6c86afaff75f07eb3289a808d2/31fd3/knack-arrow-salesforce.png 641w\"\n        sizes=\"(max-width: 641px) 100vw, 641px\"\n      />\n  </span></p>\n<p>Our Laravel backend integrates with our <a href=\"https://appexchange.salesforce.com/appxListingDetail?listingId=a0N4V00000HZVVdUAP&#x26;tab=e\">Salesforce package</a> to transfer tutoring session data from the Knack database to our partners’ Salesforce instances. We use several Salesforce API endpoints to GET and POST data to our partners’ instance. This way their administrators are able to access Knack tutoring data directly from the Salesforce instance instead of needing to log into Knack specifically to view tutoring data.  Among other things, this integration empowers administrators to encourage specific students to seek out tutoring and allows them to see which courses at their institution require the most student tutoring.</p>\n<h2>Building our Salesforce Package</h2>\n<p>The different types of Salesforce packages can be read about <a href=\"https://help.salesforce.com/s/articleView?id=sf.c360_a_packaging_in_customer_360_audiences.htm&#x26;type=5\">here</a>, but for brevity I am just going to share that we decided to use <a href=\"https://www.salesforce.com/products/platform/best-practices/declarative-programming-vs-imperative-programming/\">Clicks Not Code</a> to create a managed package containing a Connected app as one of its components. A Connected app is the type of Salesforce component that allows an external application (Knack) to securely integrate with a Salesforce instance via APIs. It is important to note that our entire solution is called a package, and one of many components in our package is the Connected app which is used for authentication when using an API. This led to confusion for us early on because the <a href=\"https://appexchange.salesforce.com/\"><em>AppExchange</em></a> is used to download <em>packages</em>, which can <em>optionally contain a component called a Connected app</em>.</p>\n<h3>Selecting the Right Components</h3>\n<p>Our Salesforce package is made up of Custom Objects, their related Fields, a Tab, Page Layouts, and the previously described Connected app, which are some of the many types of components that can be present in a Salesforce package. It is also a hard requirement that anyone installing the Knack Salesforce package already has the previously described Education Data Architecture extension installed on their Salesforce instance.</p>\n<p><span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 61.17804551539492%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAACEUlEQVQoz21S2W7bMBDU//9Zn5LUceLGtqz7oESKui+SU+zKTVGgAgZa7jEcDulprSGEgJQKQRgjSTI8HiG+rnfc/QBRnOJ297nWdgNKUSNNc2RZgVoq1LJBXSvOq6aF13UdqrqGVApCVBBVhX4YMIwjhmHkmFAKgbwokOU5giDEIwg4F8Ux4iThGs17fd+j61pEYYjX1xdcLp+Qssa6LljXFdu2Yp5nfHyccTqdUFWCa9YaOGehlMT1+gXdNDBmh0dDDg55keP9/I4wDmGswW72fzBOI6Z5gm41hmngHPVRvmkbru12h9dPC5rF4uevO368nHAJYrSrg5oNo6H/tKPQI1LVQY4b9Go5pxeLTPW4PGI8MsG9HskHLPI8RVnkEGWOssyhGwVrdsAZGLMhyxJkaQIhCkzTAOcMzw1Dh+Bxh6wFr71lWbiYJBF8/8bERZHBmBXW7rB2Y0IiWZYRXac5pppzO8ckQMqKc97CCoHz+R1lWXBMZjPs8Sezb7crw/fv0LqhLkbfd7hdv56z7q/CNI34OH+OSMoO7DD7CiFyBimZ/6NQN5LXT4WOr/7t9QXLMgHO8rM4nsaxQRJHrE6pmr2lGnk2jj2iKICSNc89CS379/l5xrYdig//djh7EJJ68pkU0Uc2fF9K4LNKmvsmTNMEaRpzfJA9FdqDnJTM04i+bzHPIw8TKKbNSCH1/gZXapRVas0XLAAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Truncated list of components\"\n        title=\"Truncated list of components\"\n        src=\"/static/c4b5cf1197fdccc0738335898e3e1834/913fc/truncated-component-list.png\"\n        srcset=\"/static/c4b5cf1197fdccc0738335898e3e1834/bc34b/truncated-component-list.png 293w,\n/static/c4b5cf1197fdccc0738335898e3e1834/da9f0/truncated-component-list.png 585w,\n/static/c4b5cf1197fdccc0738335898e3e1834/913fc/truncated-component-list.png 1170w,\n/static/c4b5cf1197fdccc0738335898e3e1834/83927/truncated-component-list.png 1494w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </span></p>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">A view of some of the custom components that are in our Salesforce package</p>\n<p>Custom Objects are created and updated using the Object Manager, which can be found by clicking the gear in the top right corner of your Salesforce instance, clicking “Setup”, and searching “Object Manager” in the Quick Find search bar on the far left of your screen. Once you are in Object Manager click the “Create” button and then Custom Object.</p>\n<p><span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 51.97710718002081%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAACDUlEQVQoz3WQa0/TABSG+4/54o/wg8IHEyMmRlBjTLxhINNkgDgYEhAXYGzrrl0v67qu13Vt1659zBaJIeBJ3pxzkvc85+QIjY6MPTKROn2aYo/BcIxmOGgjF9lwUA17mWXDpT9yUQcmSkdG66mo/QF6t8+41SbSJCRJQwiULkxsfleaPPl0zMGpyN6JyP6FxGFVZq+qcdYaclJTOKsrlK41jhoGB5c9SlddipUmtY6CYTrsnLQRLNeFLKGuemyU6nxrW1ypFueyyW5D53O5wpfCFi2pjznUKV9X2dxY4+X2O3Yua3y46LP+/ZDnhRc8KxwieL5PFE/pGz71KxHbC6idl6keFQinU8q726yuPWC/+BrxeIvSx0esP1zh6ZvHlE7fI178YOvtKq82V/j6s4jgjHRcXUGUxqijCbMoRFYHaLpBHM+wbJ8gjhnbJoo2IAo8nEnIcOwQ+CZjQ8W3RjBPOROHCG5XZDaQGHZkKucirYaM0h7QbemIioOouLQ1j54e0F1qsuxbqrfsm9qEljahq/v8Ek0Ey7YJpgFBOMN1XeZpSp7nsBD5sr6t7FbNTSYnihMEz/OIoxAviPGjOYvIspzs71CapjiOQ5Ik98D/+ZJ5xtiLEBYDi5iECW6Y3rlqPp9jWRZRFC1990GX1yUZuhsjLDbfAL0wvWNeABeviOP4/8A8Z5ZmjLyYP/f849vmfGK2AAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Finding the Object Manager\"\n        title=\"Finding the Object Manager\"\n        src=\"/static/4a86e01a7a2e91b270186a64e5802542/913fc/salesforce-setup.png\"\n        srcset=\"/static/4a86e01a7a2e91b270186a64e5802542/bc34b/salesforce-setup.png 293w,\n/static/4a86e01a7a2e91b270186a64e5802542/da9f0/salesforce-setup.png 585w,\n/static/4a86e01a7a2e91b270186a64e5802542/913fc/salesforce-setup.png 1170w,\n/static/4a86e01a7a2e91b270186a64e5802542/efb0c/salesforce-setup.png 1755w,\n/static/4a86e01a7a2e91b270186a64e5802542/6924e/salesforce-setup.png 1922w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </span> </p>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">Locating the Object Manager</p>\n<p>Our custom objects mirror our Laravel models, so we created a custom object called “Tutoring Session” that mirrors what is called a <code class=\"language-text\">Session</code> model in our codebase and another called “Student Tutoring Session” which mirrors our internal <code class=\"language-text\">UserSession</code> model. The former stores all of the data related to the tutoring session, and the latter stores data related to each individual student involved in a tutoring session. These objects have a many-to-one relationship because multiple students can participate in one group tutoring session.</p>\n<p><span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 52.21700573813249%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAABZklEQVQoz62Sy07DMBBF/f9/wRewZg8LFhQh1NCq0PRBaeM075ffF3lioiy6ZKSr65HHZyajsPvFDh+HBIvNCdGe45jW2CclKT7niC/5lE/iox/4KF+3/bni8/sKdvewwNPrCo8vEZ7f1njfHLHaJ1h7HTj5lPvz7oIoPpMv4wuW2+BxgmiXgH2dCuRlg6IROP5wFEUJbQzaQaLuBLTWUFqjHhSEMlDGYpASQiqktUDdS1TdKP+GJdcSRZ5TsTSYQogBbdsAzsE5hzZNMGQpdJnDSgnni5wL95bcWgPGs4oeS23RCDNeElCgqipYG4B5hq6uoLp2ArrQzFpLbowBu6QF2qaGMo6g1DUAm6ahQh9t10EqRec5bC6/HsazOgAtyl5jUOOEUkr0fU9dfRMP9/nfNHP5mgl45jn6vqOuHjr/DF9AQIBcKRWAtyf0YlUrgLC3/wjmfxdj/f4MFMkGmZlu5bdk8As3agWE22oD/QAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Custom object in Salesforce\"\n        title=\"Custom object in Salesforce\"\n        src=\"/static/cb56522dbde886d6bfbdd2bf2cdb923f/913fc/student-tutoring-session-custom-object.png\"\n        srcset=\"/static/cb56522dbde886d6bfbdd2bf2cdb923f/bc34b/student-tutoring-session-custom-object.png 293w,\n/static/cb56522dbde886d6bfbdd2bf2cdb923f/da9f0/student-tutoring-session-custom-object.png 585w,\n/static/cb56522dbde886d6bfbdd2bf2cdb923f/913fc/student-tutoring-session-custom-object.png 1170w,\n/static/cb56522dbde886d6bfbdd2bf2cdb923f/efb0c/student-tutoring-session-custom-object.png 1755w,\n/static/cb56522dbde886d6bfbdd2bf2cdb923f/79b58/student-tutoring-session-custom-object.png 2340w,\n/static/cb56522dbde886d6bfbdd2bf2cdb923f/75c6b/student-tutoring-session-custom-object.png 3510w,\n/static/cb56522dbde886d6bfbdd2bf2cdb923f/e1f67/student-tutoring-session-custom-object.png 3834w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </span></p>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">Student Tutoring Session custom object requires unique external ID field so that we can perform API lookups (more on that later)</p>\n<p>Custom Tabs allow Salesforce users to view data related to custom objects. To add a new custom tab go to “Setup” and in the Quick Find box search for “Tabs”. Once you are in Tabs click “new” and select your custom object and the styling that you want to appear on your custom tab. On step 2 of 3 we chose to apply one tab visibility to all profiles but your individual needs may vary. The final step requires you to choose the packages you want the custom tab to appear on, and for us the EDA package was the only selection.</p>\n<p><span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 61.948955916473324%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAzklEQVQoz6WS0QqCMBSG9/6v0yME0VV0U3qXhNNtalvmmFPnHyhGF6YjD3wwDuMb5/wju/0Bx/MFpyBCGHFcI4bgA/86+/VJeKOw1kI8SjRth845tN3/kHucoFAlbNthrH4G/yI0YXhqAze4evQbIUkqoCqDuq7hnJu9NPXHN1eEcUyRCQGt9U/hxCRcHpkysKzAq6pgjNkuTNIMTFZDQms79BRyMJFDKoWmaRb35CUUIoetjVeCXsI05ZBSDp97LUUvYUw5cvVCv5Kwr/AN2yisiooh45MAAAAASUVORK5CYII='); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Custom tutoring session tab\"\n        title=\"Custom tutoring session tab\"\n        src=\"/static/7ecbdea0da60bf60df966a476e8b7ba8/913fc/custom-tutoring-session-tab.png\"\n        srcset=\"/static/7ecbdea0da60bf60df966a476e8b7ba8/bc34b/custom-tutoring-session-tab.png 293w,\n/static/7ecbdea0da60bf60df966a476e8b7ba8/da9f0/custom-tutoring-session-tab.png 585w,\n/static/7ecbdea0da60bf60df966a476e8b7ba8/913fc/custom-tutoring-session-tab.png 1170w,\n/static/7ecbdea0da60bf60df966a476e8b7ba8/b1204/custom-tutoring-session-tab.png 1293w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </span></p>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">Here's a look at our custom tab that presents the data from our Tutoring Session custom object</p>\n<h3>Data Security</h3>\n<p>Like most companies, we hold private data, which means that we have to be careful to only present data to the intended partner. We also have to make sure all data is encrypted and secure while in transit. We created a Connected app because (like mentioned previously) that is the component that is required in our package so that we can securely use the Salesforce API when uploading data to our partner’s Salesforce instances.</p>\n<p>To create a Connected app go to “Setup” and search “App Manager”. Once in App Manager click “New Connected App” in the top right. Your connected app API Name will need to be universally unique to all of Salesforce, so if you are building a prototype application you might not want to use the API Name you want to have for your final application, because it will never become available again even if you delete your prototype.</p>\n<p>After you create your Connected app go back to App Manager, find your app, find the arrow to the far right, and click “edit”. This is where you will <a href=\"https://help.salesforce.com/s/articleView?id=sf.connected_app_create_api_integration.htm&#x26;type=5\">enable OAuth</a>, use a digital signature, and upload a certificate. The OAuth scope needs to include <code class=\"language-text\">refresh_token</code> and <code class=\"language-text\">api</code>. This part of the process is what handles authorization so that our Laravel app can securely communicate with any Salesforce instance that has our package installed.</p>\n<p><span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 44.87767584097859%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABNklEQVQoz5WQUW/CIBSF+/9/0Rb3oD74MN2SPfik1mp0VqBSWi1wsT0LZDSN2ZbsJCeQC/fjcJPrTaMoJLi44HRiOPMS14ZgDEEbgrEO5Nre+rv+mxMMRM5BCYH8lINxhnOeQ3COqqqglMK1roGuw19Kuq5DtLUW8iLx9PyC0WiE8XiM6XQa1slkgtlsBiIKjc654KjICMBYsETQUiLbbJBut1itVlgul+Cchztt20JKCSEE9vs9DodDSN80DSInibAoV1f4eH/D63yOxWKO3W4HrXU4M8YEmAd7+777/R6S9sC4scbCGItbqbBeb5GmKbIsA2MMdV2HcXh5wHBMQ/cJiRy4KHA8foLxApraPrFP4hPERMN5PcL6hPELZakgZYn2IcVvkJ+cDJuUqqBK1X8rPvQffwHFLbkiCCF6ngAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Enable OAuth Settings\"\n        title=\"Enable OAuth Settings\"\n        src=\"/static/3553655106d0fe266e6684b85d9e4660/913fc/enable-oauth-settings.png\"\n        srcset=\"/static/3553655106d0fe266e6684b85d9e4660/bc34b/enable-oauth-settings.png 293w,\n/static/3553655106d0fe266e6684b85d9e4660/da9f0/enable-oauth-settings.png 585w,\n/static/3553655106d0fe266e6684b85d9e4660/913fc/enable-oauth-settings.png 1170w,\n/static/3553655106d0fe266e6684b85d9e4660/efb0c/enable-oauth-settings.png 1755w,\n/static/3553655106d0fe266e6684b85d9e4660/79b58/enable-oauth-settings.png 2340w,\n/static/3553655106d0fe266e6684b85d9e4660/919be/enable-oauth-settings.png 2616w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </span></p>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">This is what our OAuth settings and scope look like after we are finished editing our Connected App component</p>\n<h3>Packaging our Solution</h3>\n<p>Now that we have custom objects to store our data, custom tabs to present our data to end users, and a Connected app to give our Laravel instance API access to Salesforce we are ready to package up our solution. Go to Setup and search “Package Manager”. Click “new”, name your new package, and click “save”. While viewing your package’s detail page you can click “Add” to add your custom objects, custom tabs, and Connected app. Choose your component type (Custom Object, tab, etc) and add it to your package. Repeat this process until you have your Custom Objects, Tabs, Connected app, and anything else you might require for your custom Salesforce app.</p>\n<p>Once we have all of our desired components it is time to upload our package. While viewing your package click “upload” and give your package a version name and number. If you are in a test state or you are still going to be making changes to your package then it is imperative that you not mark your package as “Managed - Released”. You must mark your package as “Managed - Released” before you can release it on the AppExchange, but once you do that you lose a lot of abilities when it comes to editing your package, so it is ideal to put that off as long as possible.</p>\n<p><span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 31.927083333333332%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABB0lEQVQY012QT2/CMAzF+/2/0G4778Bhp8HEYRsgGkrThCbO3yZviosmmKWf8vJsWbY7MgYhJrxs9njdbPG++8buMGD3c2E+H/Qzq7+9sz9e8fZxQDdpCamv+DpLnPoB5BOsCyAfmZsh9h5xIcGQh/WRNd3f46DR3WYNpTWbQEUpBZOUSHH911LwP1rOOcKy5Cc/pAVdE6UC1kWEEDBNCpfLACIH7wPTco0YE0KImI2BtcS5ECNSysg5Y7ahNWxTVV5NiB5aazjnIISAtRZKKQzjxBv0QuDc9yAijOMI7z3T6mIMUDOtEy6lrvfS6m8NKUfknBiyhr2b1kyL1rTWwmdBravnE34B8m3M0X35NPQAAAAASUVORK5CYII='); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Upload Package\"\n        title=\"Upload Package\"\n        src=\"/static/593eb0675c2374b34ffa2cf87d736612/913fc/upload-package.png\"\n        srcset=\"/static/593eb0675c2374b34ffa2cf87d736612/bc34b/upload-package.png 293w,\n/static/593eb0675c2374b34ffa2cf87d736612/da9f0/upload-package.png 585w,\n/static/593eb0675c2374b34ffa2cf87d736612/913fc/upload-package.png 1170w,\n/static/593eb0675c2374b34ffa2cf87d736612/efb0c/upload-package.png 1755w,\n/static/593eb0675c2374b34ffa2cf87d736612/79b58/upload-package.png 2340w,\n/static/593eb0675c2374b34ffa2cf87d736612/75c6b/upload-package.png 3510w,\n/static/593eb0675c2374b34ffa2cf87d736612/4de54/upload-package.png 3840w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </span></p>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">Our package type is \"Managed(Extension)\" but yours will say \"Unmanaged\" until released to the AppExchange (Click to enlarge)</p>\n<p>Once that your package is finished you will want to create a <a href=\"https://help.salesforce.com/s/articleView?id=sf.data_sandbox_create.htm&#x26;type=5\">sandbox organization</a> to test it in. Just keep in mind you will only be able to make changes to your custom objects up until the point you make a <code class=\"language-text\">Managed - Released</code> version of your app. This is to prevent untenable schema changes from breaking your users’ Salesforce instances.</p>\n<h2>Building the integration on our end using Laravel</h2>\n<p>At Knack we use the Laravel framework to power our GraphQL API and Laravel Nova to give our Operations team an administrative interface. This section assumes you understand the basics of Laravel and REST APIs. We also use <a href=\"https://laravelactions.com/\">Laravel Actions</a>, so everytime I reference an action I will be referring to a class that uses that library’s <code class=\"language-text\">AsAction</code> trait. Side note that we will not be using the Salesforce PHP SDK or any other Salesforce PHP toolkit since this integration is not too involved.</p>\n<p>To start, we create a new model in our Laravel codebase called <code class=\"language-text\">DataDeliveryDestination</code> which holds information about our data syncing process. On this object we store the related organization, most recent sync time, a verbose mode boolean that we activate for logging, an editable cron string, and an <code class=\"language-text\">is_enabled</code> boolean that can be used to switch the integration on and off. Most importantly, the DataDeliveryDestination object stores an encrypted json array (stored in MySQL as TEXT) that contains sensitive information about the related organization’s Salesforce instance.</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">/**\n * @property int $id\n * @property string|array $configuration - encrypted configuration array for each organization\n * @property bool $is_enabled - allows us to easily disable integration\n * @property bool $verbose_mode - allows us to turn logging on and off\n * @property int $organization_id - related Organization\n * @property Carbon $last_sync - last time data sync was executed\n * @property string $connector_type - Salesforce, CSV, other assorted Student Information Systems\n * @property string $cron_string - a job checks this once an hour to decide whether to execute\n * @property Carbon $deleted_at\n * @property Carbon $created_at\n * @property Carbon $updated_at\n *\n * @mixin Eloquent\n *\n * @property-read Organization $organization\n */\nclass DataDeliveryDestination extends Model\n{\n    /**\n     * Associated organization.\n     */\n    public function organization(): BelongsTo\n    {\n        return $this-&gt;belongsTo(Organization::class, &#39;organization_id&#39;);\n    }\n\n    /**\n     * Lets Operators know which specific config key is missing.\n     */\n    public function listMissingConfigKeys(): array\n    {\n        if ($this-&gt;configuration === null) {\n            return [&#39;no config is set&#39;];\n        }\n\n        $missingKeys = [];\n\n        foreach (DataDeliveryDestinationType::REQUIRED_KEYS[$this-&gt;connector_type] as $requirement) {\n            if (!array_key_exists($requirement, $this-&gt;configuration)) {\n                $missingKeys[] = $requirement;\n            }\n        }\n\n        return $missingKeys;\n    }\n\n    /**\n     * Used to turn more verbose logging on/off.\n     */\n    public function logIfVerboseMode(string $message): void\n    {\n        if ($this-&gt;verbose_mode) {\n            Log::debug($message);\n        }\n    }\n}</code></pre></div>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">A truncated version of our DataDeliveryDestination class</p>\n<p>Since we need to perform lookups on Course and Contact objects in our partner’s Salesforce instances we need each partner to provide us with the name of their External ID fields on those objects. These typically follow the pattern <code class=\"language-text\">Banner_ID__c</code> and <code class=\"language-text\">External_Course_ID__c</code>. These field names will be required when building URLs for our API calls. It is likely that naming conventions will be different for each partner, so these two field names are stored in the partner config. We also store some environmental variables and the integration user’s email address, which is required for the integration to run. All of this information is stored in the previously mentioned encrypted JSON array.</p>\n<p>We run our “InitiateDataDeliveryCron” command every hour, and it checks the individual cron string for each of our Data Delivery Destinations. If the cron expression is due then we dispatch our asynchronous <code class=\"language-text\">PushSessionsDataToSalesforce</code> action, which will handle our data being delivered to Salesforce. We have separate actions for <code class=\"language-text\">RetrieveSalesforceBearerToken</code>, <code class=\"language-text\">MakeIndividualSessionApiCallToSalesforce</code> which pushes data to one of our custom objects in Salesforce, and <code class=\"language-text\">MakeIndividualUserSessionApiCallToSalesforce</code> which pushes data to our other custom object. We log any failed requests to the Salesforce API on a <code class=\"language-text\">DataDeliveryLog</code> object so that our Operations team can get to the bottom of any discrepancies.</p>\n<h3>API Endpoints we use</h3>\n<table>\n<thead>\n<tr>\n<th>Task</th>\n<th>Endpoint</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Retrieve bearer token</td>\n<td><code class=\"language-text\">/services/oauth2/token</code></td>\n</tr>\n<tr>\n<td>Retrieve Salesforce ID for Course</td>\n<td><code class=\"language-text\">/services/data/v53.0/sobjects/hed__Course__c/{course_column_name}/{course_id}</code></td>\n</tr>\n<tr>\n<td>Retrieve Salesforce ID for User</td>\n<td><code class=\"language-text\">/services/data/v53.0/sobjects/Contact/{contact_column_name}/{contact_id}</code></td>\n</tr>\n<tr>\n<td>Upsert Tutoring Session Record</td>\n<td><code class=\"language-text\">/services/data/v53.0/sobjects/Knack__Tutoring_Session__c/Knack__Knack_Id__c/{knack_id}</code></td>\n</tr>\n<tr>\n<td>Upsert Student Tutoring Session Record</td>\n<td><code class=\"language-text\">/services/data/v53.0/sobjects/Knack__Student_Tutoring_Session__c/Knack__Knack_Id__c/{knack_id}</code></td>\n</tr>\n</tbody>\n</table>\n<p>Let’s break down what <code class=\"language-text\">Retrieve Salesforce ID for Course</code> involves:\n<strong>/services/data/v53.0/sobjects/hed__Course__c/{course_column_name}/{course_id}</strong></p>\n<p>The API version we are using is v53.0. “sobjects” refers to the fact that we are querying an object, and <code class=\"language-text\">hed__Course__c</code> is the universally unique name for all Course objects within EDA. Our Laravel backend automatically concatenates the column name that our partners use (stored in their previously described encrypted config) for their Course objects, and the last variable is the course ID for the specific course we are querying. We use the response from this API request (and several others) to build a future POST request </p>\n<p>Let’s also have a look at <code class=\"language-text\">Upsert Tutoring Session Record</code> from the API table above:\n<strong>/services/data/v53.0/sobjects/Knack__Tutoring_Session__c/Knack__Knack_Id__c/{knack_id}</strong></p>\n<p><code class=\"language-text\">Knack__Tutoring_Session__c</code> is the universally unique name for our previously created custom object “Tutoring Session”. <code class=\"language-text\">Knack__Knack_Id__c</code> is the universally unique name for our unique identifier on the object, which is a ULID. This field’s data type is Text(36) (External ID) (Unique Case Sensitive), which means it can be used to uniquely identify a record in Salesforce. This is helpful because we want to update existing records instead of creating duplicate records every time our integration is run. </p>\n<h3>Full API Flow in Action</h3>\n<p><span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 761px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 38.23915900131406%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABK0lEQVQoz5VRX4uCcBD0+38bH30QkVAIJEIiDTK0sjLBP2mWlk3MHr8jjruHW1gmd2xndtTwz6qqCmEYYr1ew/d9LJdLrFYrdF0nvHa/33G73dD3PcqyxPl8Rp7nuF6veDweIE+O+Hq9sN/voes6ptMpDMOAaZqwLAtxHH8tVMrDMKBtW1wuFzRNI4pELuNi8lzKGd0lSYLtdivIjqJIDGlFUYgjOqvrGsfjUX6T5BKeuFgssNvtMI4jsizDZDKBbduCjuOIQ9d15XQ5mW64jE4+i3M6JHK5ioUCQRAIzmYzyXE+n2Oz2UB7Pp+SDdV/FucUUagce54nGTI3XnQ6neR8CmvMjA8cpmkqoRMPh8N3k2c0LAqrD0kh1ao0KpPkS381ebr7/ONv17Df+UdeT/3Ji8sAAAAASUVORK5CYII='); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Diagram showing Knack's implementation of the Salesforce Bearer flow\"\n        title=\"Salesforce Bearer Flow\"\n        src=\"/static/3de6c0da12acfdb9d5813131c2d7937d/5eb0e/salesforce-bearer-flow.png\"\n        srcset=\"/static/3de6c0da12acfdb9d5813131c2d7937d/bc34b/salesforce-bearer-flow.png 293w,\n/static/3de6c0da12acfdb9d5813131c2d7937d/da9f0/salesforce-bearer-flow.png 585w,\n/static/3de6c0da12acfdb9d5813131c2d7937d/5eb0e/salesforce-bearer-flow.png 761w\"\n        sizes=\"(max-width: 761px) 100vw, 761px\"\n      />\n  </span></p>\n<p><span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1148px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 56.968641114982574%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWUlEQVQoz3WSiY6CQAyGef+nM4ZEQMRwKATlVA7Pbr4mddmYbTKZmbb8xxTn8XjI7XaTeZ5lmiYZx1GSJJHr9SrU7ve7Ls5d10nTNNK2rfR9rzs5djAIh0YWASDFNE11h+T9fsvz+dSa7/uyXq91ua4rq9VKz+TLsvwGhAXmLMuUGUACQFQeDgetHY9H7RuGQZ1QJyD/AwjA6XRSy+yoskZqm81Gdrud1nFR17UCG/FHIQkUwIiyoii0kfcElDrqgyBQQFQCik2+oW6iHOSSvFwun4c2y+T5iBqEnudJGIaqjuHQg2UAzbYqtAsqaIyiSHezYpaXgJCabchwAtbXGwK03+9VKU02ZT6K41hB8jzXAbEY0FKlY/8ZgUXUbbdb3bnblCGFjGGh6nw+6yL3er1+h8LFErAwCN7NWO3HN5XkbIhLMTjR3wZrJr2qKlmG2YWQ839hNQB/AMpzTZIPuKD5AAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Diagram showing Knack's Data Delivery flow\"\n        title=\"Knack Data Delivery Flow\"\n        src=\"/static/13780e0b4d52a0f42dd6740cde895aad/00eef/salesforce-data-delivery-flow.png\"\n        srcset=\"/static/13780e0b4d52a0f42dd6740cde895aad/bc34b/salesforce-data-delivery-flow.png 293w,\n/static/13780e0b4d52a0f42dd6740cde895aad/da9f0/salesforce-data-delivery-flow.png 585w,\n/static/13780e0b4d52a0f42dd6740cde895aad/00eef/salesforce-data-delivery-flow.png 1148w\"\n        sizes=\"(max-width: 1148px) 100vw, 1148px\"\n      />\n  </span></p>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">Steps 1, 2, and 4 of the Knack-Salesforce Data Delivery flow are simply requesting the Salesforce ID of objects so that we can use those IDs to build POST requests for steps 3 and 5.</p>\n<h3>Testing Connectivity</h3>\n<p>Once the app is installed we can run a connectivity test. We just need the integration user’s email, the external ID fields for the Contact and Course objects in the target Salesforce instance, and a client ID and private key that never change. Then we go to Knack’s internal admin (Laravel Nova), click Test Connectivity, and voilà!\n<span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 57.870060281312796%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAABm0lEQVQoz5WT7W6bMBSGfQNblj8hVClgQ8EfEELadcU25GPTpK3a7v9u3umYNl20TMt+PDK2rOe89jEsSrdYJB1ojJIufC+SDeZxjfnSXMUsMpjfNIj5AxivR6TaBxLlkOoBqRmwqnrcSvtvlMMq22IZG9wWj2DF5ojMDEE2CT3y9oh8vT9b/yvao+AdyhuFvHgAu+s+QzS7UO1VeLf5DyElrHoU5UespAUT6z1S5c8SZno4FbiGVDuUetrPKA1vdlMl5ZArh0K5cDfXyGifUBa5mu4zJORmQKZcIKXx5ajUHEr8ltafJwtCH2RBqD0YWbfGwtcWQ+Nwr3uU613YSOKy+4KiPZxewaWEfwhTaZEpi1RZcGUh6jE0ioTUNLqWRF1+NtQIIXsI+YSVdGCPjcdz6/CtdRgbi1L1qDrq8iEkq+6/oqBn1B4uItojZLtH1e4h1gcwXj0hf4GXxCcIM3WZjkkyghLz+g0RGAPaeOT1CG5GsFmk8f43aD5bKHyI9InwG8b1af4u0siWEj8Tie+JxHMi8SOR0LHEL4y+Zwx691MmAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Connectivity Test\"\n        title=\"Connectivity Test\"\n        src=\"/static/68a2a8614a20b938a0ad57ec95f80441/913fc/connectivity-test-failure.png\"\n        srcset=\"/static/68a2a8614a20b938a0ad57ec95f80441/bc34b/connectivity-test-failure.png 293w,\n/static/68a2a8614a20b938a0ad57ec95f80441/da9f0/connectivity-test-failure.png 585w,\n/static/68a2a8614a20b938a0ad57ec95f80441/913fc/connectivity-test-failure.png 1170w,\n/static/68a2a8614a20b938a0ad57ec95f80441/efb0c/connectivity-test-failure.png 1755w,\n/static/68a2a8614a20b938a0ad57ec95f80441/79b58/connectivity-test-failure.png 2340w,\n/static/68a2a8614a20b938a0ad57ec95f80441/f15de/connectivity-test-failure.png 2986w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </span></p>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">Operators can run a connectivity test and receive more verbose output than \"Failure\"</p>\n<p>All we need to do is receive a bearer token and we know that we have a successful connection, so if we get a good response from <code class=\"language-text\">RetrieveSalesforceBearerToken</code> we know our ping was successful. As you can see in the code snippet below, we check for all config key/values, check the connector type, and meaningfully alert our Operations team if there are any errors:</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">class TestConnectivity\n{\n    use AsAction;\n\n    /**\n     * Execute the command.\n     */\n    public function handle(DataDeliveryDestination $dataDeliveryDestination): array|string\n    {\n        if ($dataDeliveryDestination-&gt;listMissingConfigKeys()) {\n            return Action::danger(&#39;A Configuration Key is missing or misspelled.&#39;);\n        }\n\n        if (DataDeliveryDestinationType::SALESFORCE === $dataDeliveryDestination-&gt;connector_type) {\n            $bearerTokenResponse = RetrieveSalesforceBearerToken::make()-&gt;handle($dataDeliveryDestination);\n\n            if ($bearerTokenResponse-&gt;status() === 200) {\n                return Action::message(&#39;Salesforce Connectivity Test Successful&#39;);\n            }\n\n            return Action::danger(&#39;A Configuration Value is incorrect&#39;);\n        } elseif (DataDeliveryDestinationType::CSV === $dataDeliveryDestination-&gt;connector_type) {\n            return Action::danger(&#39;Full sync not available for CSV Data Delivery&#39;);\n        }\n\n        return Action::danger(&#39;Unknown connector type&#39;);\n    }\n}</code></pre></div>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">All actions described in this blogpost use the <a href=\"https://laravelactions.com/\">Laravel Actions</a> library, which we highly recommend</p>\n<h2>Putting the Knack Salesforce app to use</h2>\n<h3>Syncing Data</h3>\n<p>Now that we know that we have a successful connection we can run a full sync or partial sync of a partner’s Tutoring Session data. Once again we rely on our implementation of Laravel Nova, this time using the Full Sync action. We select a start date and the sync upserts all “Tutor Session” and “Student Tutor Session” records to Salesforce, as long as the object’s <code class=\"language-text\">updated_at</code> value is more recent than the specified time.\n<span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 62.30529595015576%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABpUlEQVQoz6VSS1LCQBTkBCYoIhJDwh/yITGxsAgkMwmopeBnoy4sF15AyyNolXdw4V3bekOiqMDGRdebzOT16+56OWnXRIaNnS62tX00nBiaMUKlO4SWwRiKuwxqJ0C+bEEqGV/98q6FnFSigwk5fSxWfdSdGFUrgm5G0KlaTJw1M4RmhKLSMOohEdS3kSJHU7ZUB5t7PeQVG0Xdg26G2GsP0PGPUbdGqHUDtBwGs3+Cjj9B0+VQ24dffYvI5cs2ChVXfNDEcqOPljcRKqP4GOPxBAe+h6vbBzy/feDp9R2PL+9w4xvkf5GlhKTQFdOIkJRZgxmM/ilCNsZ0OsXZ7BKMRRgnCWLOETGO3tE9CponXC0h/JZOP8hpyP3DIWbnF7i6vgOLInAeg3OOIAhQqlDu9iqFP7PYIig2BkOGo0kCzhmSJEESc4EwHEGpuZAVe7Xl3w9EqjY8aM19aC0PettHtU11fldQyc2SDCm3zKrII6uUqWJDKtuQlZ6wJy1gGdkfwsUchUo1Vas6YuGzbViHlYREsog5mfN/QqGw4grCbL3W4ROt9FsWtU+4egAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Sync with Salesforce\"\n        title=\"Sync with Salesforce\"\n        src=\"/static/b343b3ad9cc49298b53cc900ef4455b1/913fc/sync-with-salesforce.png\"\n        srcset=\"/static/b343b3ad9cc49298b53cc900ef4455b1/bc34b/sync-with-salesforce.png 293w,\n/static/b343b3ad9cc49298b53cc900ef4455b1/da9f0/sync-with-salesforce.png 585w,\n/static/b343b3ad9cc49298b53cc900ef4455b1/913fc/sync-with-salesforce.png 1170w,\n/static/b343b3ad9cc49298b53cc900ef4455b1/efb0c/sync-with-salesforce.png 1755w,\n/static/b343b3ad9cc49298b53cc900ef4455b1/79b58/sync-with-salesforce.png 2340w,\n/static/b343b3ad9cc49298b53cc900ef4455b1/fe067/sync-with-salesforce.png 2568w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </span></p>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">Full and partial sync options are available</p>\n<p>After the sync is complete we can check our Salesforce instance for the data. New records should be added as Tutoring Session and Student Tutoring Session custom objects. Existing records that were updated in Knack since the most recent sync will also be updated so that all data in Salesforce matches what is in our database for an organization. We can use our custom tab to view our data in Salesforce, and depending on the cron string that is set in the partner’s Data Delivery Destination a Salesforce instance could be updated as often as every hour.\n<span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 34.075723830734965%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAAA3klEQVQoz6WO60rDQBCF9/1fqoI/JJaCRWisCEKTXmKSvWQ3e6u7R3bAglCh4sDHzJw5MwxbHw2M9eCThdQOQjvKhHEYlSGNTzN6ofExKghtwZXBrp/wdlTopCVf8bC71RMWq1dUdYvl9oDHlz2x3O6pr+qG6u95yeSpW9w/N1isd3jYtKg2DWmsad9xOrQY+g6SD1BivCD5iFkreGuuEpxBdPMPjRkbcOIaAxew1iGECB/ChdKHeDssxDOkS0gZv0bO+WZY+aDTEfEzlc0/LV896H0An890MKeE9E++AMxXGqEDcMYvAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Tutoring Session Display\"\n        title=\"Tutoring Sessions Display\"\n        src=\"/static/595ec2fb2153402c61f1b38b127f47ff/913fc/eda-tutoring-sessions-displayed.png\"\n        srcset=\"/static/595ec2fb2153402c61f1b38b127f47ff/bc34b/eda-tutoring-sessions-displayed.png 293w,\n/static/595ec2fb2153402c61f1b38b127f47ff/da9f0/eda-tutoring-sessions-displayed.png 585w,\n/static/595ec2fb2153402c61f1b38b127f47ff/913fc/eda-tutoring-sessions-displayed.png 1170w,\n/static/595ec2fb2153402c61f1b38b127f47ff/efb0c/eda-tutoring-sessions-displayed.png 1755w,\n/static/595ec2fb2153402c61f1b38b127f47ff/79b58/eda-tutoring-sessions-displayed.png 2340w,\n/static/595ec2fb2153402c61f1b38b127f47ff/7c811/eda-tutoring-sessions-displayed.png 2694w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </span></p>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">Tutoring Sessions have now been synced to Salesforce</p>\n<p><span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 61.948955916473324%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAzklEQVQoz6WS0QqCMBSG9/6v0yME0VV0U3qXhNNtalvmmFPnHyhGF6YjD3wwDuMb5/wju/0Bx/MFpyBCGHFcI4bgA/86+/VJeKOw1kI8SjRth845tN3/kHucoFAlbNthrH4G/yI0YXhqAze4evQbIUkqoCqDuq7hnJu9NPXHN1eEcUyRCQGt9U/hxCRcHpkysKzAq6pgjNkuTNIMTFZDQms79BRyMJFDKoWmaRb35CUUIoetjVeCXsI05ZBSDp97LUUvYUw5cvVCv5Kwr/AN2yisiooh45MAAAAASUVORK5CYII='); background-size: cover; display: block;\"\n    ></span>\n    <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;\"\n        alt=\"Custom tutoring session tab\"\n        title=\"Custom tutoring session tab\"\n        src=\"/static/7ecbdea0da60bf60df966a476e8b7ba8/913fc/custom-tutoring-session-tab.png\"\n        srcset=\"/static/7ecbdea0da60bf60df966a476e8b7ba8/bc34b/custom-tutoring-session-tab.png 293w,\n/static/7ecbdea0da60bf60df966a476e8b7ba8/da9f0/custom-tutoring-session-tab.png 585w,\n/static/7ecbdea0da60bf60df966a476e8b7ba8/913fc/custom-tutoring-session-tab.png 1170w,\n/static/7ecbdea0da60bf60df966a476e8b7ba8/b1204/custom-tutoring-session-tab.png 1293w\"\n        sizes=\"(max-width: 1170px) 100vw, 1170px\"\n      />\n  </span></p>\n<p style=\"text-align: center; font-size: 80%; font-style: italic\">Same image from above showing what a single Tutoring Session record looks like in Salesforce</p>\n<p>We could shorten the sync interval to every minute if we would like, but that would have to be changed in Kernel.php:</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">$schedule-&gt;command(InitiateDataDeliveryCron::class)\n            -&gt;hourly() // changing this to -&gt;everyMinute() would allow more frequent execution\n            -&gt;appendOutputTo($log)\n            -&gt;onOneServer();</code></pre></div>\n<h3>Conclusion</h3>\n<p>That pretty much sums things up. We have an eloquent <a href=\"https://appexchange.salesforce.com/appxListingDetail?listingId=a0N4V00000HZVVdUAP&#x26;tab=e\">Salesforce package</a> that allows us to store data that mirrors objects in our codebase, an integration written in Laravel, and steps for how our Operations team and partner schools can use the integration to greatly enhance academic resources for students. Now that our partners can view student tutoring data directly in their Salesforce instance, they can closely monitor students and encourage them to seek out tutoring services from their peers through Knack. 65% of students who use Knack had never used campus tutoring before, so bringing more information to administrators’ fingertips can only benefit the students of every institution that uses Knack Tutoring Technologies.</p>","htmlAst":{"type":"root","children":[{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Why does our Laravel app need a Salesforce integration?"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Knack Tutoring is a unique peer-tutoring platform used by colleges and universities across the US. Students can find tutors who attend their specific school and excelled in the exact courses they are enrolled in. Dozens of higher ed institutions rely on Knack to handle their tutoring needs, and with that comes the need to share student tutoring data with college administrators in ways that are most convenient for them."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"At Knack we partner with schools all over the US with student populations ranging from the hundreds to tens of thousands. Schools such as University of Florida and Auburn University have already found that students using Knack are 10% more likely to pass courses. When schools deploy Knack, it is important that they retain sight on how often students are using Knack tutoring services."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Many of our partners use Salesforce. Salesforce has an Education Data Architecture (EDA) extension that schools implement to allow them to securely and privately upload and share course catalogs, student enrollment data, and more. Many schools also utilize the "},{"type":"element","tagName":"a","properties":{"href":"https://appexchange.salesforce.com/appxListingDetail?listingId=a0N3A00000EJjbtUAD"},"children":[{"type":"text","value":"Student Success Hub for Higher Education (formerly Advisor Link)"}]},{"type":"text","value":" for advising. We decided to build an integration to allow us to sync tutoring session data nightly with our partners’ Salesforce instances."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 641px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 26.365054602184085%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAABKklEQVQY012Py0rDYBCF8youfAKfw6Uv4FI3Lgq6ced76ELBVTfiQlqk1EhNa1srir3EUOglf5qkzaXVGpL/k0RaxQPDgRnONzMKCxOCPivJPx4EIYZhoOs6nU4nc8/3s7lYRNyN5hj+129WShRZ2of7Q6T7ggwHSK+HdF+he8HUNCjelrgrl8nn8xlwtS6nmmyctdm67JFTxyntB0jtBFk+gLRKe1DcRVaO4WYHR6+gPTZpNOoUCgWGw+H6mqPKhM1zne3rAadtf91XkoUN8xHy00G6b0jvHenpyDANx/xX+vI8DHA9n6u2oG9NST5CgiBguVyimMJmbDmYls14MsNyPMzJjJGwcaczkiQhjuPMoyii1WrxoGlUNY3uc5OnRp1qrYaqqggh+AZZzmtvvY5vgAAAAABJRU5ErkJggg=='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Tutoring Session data synced nightly","title":"Tutoring Session data synced nightly","src":"/static/cd942f6c86afaff75f07eb3289a808d2/31fd3/knack-arrow-salesforce.png","srcSet":["/static/cd942f6c86afaff75f07eb3289a808d2/bc34b/knack-arrow-salesforce.png 293w","/static/cd942f6c86afaff75f07eb3289a808d2/da9f0/knack-arrow-salesforce.png 585w","/static/cd942f6c86afaff75f07eb3289a808d2/31fd3/knack-arrow-salesforce.png 641w"],"sizes":["(max-width:","641px)","100vw,","641px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Our Laravel backend integrates with our "},{"type":"element","tagName":"a","properties":{"href":"https://appexchange.salesforce.com/appxListingDetail?listingId=a0N4V00000HZVVdUAP&tab=e"},"children":[{"type":"text","value":"Salesforce package"}]},{"type":"text","value":" to transfer tutoring session data from the Knack database to our partners’ Salesforce instances. We use several Salesforce API endpoints to GET and POST data to our partners’ instance. This way their administrators are able to access Knack tutoring data directly from the Salesforce instance instead of needing to log into Knack specifically to view tutoring data.  Among other things, this integration empowers administrators to encourage specific students to seek out tutoring and allows them to see which courses at their institution require the most student tutoring."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Building our Salesforce Package"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The different types of Salesforce packages can be read about "},{"type":"element","tagName":"a","properties":{"href":"https://help.salesforce.com/s/articleView?id=sf.c360_a_packaging_in_customer_360_audiences.htm&type=5"},"children":[{"type":"text","value":"here"}]},{"type":"text","value":", but for brevity I am just going to share that we decided to use "},{"type":"element","tagName":"a","properties":{"href":"https://www.salesforce.com/products/platform/best-practices/declarative-programming-vs-imperative-programming/"},"children":[{"type":"text","value":"Clicks Not Code"}]},{"type":"text","value":" to create a managed package containing a Connected app as one of its components. A Connected app is the type of Salesforce component that allows an external application (Knack) to securely integrate with a Salesforce instance via APIs. It is important to note that our entire solution is called a package, and one of many components in our package is the Connected app which is used for authentication when using an API. This led to confusion for us early on because the "},{"type":"element","tagName":"a","properties":{"href":"https://appexchange.salesforce.com/"},"children":[{"type":"element","tagName":"em","properties":{},"children":[{"type":"text","value":"AppExchange"}]}]},{"type":"text","value":" is used to download "},{"type":"element","tagName":"em","properties":{},"children":[{"type":"text","value":"packages"}]},{"type":"text","value":", which can "},{"type":"element","tagName":"em","properties":{},"children":[{"type":"text","value":"optionally contain a component called a Connected app"}]},{"type":"text","value":"."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Selecting the Right Components"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Our Salesforce package is made up of Custom Objects, their related Fields, a Tab, Page Layouts, and the previously described Connected app, which are some of the many types of components that can be present in a Salesforce package. It is also a hard requirement that anyone installing the Knack Salesforce package already has the previously described Education Data Architecture extension installed on their Salesforce instance."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 61.17804551539492%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAACEUlEQVQoz21S2W7bMBDU//9Zn5LUceLGtqz7oESKui+SU+zKTVGgAgZa7jEcDulprSGEgJQKQRgjSTI8HiG+rnfc/QBRnOJ297nWdgNKUSNNc2RZgVoq1LJBXSvOq6aF13UdqrqGVApCVBBVhX4YMIwjhmHkmFAKgbwokOU5giDEIwg4F8Ux4iThGs17fd+j61pEYYjX1xdcLp+Qssa6LljXFdu2Yp5nfHyccTqdUFWCa9YaOGehlMT1+gXdNDBmh0dDDg55keP9/I4wDmGswW72fzBOI6Z5gm41hmngHPVRvmkbru12h9dPC5rF4uevO368nHAJYrSrg5oNo6H/tKPQI1LVQY4b9Go5pxeLTPW4PGI8MsG9HskHLPI8RVnkEGWOssyhGwVrdsAZGLMhyxJkaQIhCkzTAOcMzw1Dh+Bxh6wFr71lWbiYJBF8/8bERZHBmBXW7rB2Y0IiWZYRXac5pppzO8ckQMqKc97CCoHz+R1lWXBMZjPs8Sezb7crw/fv0LqhLkbfd7hdv56z7q/CNI34OH+OSMoO7DD7CiFyBimZ/6NQN5LXT4WOr/7t9QXLMgHO8rM4nsaxQRJHrE6pmr2lGnk2jj2iKICSNc89CS379/l5xrYdig//djh7EJJ68pkU0Uc2fF9K4LNKmvsmTNMEaRpzfJA9FdqDnJTM04i+bzHPIw8TKKbNSCH1/gZXapRVas0XLAAAAABJRU5ErkJggg=='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Truncated list of components","title":"Truncated list of components","src":"/static/c4b5cf1197fdccc0738335898e3e1834/913fc/truncated-component-list.png","srcSet":["/static/c4b5cf1197fdccc0738335898e3e1834/bc34b/truncated-component-list.png 293w","/static/c4b5cf1197fdccc0738335898e3e1834/da9f0/truncated-component-list.png 585w","/static/c4b5cf1197fdccc0738335898e3e1834/913fc/truncated-component-list.png 1170w","/static/c4b5cf1197fdccc0738335898e3e1834/83927/truncated-component-list.png 1494w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"A view of some of the custom components that are in our Salesforce package"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Custom Objects are created and updated using the Object Manager, which can be found by clicking the gear in the top right corner of your Salesforce instance, clicking “Setup”, and searching “Object Manager” in the Quick Find search bar on the far left of your screen. Once you are in Object Manager click the “Create” button and then Custom Object."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 51.97710718002081%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAACDUlEQVQoz3WQa0/TABSG+4/54o/wg8IHEyMmRlBjTLxhINNkgDgYEhAXYGzrrl0v67qu13Vt1659zBaJIeBJ3pxzkvc85+QIjY6MPTKROn2aYo/BcIxmOGgjF9lwUA17mWXDpT9yUQcmSkdG66mo/QF6t8+41SbSJCRJQwiULkxsfleaPPl0zMGpyN6JyP6FxGFVZq+qcdYaclJTOKsrlK41jhoGB5c9SlddipUmtY6CYTrsnLQRLNeFLKGuemyU6nxrW1ypFueyyW5D53O5wpfCFi2pjznUKV9X2dxY4+X2O3Yua3y46LP+/ZDnhRc8KxwieL5PFE/pGz71KxHbC6idl6keFQinU8q726yuPWC/+BrxeIvSx0esP1zh6ZvHlE7fI178YOvtKq82V/j6s4jgjHRcXUGUxqijCbMoRFYHaLpBHM+wbJ8gjhnbJoo2IAo8nEnIcOwQ+CZjQ8W3RjBPOROHCG5XZDaQGHZkKucirYaM0h7QbemIioOouLQ1j54e0F1qsuxbqrfsm9qEljahq/v8Ek0Ey7YJpgFBOMN1XeZpSp7nsBD5sr6t7FbNTSYnihMEz/OIoxAviPGjOYvIspzs71CapjiOQ5Ik98D/+ZJ5xtiLEBYDi5iECW6Y3rlqPp9jWRZRFC1990GX1yUZuhsjLDbfAL0wvWNeABeviOP4/8A8Z5ZmjLyYP/f849vmfGK2AAAAAElFTkSuQmCC'); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Finding the Object Manager","title":"Finding the Object Manager","src":"/static/4a86e01a7a2e91b270186a64e5802542/913fc/salesforce-setup.png","srcSet":["/static/4a86e01a7a2e91b270186a64e5802542/bc34b/salesforce-setup.png 293w","/static/4a86e01a7a2e91b270186a64e5802542/da9f0/salesforce-setup.png 585w","/static/4a86e01a7a2e91b270186a64e5802542/913fc/salesforce-setup.png 1170w","/static/4a86e01a7a2e91b270186a64e5802542/efb0c/salesforce-setup.png 1755w","/static/4a86e01a7a2e91b270186a64e5802542/6924e/salesforce-setup.png 1922w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]},{"type":"text","value":" "}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"Locating the Object Manager"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Our custom objects mirror our Laravel models, so we created a custom object called “Tutoring Session” that mirrors what is called a "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"Session"}]},{"type":"text","value":" model in our codebase and another called “Student Tutoring Session” which mirrors our internal "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"UserSession"}]},{"type":"text","value":" model. The former stores all of the data related to the tutoring session, and the latter stores data related to each individual student involved in a tutoring session. These objects have a many-to-one relationship because multiple students can participate in one group tutoring session."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 52.21700573813249%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAABZklEQVQoz62Sy07DMBBF/f9/wRewZg8LFhQh1NCq0PRBaeM075ffF3lioiy6ZKSr65HHZyajsPvFDh+HBIvNCdGe45jW2CclKT7niC/5lE/iox/4KF+3/bni8/sKdvewwNPrCo8vEZ7f1njfHLHaJ1h7HTj5lPvz7oIoPpMv4wuW2+BxgmiXgH2dCuRlg6IROP5wFEUJbQzaQaLuBLTWUFqjHhSEMlDGYpASQiqktUDdS1TdKP+GJdcSRZ5TsTSYQogBbdsAzsE5hzZNMGQpdJnDSgnni5wL95bcWgPGs4oeS23RCDNeElCgqipYG4B5hq6uoLp2ArrQzFpLbowBu6QF2qaGMo6g1DUAm6ahQh9t10EqRec5bC6/HsazOgAtyl5jUOOEUkr0fU9dfRMP9/nfNHP5mgl45jn6vqOuHjr/DF9AQIBcKRWAtyf0YlUrgLC3/wjmfxdj/f4MFMkGmZlu5bdk8As3agWE22oD/QAAAABJRU5ErkJggg=='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Custom object in Salesforce","title":"Custom object in Salesforce","src":"/static/cb56522dbde886d6bfbdd2bf2cdb923f/913fc/student-tutoring-session-custom-object.png","srcSet":["/static/cb56522dbde886d6bfbdd2bf2cdb923f/bc34b/student-tutoring-session-custom-object.png 293w","/static/cb56522dbde886d6bfbdd2bf2cdb923f/da9f0/student-tutoring-session-custom-object.png 585w","/static/cb56522dbde886d6bfbdd2bf2cdb923f/913fc/student-tutoring-session-custom-object.png 1170w","/static/cb56522dbde886d6bfbdd2bf2cdb923f/efb0c/student-tutoring-session-custom-object.png 1755w","/static/cb56522dbde886d6bfbdd2bf2cdb923f/79b58/student-tutoring-session-custom-object.png 2340w","/static/cb56522dbde886d6bfbdd2bf2cdb923f/75c6b/student-tutoring-session-custom-object.png 3510w","/static/cb56522dbde886d6bfbdd2bf2cdb923f/e1f67/student-tutoring-session-custom-object.png 3834w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"Student Tutoring Session custom object requires unique external ID field so that we can perform API lookups (more on that later)"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Custom Tabs allow Salesforce users to view data related to custom objects. To add a new custom tab go to “Setup” and in the Quick Find box search for “Tabs”. Once you are in Tabs click “new” and select your custom object and the styling that you want to appear on your custom tab. On step 2 of 3 we chose to apply one tab visibility to all profiles but your individual needs may vary. The final step requires you to choose the packages you want the custom tab to appear on, and for us the EDA package was the only selection."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 61.948955916473324%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAzklEQVQoz6WS0QqCMBSG9/6v0yME0VV0U3qXhNNtalvmmFPnHyhGF6YjD3wwDuMb5/wju/0Bx/MFpyBCGHFcI4bgA/86+/VJeKOw1kI8SjRth845tN3/kHucoFAlbNthrH4G/yI0YXhqAze4evQbIUkqoCqDuq7hnJu9NPXHN1eEcUyRCQGt9U/hxCRcHpkysKzAq6pgjNkuTNIMTFZDQms79BRyMJFDKoWmaRb35CUUIoetjVeCXsI05ZBSDp97LUUvYUw5cvVCv5Kwr/AN2yisiooh45MAAAAASUVORK5CYII='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Custom tutoring session tab","title":"Custom tutoring session tab","src":"/static/7ecbdea0da60bf60df966a476e8b7ba8/913fc/custom-tutoring-session-tab.png","srcSet":["/static/7ecbdea0da60bf60df966a476e8b7ba8/bc34b/custom-tutoring-session-tab.png 293w","/static/7ecbdea0da60bf60df966a476e8b7ba8/da9f0/custom-tutoring-session-tab.png 585w","/static/7ecbdea0da60bf60df966a476e8b7ba8/913fc/custom-tutoring-session-tab.png 1170w","/static/7ecbdea0da60bf60df966a476e8b7ba8/b1204/custom-tutoring-session-tab.png 1293w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"Here's a look at our custom tab that presents the data from our Tutoring Session custom object"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Data Security"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Like most companies, we hold private data, which means that we have to be careful to only present data to the intended partner. We also have to make sure all data is encrypted and secure while in transit. We created a Connected app because (like mentioned previously) that is the component that is required in our package so that we can securely use the Salesforce API when uploading data to our partner’s Salesforce instances."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"To create a Connected app go to “Setup” and search “App Manager”. Once in App Manager click “New Connected App” in the top right. Your connected app API Name will need to be universally unique to all of Salesforce, so if you are building a prototype application you might not want to use the API Name you want to have for your final application, because it will never become available again even if you delete your prototype."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"After you create your Connected app go back to App Manager, find your app, find the arrow to the far right, and click “edit”. This is where you will "},{"type":"element","tagName":"a","properties":{"href":"https://help.salesforce.com/s/articleView?id=sf.connected_app_create_api_integration.htm&type=5"},"children":[{"type":"text","value":"enable OAuth"}]},{"type":"text","value":", use a digital signature, and upload a certificate. The OAuth scope needs to include "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"refresh_token"}]},{"type":"text","value":" and "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"api"}]},{"type":"text","value":". This part of the process is what handles authorization so that our Laravel app can securely communicate with any Salesforce instance that has our package installed."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 44.87767584097859%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABNklEQVQoz5WQUW/CIBSF+/9/0Rb3oD74MN2SPfik1mp0VqBSWi1wsT0LZDSN2ZbsJCeQC/fjcJPrTaMoJLi44HRiOPMS14ZgDEEbgrEO5Nre+rv+mxMMRM5BCYH8lINxhnOeQ3COqqqglMK1roGuw19Kuq5DtLUW8iLx9PyC0WiE8XiM6XQa1slkgtlsBiIKjc654KjICMBYsETQUiLbbJBut1itVlgul+Cchztt20JKCSEE9vs9DodDSN80DSInibAoV1f4eH/D63yOxWKO3W4HrXU4M8YEmAd7+777/R6S9sC4scbCGItbqbBeb5GmKbIsA2MMdV2HcXh5wHBMQ/cJiRy4KHA8foLxApraPrFP4hPERMN5PcL6hPELZakgZYn2IcVvkJ+cDJuUqqBK1X8rPvQffwHFLbkiCCF6ngAAAABJRU5ErkJggg=='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Enable OAuth Settings","title":"Enable OAuth Settings","src":"/static/3553655106d0fe266e6684b85d9e4660/913fc/enable-oauth-settings.png","srcSet":["/static/3553655106d0fe266e6684b85d9e4660/bc34b/enable-oauth-settings.png 293w","/static/3553655106d0fe266e6684b85d9e4660/da9f0/enable-oauth-settings.png 585w","/static/3553655106d0fe266e6684b85d9e4660/913fc/enable-oauth-settings.png 1170w","/static/3553655106d0fe266e6684b85d9e4660/efb0c/enable-oauth-settings.png 1755w","/static/3553655106d0fe266e6684b85d9e4660/79b58/enable-oauth-settings.png 2340w","/static/3553655106d0fe266e6684b85d9e4660/919be/enable-oauth-settings.png 2616w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"This is what our OAuth settings and scope look like after we are finished editing our Connected App component"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Packaging our Solution"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Now that we have custom objects to store our data, custom tabs to present our data to end users, and a Connected app to give our Laravel instance API access to Salesforce we are ready to package up our solution. Go to Setup and search “Package Manager”. Click “new”, name your new package, and click “save”. While viewing your package’s detail page you can click “Add” to add your custom objects, custom tabs, and Connected app. Choose your component type (Custom Object, tab, etc) and add it to your package. Repeat this process until you have your Custom Objects, Tabs, Connected app, and anything else you might require for your custom Salesforce app."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Once we have all of our desired components it is time to upload our package. While viewing your package click “upload” and give your package a version name and number. If you are in a test state or you are still going to be making changes to your package then it is imperative that you not mark your package as “Managed - Released”. You must mark your package as “Managed - Released” before you can release it on the AppExchange, but once you do that you lose a lot of abilities when it comes to editing your package, so it is ideal to put that off as long as possible."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 31.927083333333332%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABB0lEQVQY012QT2/CMAzF+/2/0G4778Bhp8HEYRsgGkrThCbO3yZviosmmKWf8vJsWbY7MgYhJrxs9njdbPG++8buMGD3c2E+H/Qzq7+9sz9e8fZxQDdpCamv+DpLnPoB5BOsCyAfmZsh9h5xIcGQh/WRNd3f46DR3WYNpTWbQEUpBZOUSHH911LwP1rOOcKy5Cc/pAVdE6UC1kWEEDBNCpfLACIH7wPTco0YE0KImI2BtcS5ECNSysg5Y7ahNWxTVV5NiB5aazjnIISAtRZKKQzjxBv0QuDc9yAijOMI7z3T6mIMUDOtEy6lrvfS6m8NKUfknBiyhr2b1kyL1rTWwmdBravnE34B8m3M0X35NPQAAAAASUVORK5CYII='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Upload Package","title":"Upload Package","src":"/static/593eb0675c2374b34ffa2cf87d736612/913fc/upload-package.png","srcSet":["/static/593eb0675c2374b34ffa2cf87d736612/bc34b/upload-package.png 293w","/static/593eb0675c2374b34ffa2cf87d736612/da9f0/upload-package.png 585w","/static/593eb0675c2374b34ffa2cf87d736612/913fc/upload-package.png 1170w","/static/593eb0675c2374b34ffa2cf87d736612/efb0c/upload-package.png 1755w","/static/593eb0675c2374b34ffa2cf87d736612/79b58/upload-package.png 2340w","/static/593eb0675c2374b34ffa2cf87d736612/75c6b/upload-package.png 3510w","/static/593eb0675c2374b34ffa2cf87d736612/4de54/upload-package.png 3840w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"Our package type is \"Managed(Extension)\" but yours will say \"Unmanaged\" until released to the AppExchange (Click to enlarge)"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Once that your package is finished you will want to create a "},{"type":"element","tagName":"a","properties":{"href":"https://help.salesforce.com/s/articleView?id=sf.data_sandbox_create.htm&type=5"},"children":[{"type":"text","value":"sandbox organization"}]},{"type":"text","value":" to test it in. Just keep in mind you will only be able to make changes to your custom objects up until the point you make a "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"Managed - Released"}]},{"type":"text","value":" version of your app. This is to prevent untenable schema changes from breaking your users’ Salesforce instances."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Building the integration on our end using Laravel"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"At Knack we use the Laravel framework to power our GraphQL API and Laravel Nova to give our Operations team an administrative interface. This section assumes you understand the basics of Laravel and REST APIs. We also use "},{"type":"element","tagName":"a","properties":{"href":"https://laravelactions.com/"},"children":[{"type":"text","value":"Laravel Actions"}]},{"type":"text","value":", so everytime I reference an action I will be referring to a class that uses that library’s "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"AsAction"}]},{"type":"text","value":" trait. Side note that we will not be using the Salesforce PHP SDK or any other Salesforce PHP toolkit since this integration is not too involved."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"To start, we create a new model in our Laravel codebase called "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"DataDeliveryDestination"}]},{"type":"text","value":" which holds information about our data syncing process. On this object we store the related organization, most recent sync time, a verbose mode boolean that we activate for logging, an editable cron string, and an "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"is_enabled"}]},{"type":"text","value":" boolean that can be used to switch the integration on and off. Most importantly, the DataDeliveryDestination object stores an encrypted json array (stored in MySQL as TEXT) that contains sensitive information about the related organization’s Salesforce instance."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"text"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-text"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"/**\n * @property int $id\n * @property string|array $configuration - encrypted configuration array for each organization\n * @property bool $is_enabled - allows us to easily disable integration\n * @property bool $verbose_mode - allows us to turn logging on and off\n * @property int $organization_id - related Organization\n * @property Carbon $last_sync - last time data sync was executed\n * @property string $connector_type - Salesforce, CSV, other assorted Student Information Systems\n * @property string $cron_string - a job checks this once an hour to decide whether to execute\n * @property Carbon $deleted_at\n * @property Carbon $created_at\n * @property Carbon $updated_at\n *\n * @mixin Eloquent\n *\n * @property-read Organization $organization\n */\nclass DataDeliveryDestination extends Model\n{\n    /**\n     * Associated organization.\n     */\n    public function organization(): BelongsTo\n    {\n        return $this->belongsTo(Organization::class, 'organization_id');\n    }\n\n    /**\n     * Lets Operators know which specific config key is missing.\n     */\n    public function listMissingConfigKeys(): array\n    {\n        if ($this->configuration === null) {\n            return ['no config is set'];\n        }\n\n        $missingKeys = [];\n\n        foreach (DataDeliveryDestinationType::REQUIRED_KEYS[$this->connector_type] as $requirement) {\n            if (!array_key_exists($requirement, $this->configuration)) {\n                $missingKeys[] = $requirement;\n            }\n        }\n\n        return $missingKeys;\n    }\n\n    /**\n     * Used to turn more verbose logging on/off.\n     */\n    public function logIfVerboseMode(string $message): void\n    {\n        if ($this->verbose_mode) {\n            Log::debug($message);\n        }\n    }\n}"}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"A truncated version of our DataDeliveryDestination class"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Since we need to perform lookups on Course and Contact objects in our partner’s Salesforce instances we need each partner to provide us with the name of their External ID fields on those objects. These typically follow the pattern "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"Banner_ID__c"}]},{"type":"text","value":" and "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"External_Course_ID__c"}]},{"type":"text","value":". These field names will be required when building URLs for our API calls. It is likely that naming conventions will be different for each partner, so these two field names are stored in the partner config. We also store some environmental variables and the integration user’s email address, which is required for the integration to run. All of this information is stored in the previously mentioned encrypted JSON array."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"We run our “InitiateDataDeliveryCron” command every hour, and it checks the individual cron string for each of our Data Delivery Destinations. If the cron expression is due then we dispatch our asynchronous "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"PushSessionsDataToSalesforce"}]},{"type":"text","value":" action, which will handle our data being delivered to Salesforce. We have separate actions for "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"RetrieveSalesforceBearerToken"}]},{"type":"text","value":", "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"MakeIndividualSessionApiCallToSalesforce"}]},{"type":"text","value":" which pushes data to one of our custom objects in Salesforce, and "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"MakeIndividualUserSessionApiCallToSalesforce"}]},{"type":"text","value":" which pushes data to our other custom object. We log any failed requests to the Salesforce API on a "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"DataDeliveryLog"}]},{"type":"text","value":" object so that our Operations team can get to the bottom of any discrepancies."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"API Endpoints we use"}]},{"type":"text","value":"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"},{"type":"element","tagName":"table","properties":{},"children":[{"type":"element","tagName":"thead","properties":{},"children":[{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"th","properties":{},"children":[{"type":"text","value":"Task"}]},{"type":"element","tagName":"th","properties":{},"children":[{"type":"text","value":"Endpoint"}]}]}]},{"type":"element","tagName":"tbody","properties":{},"children":[{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"td","properties":{},"children":[{"type":"text","value":"Retrieve bearer token"}]},{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"/services/oauth2/token"}]}]}]},{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"td","properties":{},"children":[{"type":"text","value":"Retrieve Salesforce ID for Course"}]},{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"/services/data/v53.0/sobjects/hed__Course__c/{course_column_name}/{course_id}"}]}]}]},{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"td","properties":{},"children":[{"type":"text","value":"Retrieve Salesforce ID for User"}]},{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"/services/data/v53.0/sobjects/Contact/{contact_column_name}/{contact_id}"}]}]}]},{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"td","properties":{},"children":[{"type":"text","value":"Upsert Tutoring Session Record"}]},{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"/services/data/v53.0/sobjects/Knack__Tutoring_Session__c/Knack__Knack_Id__c/{knack_id}"}]}]}]},{"type":"element","tagName":"tr","properties":{},"children":[{"type":"element","tagName":"td","properties":{},"children":[{"type":"text","value":"Upsert Student Tutoring Session Record"}]},{"type":"element","tagName":"td","properties":{},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"/services/data/v53.0/sobjects/Knack__Student_Tutoring_Session__c/Knack__Knack_Id__c/{knack_id}"}]}]}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Let’s break down what "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"Retrieve Salesforce ID for Course"}]},{"type":"text","value":" involves:\n"},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"/services/data/v53.0/sobjects/hed__Course__c/{course_column_name}/{course_id}"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"The API version we are using is v53.0. “sobjects” refers to the fact that we are querying an object, and "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"hed__Course__c"}]},{"type":"text","value":" is the universally unique name for all Course objects within EDA. Our Laravel backend automatically concatenates the column name that our partners use (stored in their previously described encrypted config) for their Course objects, and the last variable is the course ID for the specific course we are querying. We use the response from this API request (and several others) to build a future POST request "}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Let’s also have a look at "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"Upsert Tutoring Session Record"}]},{"type":"text","value":" from the API table above:\n"},{"type":"element","tagName":"strong","properties":{},"children":[{"type":"text","value":"/services/data/v53.0/sobjects/Knack__Tutoring_Session__c/Knack__Knack_Id__c/{knack_id}"}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"Knack__Tutoring_Session__c"}]},{"type":"text","value":" is the universally unique name for our previously created custom object “Tutoring Session”. "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"Knack__Knack_Id__c"}]},{"type":"text","value":" is the universally unique name for our unique identifier on the object, which is a ULID. This field’s data type is Text(36) (External ID) (Unique Case Sensitive), which means it can be used to uniquely identify a record in Salesforce. This is helpful because we want to update existing records instead of creating duplicate records every time our integration is run. "}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Full API Flow in Action"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 761px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 38.23915900131406%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABK0lEQVQoz5VRX4uCcBD0+38bH30QkVAIJEIiDTK0sjLBP2mWlk3MHr8jjruHW1gmd2xndtTwz6qqCmEYYr1ew/d9LJdLrFYrdF0nvHa/33G73dD3PcqyxPl8Rp7nuF6veDweIE+O+Hq9sN/voes6ptMpDMOAaZqwLAtxHH8tVMrDMKBtW1wuFzRNI4pELuNi8lzKGd0lSYLtdivIjqJIDGlFUYgjOqvrGsfjUX6T5BKeuFgssNvtMI4jsizDZDKBbduCjuOIQ9d15XQ5mW64jE4+i3M6JHK5ioUCQRAIzmYzyXE+n2Oz2UB7Pp+SDdV/FucUUagce54nGTI3XnQ6neR8CmvMjA8cpmkqoRMPh8N3k2c0LAqrD0kh1ao0KpPkS381ebr7/ONv17Df+UdeT/3Ji8sAAAAASUVORK5CYII='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Diagram showing Knack's implementation of the Salesforce Bearer flow","title":"Salesforce Bearer Flow","src":"/static/3de6c0da12acfdb9d5813131c2d7937d/5eb0e/salesforce-bearer-flow.png","srcSet":["/static/3de6c0da12acfdb9d5813131c2d7937d/bc34b/salesforce-bearer-flow.png 293w","/static/3de6c0da12acfdb9d5813131c2d7937d/da9f0/salesforce-bearer-flow.png 585w","/static/3de6c0da12acfdb9d5813131c2d7937d/5eb0e/salesforce-bearer-flow.png 761w"],"sizes":["(max-width:","761px)","100vw,","761px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1148px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 56.968641114982574%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWUlEQVQoz3WSiY6CQAyGef+nM4ZEQMRwKATlVA7Pbr4mddmYbTKZmbb8xxTn8XjI7XaTeZ5lmiYZx1GSJJHr9SrU7ve7Ls5d10nTNNK2rfR9rzs5djAIh0YWASDFNE11h+T9fsvz+dSa7/uyXq91ua4rq9VKz+TLsvwGhAXmLMuUGUACQFQeDgetHY9H7RuGQZ1QJyD/AwjA6XRSy+yoskZqm81Gdrud1nFR17UCG/FHIQkUwIiyoii0kfcElDrqgyBQQFQCik2+oW6iHOSSvFwun4c2y+T5iBqEnudJGIaqjuHQg2UAzbYqtAsqaIyiSHezYpaXgJCabchwAtbXGwK03+9VKU02ZT6K41hB8jzXAbEY0FKlY/8ZgUXUbbdb3bnblCGFjGGh6nw+6yL3er1+h8LFErAwCN7NWO3HN5XkbIhLMTjR3wZrJr2qKlmG2YWQ839hNQB/AMpzTZIPuKD5AAAAAElFTkSuQmCC'); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Diagram showing Knack's Data Delivery flow","title":"Knack Data Delivery Flow","src":"/static/13780e0b4d52a0f42dd6740cde895aad/00eef/salesforce-data-delivery-flow.png","srcSet":["/static/13780e0b4d52a0f42dd6740cde895aad/bc34b/salesforce-data-delivery-flow.png 293w","/static/13780e0b4d52a0f42dd6740cde895aad/da9f0/salesforce-data-delivery-flow.png 585w","/static/13780e0b4d52a0f42dd6740cde895aad/00eef/salesforce-data-delivery-flow.png 1148w"],"sizes":["(max-width:","1148px)","100vw,","1148px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"Steps 1, 2, and 4 of the Knack-Salesforce Data Delivery flow are simply requesting the Salesforce ID of objects so that we can use those IDs to build POST requests for steps 3 and 5."}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Testing Connectivity"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Once the app is installed we can run a connectivity test. We just need the integration user’s email, the external ID fields for the Contact and Course objects in the target Salesforce instance, and a client ID and private key that never change. Then we go to Knack’s internal admin (Laravel Nova), click Test Connectivity, and voilà!\n"},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 57.870060281312796%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAABm0lEQVQoz5WT7W6bMBSGfQNblj8hVClgQ8EfEELadcU25GPTpK3a7v9u3umYNl20TMt+PDK2rOe89jEsSrdYJB1ojJIufC+SDeZxjfnSXMUsMpjfNIj5AxivR6TaBxLlkOoBqRmwqnrcSvtvlMMq22IZG9wWj2DF5ojMDEE2CT3y9oh8vT9b/yvao+AdyhuFvHgAu+s+QzS7UO1VeLf5DyElrHoU5UespAUT6z1S5c8SZno4FbiGVDuUetrPKA1vdlMl5ZArh0K5cDfXyGifUBa5mu4zJORmQKZcIKXx5ajUHEr8ltafJwtCH2RBqD0YWbfGwtcWQ+Nwr3uU613YSOKy+4KiPZxewaWEfwhTaZEpi1RZcGUh6jE0ioTUNLqWRF1+NtQIIXsI+YSVdGCPjcdz6/CtdRgbi1L1qDrq8iEkq+6/oqBn1B4uItojZLtH1e4h1gcwXj0hf4GXxCcIM3WZjkkyghLz+g0RGAPaeOT1CG5GsFmk8f43aD5bKHyI9InwG8b1af4u0siWEj8Tie+JxHMi8SOR0LHEL4y+Zwx691MmAAAAAElFTkSuQmCC'); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Connectivity Test","title":"Connectivity Test","src":"/static/68a2a8614a20b938a0ad57ec95f80441/913fc/connectivity-test-failure.png","srcSet":["/static/68a2a8614a20b938a0ad57ec95f80441/bc34b/connectivity-test-failure.png 293w","/static/68a2a8614a20b938a0ad57ec95f80441/da9f0/connectivity-test-failure.png 585w","/static/68a2a8614a20b938a0ad57ec95f80441/913fc/connectivity-test-failure.png 1170w","/static/68a2a8614a20b938a0ad57ec95f80441/efb0c/connectivity-test-failure.png 1755w","/static/68a2a8614a20b938a0ad57ec95f80441/79b58/connectivity-test-failure.png 2340w","/static/68a2a8614a20b938a0ad57ec95f80441/f15de/connectivity-test-failure.png 2986w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"Operators can run a connectivity test and receive more verbose output than \"Failure\""}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"All we need to do is receive a bearer token and we know that we have a successful connection, so if we get a good response from "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"RetrieveSalesforceBearerToken"}]},{"type":"text","value":" we know our ping was successful. As you can see in the code snippet below, we check for all config key/values, check the connector type, and meaningfully alert our Operations team if there are any errors:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"text"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-text"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"class TestConnectivity\n{\n    use AsAction;\n\n    /**\n     * Execute the command.\n     */\n    public function handle(DataDeliveryDestination $dataDeliveryDestination): array|string\n    {\n        if ($dataDeliveryDestination->listMissingConfigKeys()) {\n            return Action::danger('A Configuration Key is missing or misspelled.');\n        }\n\n        if (DataDeliveryDestinationType::SALESFORCE === $dataDeliveryDestination->connector_type) {\n            $bearerTokenResponse = RetrieveSalesforceBearerToken::make()->handle($dataDeliveryDestination);\n\n            if ($bearerTokenResponse->status() === 200) {\n                return Action::message('Salesforce Connectivity Test Successful');\n            }\n\n            return Action::danger('A Configuration Value is incorrect');\n        } elseif (DataDeliveryDestinationType::CSV === $dataDeliveryDestination->connector_type) {\n            return Action::danger('Full sync not available for CSV Data Delivery');\n        }\n\n        return Action::danger('Unknown connector type');\n    }\n}"}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"All actions described in this blogpost use the "},{"type":"element","tagName":"a","properties":{"href":"https://laravelactions.com/"},"children":[{"type":"text","value":"Laravel Actions"}]},{"type":"text","value":" library, which we highly recommend"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h2","properties":{},"children":[{"type":"text","value":"Putting the Knack Salesforce app to use"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Syncing Data"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"Now that we know that we have a successful connection we can run a full sync or partial sync of a partner’s Tutoring Session data. Once again we rely on our implementation of Laravel Nova, this time using the Full Sync action. We select a start date and the sync upserts all “Tutor Session” and “Student Tutor Session” records to Salesforce, as long as the object’s "},{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"updated_at"}]},{"type":"text","value":" value is more recent than the specified time.\n"},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 62.30529595015576%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABpUlEQVQoz6VSS1LCQBTkBCYoIhJDwh/yITGxsAgkMwmopeBnoy4sF15AyyNolXdw4V3bekOiqMDGRdebzOT16+56OWnXRIaNnS62tX00nBiaMUKlO4SWwRiKuwxqJ0C+bEEqGV/98q6FnFSigwk5fSxWfdSdGFUrgm5G0KlaTJw1M4RmhKLSMOohEdS3kSJHU7ZUB5t7PeQVG0Xdg26G2GsP0PGPUbdGqHUDtBwGs3+Cjj9B0+VQ24dffYvI5cs2ChVXfNDEcqOPljcRKqP4GOPxBAe+h6vbBzy/feDp9R2PL+9w4xvkf5GlhKTQFdOIkJRZgxmM/ilCNsZ0OsXZ7BKMRRgnCWLOETGO3tE9CponXC0h/JZOP8hpyP3DIWbnF7i6vgOLInAeg3OOIAhQqlDu9iqFP7PYIig2BkOGo0kCzhmSJEESc4EwHEGpuZAVe7Xl3w9EqjY8aM19aC0PettHtU11fldQyc2SDCm3zKrII6uUqWJDKtuQlZ6wJy1gGdkfwsUchUo1Vas6YuGzbViHlYREsog5mfN/QqGw4grCbL3W4ROt9FsWtU+4egAAAABJRU5ErkJggg=='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Sync with Salesforce","title":"Sync with Salesforce","src":"/static/b343b3ad9cc49298b53cc900ef4455b1/913fc/sync-with-salesforce.png","srcSet":["/static/b343b3ad9cc49298b53cc900ef4455b1/bc34b/sync-with-salesforce.png 293w","/static/b343b3ad9cc49298b53cc900ef4455b1/da9f0/sync-with-salesforce.png 585w","/static/b343b3ad9cc49298b53cc900ef4455b1/913fc/sync-with-salesforce.png 1170w","/static/b343b3ad9cc49298b53cc900ef4455b1/efb0c/sync-with-salesforce.png 1755w","/static/b343b3ad9cc49298b53cc900ef4455b1/79b58/sync-with-salesforce.png 2340w","/static/b343b3ad9cc49298b53cc900ef4455b1/fe067/sync-with-salesforce.png 2568w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"Full and partial sync options are available"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"After the sync is complete we can check our Salesforce instance for the data. New records should be added as Tutoring Session and Student Tutoring Session custom objects. Existing records that were updated in Knack since the most recent sync will also be updated so that all data in Salesforce matches what is in our database for an organization. We can use our custom tab to view our data in Salesforce, and depending on the cron string that is set in the partner’s Data Delivery Destination a Salesforce instance could be updated as often as every hour.\n"},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 34.075723830734965%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAAA3klEQVQoz6WO60rDQBCF9/1fqoI/JJaCRWisCEKTXmKSvWQ3e6u7R3bAglCh4sDHzJw5MwxbHw2M9eCThdQOQjvKhHEYlSGNTzN6ofExKghtwZXBrp/wdlTopCVf8bC71RMWq1dUdYvl9oDHlz2x3O6pr+qG6u95yeSpW9w/N1isd3jYtKg2DWmsad9xOrQY+g6SD1BivCD5iFkreGuuEpxBdPMPjRkbcOIaAxew1iGECB/ChdKHeDssxDOkS0gZv0bO+WZY+aDTEfEzlc0/LV896H0An890MKeE9E++AMxXGqEDcMYvAAAAAElFTkSuQmCC'); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Tutoring Session Display","title":"Tutoring Sessions Display","src":"/static/595ec2fb2153402c61f1b38b127f47ff/913fc/eda-tutoring-sessions-displayed.png","srcSet":["/static/595ec2fb2153402c61f1b38b127f47ff/bc34b/eda-tutoring-sessions-displayed.png 293w","/static/595ec2fb2153402c61f1b38b127f47ff/da9f0/eda-tutoring-sessions-displayed.png 585w","/static/595ec2fb2153402c61f1b38b127f47ff/913fc/eda-tutoring-sessions-displayed.png 1170w","/static/595ec2fb2153402c61f1b38b127f47ff/efb0c/eda-tutoring-sessions-displayed.png 1755w","/static/595ec2fb2153402c61f1b38b127f47ff/79b58/eda-tutoring-sessions-displayed.png 2340w","/static/595ec2fb2153402c61f1b38b127f47ff/7c811/eda-tutoring-sessions-displayed.png 2694w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"Tutoring Sessions have now been synced to Salesforce"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-wrapper"],"style":"position: relative; display: block; margin-left: auto; margin-right: auto;  max-width: 1170px;"},"children":[{"type":"text","value":"\n    "},{"type":"element","tagName":"span","properties":{"className":["gatsby-resp-image-background-image"],"style":"padding-bottom: 61.948955916473324%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAzklEQVQoz6WS0QqCMBSG9/6v0yME0VV0U3qXhNNtalvmmFPnHyhGF6YjD3wwDuMb5/wju/0Bx/MFpyBCGHFcI4bgA/86+/VJeKOw1kI8SjRth845tN3/kHucoFAlbNthrH4G/yI0YXhqAze4evQbIUkqoCqDuq7hnJu9NPXHN1eEcUyRCQGt9U/hxCRcHpkysKzAq6pgjNkuTNIMTFZDQms79BRyMJFDKoWmaRb35CUUIoetjVeCXsI05ZBSDp97LUUvYUw5cvVCv5Kwr/AN2yisiooh45MAAAAASUVORK5CYII='); background-size: cover; display: block;"},"children":[]},{"type":"text","value":"\n    "},{"type":"element","tagName":"img","properties":{"className":["gatsby-resp-image-image"],"style":"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;box-shadow:inset 0px 0px 0px 400px white;","alt":"Custom tutoring session tab","title":"Custom tutoring session tab","src":"/static/7ecbdea0da60bf60df966a476e8b7ba8/913fc/custom-tutoring-session-tab.png","srcSet":["/static/7ecbdea0da60bf60df966a476e8b7ba8/bc34b/custom-tutoring-session-tab.png 293w","/static/7ecbdea0da60bf60df966a476e8b7ba8/da9f0/custom-tutoring-session-tab.png 585w","/static/7ecbdea0da60bf60df966a476e8b7ba8/913fc/custom-tutoring-session-tab.png 1170w","/static/7ecbdea0da60bf60df966a476e8b7ba8/b1204/custom-tutoring-session-tab.png 1293w"],"sizes":["(max-width:","1170px)","100vw,","1170px"]},"children":[]},{"type":"text","value":"\n  "}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{"style":"text-align: center; font-size: 80%; font-style: italic"},"children":[{"type":"text","value":"Same image from above showing what a single Tutoring Session record looks like in Salesforce"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"We could shorten the sync interval to every minute if we would like, but that would have to be changed in Kernel.php:"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"div","properties":{"className":["gatsby-highlight"],"dataLanguage":"text"},"children":[{"type":"element","tagName":"pre","properties":{"className":["language-text"]},"children":[{"type":"element","tagName":"code","properties":{"className":["language-text"]},"children":[{"type":"text","value":"$schedule->command(InitiateDataDeliveryCron::class)\n            ->hourly() // changing this to ->everyMinute() would allow more frequent execution\n            ->appendOutputTo($log)\n            ->onOneServer();"}]}]}]},{"type":"text","value":"\n"},{"type":"element","tagName":"h3","properties":{},"children":[{"type":"text","value":"Conclusion"}]},{"type":"text","value":"\n"},{"type":"element","tagName":"p","properties":{},"children":[{"type":"text","value":"That pretty much sums things up. We have an eloquent "},{"type":"element","tagName":"a","properties":{"href":"https://appexchange.salesforce.com/appxListingDetail?listingId=a0N4V00000HZVVdUAP&tab=e"},"children":[{"type":"text","value":"Salesforce package"}]},{"type":"text","value":" that allows us to store data that mirrors objects in our codebase, an integration written in Laravel, and steps for how our Operations team and partner schools can use the integration to greatly enhance academic resources for students. Now that our partners can view student tutoring data directly in their Salesforce instance, they can closely monitor students and encourage them to seek out tutoring services from their peers through Knack. 65% of students who use Knack had never used campus tutoring before, so bringing more information to administrators’ fingertips can only benefit the students of every institution that uses Knack Tutoring Technologies."}]}],"data":{"quirksMode":false}},"excerpt":"Why does our Laravel app need a Salesforce integration?Knack Tutoring is a unique peer-tutoring platform used by colleges and universities…","timeToRead":13,"frontmatter":{"title":"Building a Salesforce PHP Integration Using Laravel","userDate":"July 11 2023","date":"2023-07-11T15:00:00.0Z","tags":["Engineering"],"image":{"childImageSharp":{"fluid":{"base64":"data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAMEAgX/xAAWAQEBAQAAAAAAAAAAAAAAAAACAAH/2gAMAwEAAhADEAAAAZ3c6nZhgC//xAAcEAACAgIDAAAAAAAAAAAAAAABAgADERITISL/2gAIAQEAAQUCqYbWoxdqczPfJ5NrCf/EABURAQEAAAAAAAAAAAAAAAAAAAAh/9oACAEDAQE/AUf/xAAVEQEBAAAAAAAAAAAAAAAAAAAAIf/aAAgBAgEBPwFX/8QAGRAAAgMBAAAAAAAAAAAAAAAAAUEAESAx/9oACAEBAAY/AgXOUs//xAAaEAEBAQEBAQEAAAAAAAAAAAABEQAxQVFh/9oACAEBAAE/IfHPR9wThw8uoo0mILlqA77ctCQ/N//aAAwDAQACAAMAAAAQww//xAAWEQEBAQAAAAAAAAAAAAAAAAABABH/2gAIAQMBAT8QMGyf/8QAFhEBAQEAAAAAAAAAAAAAAAAAARAR/9oACAECAQE/EHWf/8QAGxABAQADAAMAAAAAAAAAAAAAAREAIWExQVH/2gAIAQEAAT8QSpCYngyafTzINRRUBVq8MUzQRLvuMhuI4+wZFXr5gDVAMf/Z","aspectRatio":1.499844768705371,"src":"/static/7f17f2acfe74bb2501ad593272956f89/45a11/multiple-people-use-mac-at-once.jpg","srcSet":"/static/7f17f2acfe74bb2501ad593272956f89/f8f18/multiple-people-use-mac-at-once.jpg 930w,\n/static/7f17f2acfe74bb2501ad593272956f89/0e6ff/multiple-people-use-mac-at-once.jpg 1860w,\n/static/7f17f2acfe74bb2501ad593272956f89/45a11/multiple-people-use-mac-at-once.jpg 3720w,\n/static/7f17f2acfe74bb2501ad593272956f89/d65f9/multiple-people-use-mac-at-once.jpg 4831w","sizes":"(max-width: 3720px) 100vw, 3720px"}}},"author":{"id":"Scott Chaplinski","bio":"Senior Software Engineer","avatar":{"children":[{"__typename":"ImageSharp","fixed":{"base64":"data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAUABQDASIAAhEBAxEB/8QAGAABAQEBAQAAAAAAAAAAAAAAAAQDAQL/xAAWAQEBAQAAAAAAAAAAAAAAAAACAQP/2gAMAwEAAhADEAAAAfM++EUq8hPgaTgN/8QAGhABAAMAAwAAAAAAAAAAAAAAAQACAxMxQf/aAAgBAQABBQK+JyOALWOjWX3heievc//EABYRAQEBAAAAAAAAAAAAAAAAABEAEP/aAAgBAwEBPwFZ3//EABYRAQEBAAAAAAAAAAAAAAAAABARAf/aAAgBAgEBPwGQ0//EABwQAAEEAwEAAAAAAAAAAAAAACEAARExECBBgf/aAAgBAQAGPwJmsIPPmOWgjE6f/8QAGxABAAIDAQEAAAAAAAAAAAAAAQAhETFxQRD/2gAIAQEAAT8hxkgdiMs/cHXI/nfTktpcPoPqo2l9+/8A/9oADAMBAAIAAwAAABCAwMH/xAAWEQEBAQAAAAAAAAAAAAAAAAABEDH/2gAIAQMBAT8QAFdgn//EABgRAAMBAQAAAAAAAAAAAAAAAAABERAx/9oACAECAQE/EG8EOM//xAAeEAEAAgMAAgMAAAAAAAAAAAABABEhMUFRYXGBof/aAAgBAQABPxDZTANvsIWX3EDx33AoExT9YXOMKdwjp0q1vtxZ8MHVnQFZv7hODRyO8Y+J/9k=","width":400,"height":400,"src":"/static/9cde8d0f93b2f89dfc7baab094b0567a/c32cc/scott-chaplinski.jpg","srcSet":"/static/9cde8d0f93b2f89dfc7baab094b0567a/c32cc/scott-chaplinski.jpg 1x"}}]}}}},"relatedPosts":{"totalCount":20,"edges":[{"node":{"id":"e7b92535-950b-59c7-95f5-8ab80af08338","timeToRead":5,"excerpt":"IntroClueless, but looking to up your game on accessibility. Starting on a new project and wanting to make sure you keep accessibility in…","frontmatter":{"title":"Beginners Tips for Web Accessibility in React"},"fields":{"slug":"/a11y/"}}},{"node":{"id":"6c69d8f0-7998-5387-93e4-6547e3940c0d","timeToRead":9,"excerpt":"Pull Requests (PRs) are an essential part of the software development workflow, allowing developers to propose changes, contribute new…","frontmatter":{"title":"The Art (and Science) of Reviewable PRs"},"fields":{"slug":"/art-and-science-of-reviewable-prs/"}}},{"node":{"id":"2ababe59-b3e5-5fc9-9863-6d51c8356a00","timeToRead":10,"excerpt":"Late last year, we started looking into embedded analytical dashboard solutions. We concluded that embedding pre-built dashboards would…","frontmatter":{"title":"Unlocking the Power of Data with Cube on AWS: A Comprehensive Guide"},"fields":{"slug":"/aws-cube-js-deployment/"}}}]}},"pageContext":{"isCreatedByStatefulCreatePages":false,"slug":"/salesforce-appexchange/","prev":{"excerpt":"IntroductionAnomaly Detection can get intimidating when you hear it for the first time. You probably think about mathematical standard…","timeToRead":5,"frontmatter":{"title":"Simple Anomaly Detection Process","tags":["Engineering"],"date":"2023-04-02T15:24:00.0Z","draft":false,"image":{"childImageSharp":{"fluid":{"aspectRatio":2.043103448275862,"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAsUlEQVQoz6WSiQrDIBBE/f8vbaFQ6OWxx5RRU1NILDTCoFl3385igpnB3fcFQFOCve71XDXJD1OYsxyI2SCXE3A9w5+3KfQ30B2pOJSU+IAvTv932IHW3B4fuTsUnedtAtF3sxEjiCr6nUfZDMhhOBa1jFjEEUtrQKD2RjwzlqQ1W9eGtRMCCKtF0gpjGXe5i7AsI679++NQFBDD5mJcd+7qIzlak/ZmCPyxRRSqx0XWG3jyGMedIxh/AAAAAElFTkSuQmCC","sizes":"(max-width: 1896px) 100vw, 1896px","src":"/static/48f0a4a43eda321748e7901cd07db6dc/5db41/anomaly-detection.png","srcSet":"/static/48f0a4a43eda321748e7901cd07db6dc/4c9af/anomaly-detection.png 930w,\n/static/48f0a4a43eda321748e7901cd07db6dc/e914e/anomaly-detection.png 1860w,\n/static/48f0a4a43eda321748e7901cd07db6dc/5db41/anomaly-detection.png 1896w"}}},"author":{"id":"Lars Koester","bio":"Software Engineer","avatar":{"children":[{"fixed":{"src":"/static/f900b40b3dbf4da6f58d7852b7aa5625/f6494/lars-koester.png"}}]}}},"fields":{"layout":"post","slug":"/simple-anomaly-detection-process/"}},"next":{"excerpt":"Pull Requests (PRs) are an essential part of the software development workflow, allowing developers to propose changes, contribute new…","timeToRead":9,"frontmatter":{"title":"The Art (and Science) of Reviewable PRs","tags":["Engineering"],"date":"2023-11-29T15:00:00.0Z","draft":false,"image":null,"author":{"id":"Misha Hawthorn","bio":"Senior Software Engineer","avatar":{"children":[{"fixed":{"src":"/static/07417efdedf83537485b65256cece4bf/c32cc/misha-hawthorn.jpg"}}]}}},"fields":{"layout":"post","slug":"/art-and-science-of-reviewable-prs/"}},"primaryTag":"Engineering"}}