├── .eslintignore ├── .forceignore ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── apps ├── pulsar-mqtt-broker │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── server.js └── pulsar-simulator │ ├── index.js │ ├── package-lock.json │ └── package.json ├── assets ├── apex_classes.png ├── architecture.png └── schema.png ├── config └── project-scratch-def.json ├── data ├── Account-Service__c-plan.json ├── Accounts.json ├── Service__cs.json ├── Vehicle__c.json └── alt_fuel_stations.csv ├── force-app └── main │ └── default │ ├── applications │ └── RoutePlanner.app-meta.xml │ ├── aura │ └── .eslintrc.json │ ├── classes │ ├── ChargingStationsJobManager.cls │ ├── ChargingStationsJobManager.cls-meta.xml │ ├── DeliveryRouteController.cls │ ├── DeliveryRouteController.cls-meta.xml │ ├── functions │ │ ├── ChargingStationsFunction.cls │ │ ├── ChargingStationsFunction.cls-meta.xml │ │ ├── JobManagerFunction.cls │ │ ├── JobManagerFunction.cls-meta.xml │ │ ├── RoutePlannerFunction.cls │ │ └── RoutePlannerFunction.cls-meta.xml │ └── trigger handlers │ │ ├── DeliveryPlanTriggerHandler.cls │ │ └── DeliveryPlanTriggerHandler.cls-meta.xml │ ├── flexipages │ └── Account_Record_Page.flexipage-meta.xml │ ├── layouts │ ├── Account-Account Layout.layout-meta.xml │ ├── ChargingStation__c-Charging Station Layout.layout-meta.xml │ ├── DeliveryPlan__c-Delivery Plan Layout.layout-meta.xml │ ├── DeliveryRoute__c-Delivery Route Layout.layout-meta.xml │ ├── DeliveryWaypoint__c-Delivery Waypoint Layout.layout-meta.xml │ ├── Service__c-Service Layout.layout-meta.xml │ └── Vehicle__c-Vehicle Layout.layout-meta.xml │ ├── lwc │ ├── .eslintrc.json │ └── deliveries │ │ ├── deliveries.css │ │ ├── deliveries.html │ │ ├── deliveries.js │ │ └── deliveries.js-meta.xml │ ├── objectTranslations │ └── ChargingStation__c-en_US │ │ ├── ChargingStation__c-en_US.objectTranslation-meta.xml │ │ ├── City__c.fieldTranslation-meta.xml │ │ ├── Delivery_Waypoint__c.fieldTranslation-meta.xml │ │ ├── Distance__c.fieldTranslation-meta.xml │ │ ├── Location__c.fieldTranslation-meta.xml │ │ ├── State__c.fieldTranslation-meta.xml │ │ ├── Street_Address__c.fieldTranslation-meta.xml │ │ └── Zip__c.fieldTranslation-meta.xml │ ├── objects │ ├── ChargingStation__c │ │ ├── ChargingStation__c.object-meta.xml │ │ └── fields │ │ │ ├── City__c.field-meta.xml │ │ │ ├── Delivery_Waypoint__c.field-meta.xml │ │ │ ├── Distance__c.field-meta.xml │ │ │ ├── Location__c.field-meta.xml │ │ │ ├── State__c.field-meta.xml │ │ │ ├── Street_Address__c.field-meta.xml │ │ │ └── Zip__c.field-meta.xml │ ├── DeliveryPlan__c │ │ └── DeliveryPlan__c.object-meta.xml │ ├── DeliveryRoute__c │ │ ├── DeliveryRoute__c.object-meta.xml │ │ └── fields │ │ │ ├── DeliveryPlan__c.field-meta.xml │ │ │ └── Vehicle__c.field-meta.xml │ ├── DeliveryWaypoint__c │ │ ├── DeliveryWaypoint__c.object-meta.xml │ │ └── fields │ │ │ ├── DeliveryPlan__c.field-meta.xml │ │ │ ├── DeliveryRoute__c.field-meta.xml │ │ │ ├── Number__c.field-meta.xml │ │ │ ├── Service__c.field-meta.xml │ │ │ ├── VehicleName__c.field-meta.xml │ │ │ └── Vehicle_Vin__c.field-meta.xml │ ├── JobCompleted__e │ │ ├── JobCompleted__e.object-meta.xml │ │ └── fields │ │ │ └── DeliveryPlan_Id__c.field-meta.xml │ ├── Service__c │ │ ├── Service__c.object-meta.xml │ │ ├── fields │ │ │ ├── Account__c.field-meta.xml │ │ │ └── Location__c.field-meta.xml │ │ └── listViews │ │ │ └── All.listView-meta.xml │ └── Vehicle__c │ │ ├── Vehicle__c.object-meta.xml │ │ ├── fields │ │ ├── Location__c.field-meta.xml │ │ └── Vin__c.field-meta.xml │ │ └── listViews │ │ └── All.listView-meta.xml │ ├── permissionsets │ ├── Functions.permissionset-meta.xml │ ├── RoutePlanner.permissionset-meta.xml │ └── sfdcInternalInt__sfdc_evergreen_event_source.permissionset-meta.xml │ ├── platformEventChannelMembers │ └── ChangeEvents_DeliveryPlan_ChangeEvent.platformEventChannelMember-meta.xml │ ├── tabs │ ├── DeliveryPlan__c.tab-meta.xml │ ├── Service__c.tab-meta.xml │ └── Vehicle__c.tab-meta.xml │ └── triggers │ ├── DeliveryPlanTrigger.trigger │ └── DeliveryPlanTrigger.trigger-meta.xml ├── functions ├── chargingstations │ ├── .eslintrc │ ├── .mocharc.json │ ├── README.md │ ├── data │ │ └── payload.json │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── project.toml │ └── test │ │ └── index.test.js ├── jobmanager │ ├── .eslintrc │ ├── .mocharc.json │ ├── README.md │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── project.toml │ └── test │ │ └── index.test.js └── routeplanner │ ├── .classpath │ ├── .mvn │ └── wrapper │ │ ├── MavenWrapperDownloader.java │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties │ ├── data │ └── sample-payload.json │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ ├── project.toml │ ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── salesforce │ │ │ └── functions │ │ │ └── demo │ │ │ ├── FunctionInput.java │ │ │ ├── FunctionOutput.java │ │ │ └── RoutePlannerFunction.java │ └── test │ │ └── java │ │ └── com │ │ └── salesforce │ │ └── functions │ │ └── demo │ │ └── FunctionTest.java │ └── system.properties ├── jest.config.js ├── package-lock.json ├── package.json ├── scripts ├── apex │ └── hello.apex ├── create-db.js ├── package-lock.json ├── package.json └── soql │ └── account.soql └── sfdx-project.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/lwc/**/*.css 2 | **/lwc/**/*.html 3 | **/lwc/**/*.json 4 | **/lwc/**/*.svg 5 | **/lwc/**/*.xml 6 | **/aura/**/*.auradoc 7 | **/aura/**/*.cmp 8 | **/aura/**/*.css 9 | **/aura/**/*.design 10 | **/aura/**/*.evt 11 | **/aura/**/*.json 12 | **/aura/**/*.svg 13 | **/aura/**/*.tokens 14 | **/aura/**/*.xml 15 | **/aura/**/*.app 16 | .sfdx 17 | -------------------------------------------------------------------------------- /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | force-app/main/default/profiles/Admin.profile 5 | force-app/main/default/profiles/Admin.profile-meta.xml 6 | package.xml 7 | 8 | # LWC configuration files 9 | **/jsconfig.json 10 | **/.eslintrc.json 11 | 12 | # LWC Jest 13 | **/__tests__/** 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | # Salesforce cache 6 | .sf/ 7 | .sfdx/ 8 | target/ 9 | .localdevserver/ 10 | deploy-options.json 11 | 12 | # LWC VSCode autocomplete 13 | **/lwc/jsconfig.json 14 | 15 | # LWC Jest coverage reports 16 | coverage/ 17 | 18 | # Logs 19 | logs 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Dependency directories 26 | node_modules/ 27 | 28 | # Eslint cache 29 | .eslintcache 30 | 31 | # MacOS system files 32 | .DS_Store 33 | 34 | # Windows system files 35 | Thumbs.db 36 | ehthumbs.db 37 | [Dd]esktop.ini 38 | $RECYCLE.BIN/ 39 | 40 | # Local environment variables 41 | .env 42 | 43 | # Unused Profiles 44 | force-app/main/default/profiles/Admin.profile 45 | force-app/main/default/profiles/Admin.profile-meta.xml 46 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run precommit -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running prettier 2 | # More information: https://prettier.io/docs/en/ignore.html 3 | # 4 | 5 | **/staticresources/** 6 | .localdevserver 7 | .sfdx 8 | .vscode 9 | 10 | coverage/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "overrides": [ 4 | { 5 | "files": "**/lwc/**/*.html", 6 | "options": { "parser": "lwc" } 7 | }, 8 | { 9 | "files": "*.{cmp,page,component}", 10 | "options": { "parser": "html" } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "salesforcedx-vscode-core.show-cli-success-msg": false, 3 | "java.configuration.updateBuildConfiguration": "automatic" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Route Planner with Charging Stations 2 | 3 | ## Architecture 4 | 5 | ![architecture](assets/architecture.png) 6 | 7 | ## Apex Utility Classes 8 | 9 | ![apex-utility-classes](assets/apex_classes.png) 10 | 11 | ## Salesforce Schema 12 | 13 | ![salesforce-schema](assets/schema.png) 14 | 15 | ## Deployment Instructions 16 | 17 | 1. Create a Scratch Org 18 | 19 | ``` 20 | sfdx force:org:create -s -f config/project-scratch-def.json -a routeplannerdemo 21 | ``` 22 | 23 | 2. Push Source to Org 24 | 25 | ``` 26 | sfdx force:source:push -f 27 | ``` 28 | 29 | 3. Assign Permission Sets 30 | 31 | ``` 32 | sfdx force:user:permset:assign -n RoutePlanner 33 | sfdx force:user:permset:assign -n Functions 34 | ``` 35 | 36 | 4. Import Sample Data 37 | 38 | ``` 39 | sfdx force:data:tree:import -p data/Account-Service__c-plan.json 40 | ``` 41 | 42 | 5. Create a Compute Environment to Deploy Functions 43 | 44 | ``` 45 | sf env create compute -o routeplannerdemo -a routeplannerenv 46 | ``` 47 | 48 | 6. Deploy Functions 49 | 50 | ``` 51 | sf deploy functions -o routeplannerdemo 52 | ``` 53 | 54 | 7. Create Heroku Application for Data Resources 55 | 56 | ``` 57 | heroku create 58 | ``` 59 | 60 | 8. Add Heroku User Collaborator to Functions Account 61 | 62 | ``` 63 | sf env compute collaborator add --heroku-user username@example.com 64 | ``` 65 | 66 | 9. Create Postgres and Redis Resources 67 | 68 | ``` 69 | heroku addons:create heroku-postgresql:hobby-dev 70 | heroku addons:create heroku-redis:hobby-dev 71 | ``` 72 | 73 | 10. Attach Data Resources to Compute Environment 74 | 75 | ``` 76 | heroku addons:attach --app 77 | heroku addons:attach --app 78 | ``` 79 | 80 | 11. Deploy MQTT Heroku App 81 | 82 | Setup [Monorepo](https://elements.heroku.com/buildpacks/lstoll/heroku-buildpack-monorepo) and Node.js Buildpacks: 83 | 84 | ``` 85 | heroku buildpacks:add https://github.com/lstoll/heroku-buildpack-monorepo -a 86 | heroku buildpacks:add heroku/nodejs -a 87 | ``` 88 | 89 | Deploy Application using the Monorepo buildpack 90 | 91 | ``` 92 | heroku config:set APP_BASE=apps/pulsar-mqtt-broker 93 | git push https://git.heroku.com/.git main 94 | ``` 95 | 96 | 12. Setup `MQTT_URL` env variable 97 | 98 | ``` 99 | sf env var set MQTT_URL=wss://.herokuapp.com 100 | ``` 101 | 102 | 13. Load Charging Stations Dataset 103 | 104 | ``` 105 | cd scripts 106 | npm install 107 | heroku config --shell -a > .env 108 | node create-db.js 109 | ``` 110 | -------------------------------------------------------------------------------- /apps/pulsar-mqtt-broker/README.md: -------------------------------------------------------------------------------- 1 | # pulsar-mqtt-broker 2 | 3 | MQTT Broker with WebSockets Support 4 | 5 | ## Run locally 6 | 7 | ``` 8 | npm install 9 | node server.js 10 | ``` 11 | -------------------------------------------------------------------------------- /apps/pulsar-mqtt-broker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pulsar-mqtt-broker", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Pulsar - MQTT Broker over WebSockets", 6 | "author": "Salesforce Developer Relations", 7 | "license": "CC0-1.0", 8 | "scripts": { 9 | "lint": "eslint '**/*.{js,ts}'" 10 | }, 11 | "dependencies": { 12 | "aedes": "^0.46.3", 13 | "websocket-stream": "^5.5.2" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^8.12.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/pulsar-mqtt-broker/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const http = require("http"); 4 | const aedes = require("aedes")(); 5 | const ws = require("websocket-stream"); 6 | const port = process.env.PORT || 1883; 7 | 8 | const server = http.createServer((_req, res) => { 9 | res.writeHead(200); 10 | res.end("MQTT Broker"); 11 | }); 12 | ws.createServer({ server }, aedes.handle); 13 | server.listen(port, () => { 14 | console.log(`Listening on port ${port}`); 15 | }); 16 | -------------------------------------------------------------------------------- /apps/pulsar-simulator/index.js: -------------------------------------------------------------------------------- 1 | const mqtt = require("mqtt"); 2 | 3 | const MQTT_URL = process.env.MQTT_URL; 4 | if (!MQTT_URL) { 5 | console.error("MQTT_URL is not set"); 6 | process.exit(1); 7 | } 8 | 9 | const mqttClient = mqtt.connect(MQTT_URL); 10 | 11 | mqttClient.on("connect", () => { 12 | mqttClient.subscribe("/chargingstations/#", () => { 13 | mqttClient.on("message", (topic, message) => { 14 | console.log(`${topic}: ${message}`); 15 | }); 16 | }); 17 | }); 18 | 19 | mqttClient.on("error", (err) => { 20 | console.error(err.message); 21 | process.exit(1); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/pulsar-simulator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pulsar-simulator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mqtt": "^4.3.7" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /assets/apex_classes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heroku-examples/tdx-advanced-functions/b98df851a6963f189dc52f9ebba86cae743ee393/assets/apex_classes.png -------------------------------------------------------------------------------- /assets/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heroku-examples/tdx-advanced-functions/b98df851a6963f189dc52f9ebba86cae743ee393/assets/architecture.png -------------------------------------------------------------------------------- /assets/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heroku-examples/tdx-advanced-functions/b98df851a6963f189dc52f9ebba86cae743ee393/assets/schema.png -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "jduque company", 3 | "edition": "Developer", 4 | "features": ["EnableSetPasswordInApi", "Functions", "DataWeaveInApex"], 5 | "settings": { 6 | "lightningExperienceSettings": { 7 | "enableS1DesktopEnabled": true 8 | }, 9 | "mobileSettings": { 10 | "enableS1EncryptedStoragePref2": false 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /data/Account-Service__c-plan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sobject": "Account", 4 | "saveRefs": true, 5 | "resolveRefs": false, 6 | "files": ["Accounts.json"] 7 | }, 8 | { 9 | "sobject": "Service__c", 10 | "saveRefs": false, 11 | "resolveRefs": true, 12 | "files": ["Service__cs.json"] 13 | }, 14 | { 15 | "sobject": "Vehicle__c", 16 | "saveRefs": false, 17 | "resolveRefs": true, 18 | "files": ["Vehicle__c.json"] 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /data/Accounts.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Account", 6 | "referenceId": "AccountRef1" 7 | }, 8 | "Name": "Frisco Deliveries" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /data/Service__cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Service__c", 6 | "referenceId": "Service__cRef1" 7 | }, 8 | "Name": "Delivery to Benioff Mansion", 9 | "Location__Latitude__s": 37.7895878, 10 | "Location__Longitude__s": -122.4479755, 11 | "Account__c": "@AccountRef1" 12 | }, 13 | { 14 | "attributes": { 15 | "type": "Service__c", 16 | "referenceId": "Service__cRef2" 17 | }, 18 | "Name": "Delivery to Salesforce Tower", 19 | "Location__Latitude__s": 37.7897484, 20 | "Location__Longitude__s": -122.3994277, 21 | "Account__c": "@AccountRef1" 22 | }, 23 | { 24 | "attributes": { 25 | "type": "Service__c", 26 | "referenceId": "Service__cRef3" 27 | }, 28 | "Name": "Delivery to Landmark", 29 | "Location__Latitude__s": 37.7941843, 30 | "Location__Longitude__s": -122.3948638, 31 | "Account__c": "@AccountRef1" 32 | }, 33 | { 34 | "attributes": { 35 | "type": "Service__c", 36 | "referenceId": "Service__cRef4" 37 | }, 38 | "Name": "Delivery to Salesforce West", 39 | "Location__Latitude__s": 37.7906009, 40 | "Location__Longitude__s": -122.3993139, 41 | "Account__c": "@AccountRef1" 42 | }, 43 | { 44 | "attributes": { 45 | "type": "Service__c", 46 | "referenceId": "Service__cRef5" 47 | }, 48 | "Name": "Delivery to Moscone Center (Dreamforce)", 49 | "Location__Latitude__s": 37.7843, 50 | "Location__Longitude__s": -122.4007, 51 | "Account__c": "@AccountRef1" 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /data/Vehicle__c.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Vehicle__c", 6 | "referenceId": "Vehicle__cRef1" 7 | }, 8 | "Name": "Pulsar Two - Van", 9 | "Vin__c": "4JGCB65E16A196964", 10 | "Location__Latitude__s": 10, 11 | "Location__Longitude__s": 10 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /force-app/main/default/applications/RoutePlanner.app-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | View 5 | Action override created by Lightning App Builder during activation. 7 | Account_Record_Page 8 | Large 9 | false 10 | Flexipage 11 | Account 12 | 13 | 14 | #0070D2 15 | false 16 | 17 | Large 18 | false 19 | false 20 | 21 | Standard 22 | standard-Account 23 | Vehicle__c 24 | Service__c 25 | DeliveryPlan__c 26 | Lightning 27 | 28 | -------------------------------------------------------------------------------- /force-app/main/default/aura/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@salesforce/eslint-plugin-aura"], 3 | "extends": ["plugin:@salesforce/eslint-plugin-aura/recommended"], 4 | "rules": { 5 | "vars-on-top": "off", 6 | "no-unused-expressions": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /force-app/main/default/classes/ChargingStationsJobManager.cls: -------------------------------------------------------------------------------- 1 | public with sharing class ChargingStationsJobManager { 2 | public static void execute(List waypoints) { 3 | Integer jobs = waypoints.size(); 4 | if (jobs == 0) { 5 | System.debug( 6 | 'No waypoints found for this delivery plan. Ignoring execution.' 7 | ); 8 | // Should we trigger the event to notify the frontend? Maybe 9 | return; 10 | } 11 | JobManagerFunction.executeAsync(jobs, new Callback(waypoints)); 12 | } 13 | 14 | // Job Manager Callback 15 | private class Callback extends JobManagerFunction.Callback { 16 | protected List waypoints; 17 | public Callback(List waypoints) { 18 | this.waypoints = waypoints; 19 | } 20 | 21 | public override void handleJobStatus(JobManagerFunction.JobStatus status) { 22 | String jobId = status.jobId; 23 | List inputs = new List(); 24 | for (DeliveryWaypoint__c waypoint : waypoints) { 25 | ChargingStationsFunction.Input input = new ChargingStationsFunction.Input(); 26 | input.jobId = jobId; 27 | input.waypointId = waypoint.id; 28 | inputs.add(input); 29 | } 30 | // Retrieve Charging Stations by Waypoints in Parallel 31 | ChargingStationsFunction.executeParallel(inputs); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /force-app/main/default/classes/ChargingStationsJobManager.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DeliveryRouteController.cls: -------------------------------------------------------------------------------- 1 | public with sharing class DeliveryRouteController { 2 | @AuraEnabled 3 | public static void planDeliveryRoute(Id accountId) { 4 | try { 5 | Account account = [ 6 | SELECT Name 7 | FROM Account 8 | WHERE Id = :accountId 9 | WITH SECURITY_ENFORCED 10 | LIMIT 1 11 | ]; 12 | RoutePlannerFunction.execute(account.Name); 13 | } catch (Exception ex) { 14 | throw new AuraHandledException(ex.getMessage()); 15 | } 16 | } 17 | 18 | @AuraEnabled(cacheable=true) 19 | public static List getWaypoints(Id deliveryPlanId) { 20 | try { 21 | return [ 22 | SELECT 23 | Name, 24 | Service__r.Name, 25 | Service__r.Location__Latitude__s, 26 | Service__r.Location__Longitude__s, 27 | Number__c 28 | FROM DeliveryWaypoint__c 29 | WHERE DeliveryRoute__r.DeliveryPlan__c = :deliveryPlanId 30 | WITH SECURITY_ENFORCED 31 | ORDER BY CreatedDate DESC 32 | ]; 33 | } catch (Exception ex) { 34 | throw new AuraHandledException(ex.getMessage()); 35 | } 36 | } 37 | 38 | @AuraEnabled(cacheable=true) 39 | public static List getChargingStations( 40 | Id deliveryPlanId 41 | ) { 42 | try { 43 | List chargingStations = [ 44 | SELECT 45 | Id, 46 | Name, 47 | Street_Address__c, 48 | City__c, 49 | State__c, 50 | Zip__c, 51 | Location__latitude__s, 52 | Location__longitude__s, 53 | Distance__c 54 | FROM ChargingStation__c 55 | WHERE 56 | Delivery_Waypoint__r.DeliveryRoute__r.DeliveryPlan__c = :deliveryPlanId 57 | WITH SECURITY_ENFORCED 58 | ORDER BY Distance__c 59 | ]; 60 | return chargingStations; 61 | } catch (Exception e) { 62 | throw new AuraHandledException(e.getMessage()); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DeliveryRouteController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/functions/ChargingStationsFunction.cls: -------------------------------------------------------------------------------- 1 | public with sharing class ChargingStationsFunction { 2 | public static void executeParallel(List inputs) { 3 | for (Input input : inputs) { 4 | ChargingStationsFunction.executeAsync( 5 | input, 6 | new JobCallback(input.jobId) 7 | ); 8 | } 9 | } 10 | 11 | public static void executeAsync( 12 | Input input, 13 | functions.FunctionCallback callback 14 | ) { 15 | functions.Function fn = functions.Function.get( 16 | 'tdx_advanced_functions.chargingstations' 17 | ); 18 | fn.invoke(JSON.serialize(input), callback); 19 | } 20 | 21 | public class Input { 22 | public Id waypointId; 23 | public String jobId; 24 | } 25 | 26 | public class Output { 27 | public Id deliveryPlanId; 28 | public Id waypointId; 29 | public JobManagerFunction.JobStatus job; 30 | } 31 | 32 | private class JobCallback implements functions.FunctionCallback { 33 | protected String jobId; 34 | public JobCallback(String jobId) { 35 | this.jobId = jobId; 36 | } 37 | 38 | public void handleResponse(functions.FunctionInvocation invocation) { 39 | // Check if there is any error during the invocation 40 | if (invocation.getStatus() == functions.FunctionInvocationStatus.ERROR) { 41 | String errorMessage = invocation.getError() != null 42 | ? invocation.getError().getMessage() 43 | : 'UNKNOWN'; 44 | System.debug(errorMessage); 45 | throw new CalloutException(errorMessage); 46 | } 47 | 48 | String response = invocation.getResponse(); 49 | Output output = (Output) JSON.deserialize(response, Output.class); 50 | 51 | // Check if this callback was executed in a Job context and it completed successfully 52 | if ( 53 | output.job != null && 54 | (this.jobId == output.job.jobId && 55 | output.job.status == 'completed') 56 | ) { 57 | System.debug('Trigger Custom Notification'); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /force-app/main/default/classes/functions/ChargingStationsFunction.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/functions/JobManagerFunction.cls: -------------------------------------------------------------------------------- 1 | public with sharing class JobManagerFunction { 2 | // Schedule a certain number of jobs and return the Job Status 3 | public static void executeAsync(Integer jobs, Callback callback) { 4 | functions.Function fn = functions.Function.get( 5 | 'tdx_advanced_functions.jobmanager' 6 | ); 7 | functions.FunctionInvocation invocation = fn.invoke( 8 | '{ "jobs": ' + 9 | jobs + 10 | ' }', 11 | callback 12 | ); 13 | } 14 | 15 | public class JobStatus { 16 | public String jobId; 17 | public String status; 18 | } 19 | 20 | public virtual class Callback implements functions.FunctionCallback { 21 | public void handleResponse(functions.FunctionInvocation invocation) { 22 | // Check if there is any error during the invocation 23 | if (invocation.getStatus() == functions.FunctionInvocationStatus.ERROR) { 24 | String errorMessage = invocation.getError() != null 25 | ? invocation.getError().getMessage() 26 | : 'UNKNOWN'; 27 | System.debug(errorMessage); 28 | throw new CalloutException(errorMessage); 29 | } 30 | 31 | String response = invocation.getResponse(); 32 | JobStatus status = (JobStatus) JSON.deserialize( 33 | response, 34 | JobStatus.class 35 | ); 36 | handleJobStatus(status); 37 | } 38 | 39 | public virtual void handleJobStatus(JobStatus status) { 40 | System.debug('Job ' + status.jobId + ' is ' + status.status); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /force-app/main/default/classes/functions/JobManagerFunction.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/functions/RoutePlannerFunction.cls: -------------------------------------------------------------------------------- 1 | public with sharing class RoutePlannerFunction { 2 | public static String execute(String accountName) { 3 | functions.Function fn = functions.Function.get( 4 | 'tdx_advanced_functions.routeplanner' 5 | ); 6 | functions.FunctionInvocation invocation = fn.invoke( 7 | '{ "accountName": "' + 8 | accountName + 9 | '" }' 10 | ); 11 | 12 | // Check if there is any error during the invocation 13 | if (invocation.getStatus() == functions.FunctionInvocationStatus.ERROR) { 14 | String errorMessage = invocation.getError() != null 15 | ? invocation.getError().getMessage() 16 | : 'UNKNOWN'; 17 | System.debug(errorMessage); 18 | throw new CalloutException(errorMessage); 19 | } 20 | 21 | String response = invocation.getResponse(); 22 | return response; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /force-app/main/default/classes/functions/RoutePlannerFunction.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/trigger handlers/DeliveryPlanTriggerHandler.cls: -------------------------------------------------------------------------------- 1 | public with sharing class DeliveryPlanTriggerHandler { 2 | protected List events; 3 | public DeliveryPlanTriggerHandler(List events) { 4 | this.events = events; 5 | handleDeliveryPlan(); 6 | } 7 | 8 | private void handleDeliveryPlan() { 9 | for (DeliveryPlan__ChangeEvent event : events) { 10 | EventBus.ChangeEventHeader header = event.ChangeEventHeader; 11 | if (header.changetype == 'CREATE') { 12 | handleCreate(header.getRecordIds().get(0)); 13 | } 14 | } 15 | } 16 | 17 | private void handleCreate(Id deliveryPlanId) { 18 | List waypoints = [ 19 | SELECT Id 20 | FROM DeliveryWaypoint__c 21 | WHERE DeliveryRoute__r.DeliveryPlan__c = :deliveryPlanId 22 | WITH SECURITY_ENFORCED 23 | ]; 24 | // Execute the Job 25 | ChargingStationsJobManager.execute(waypoints); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /force-app/main/default/classes/trigger handlers/DeliveryPlanTriggerHandler.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/flexipages/Account_Record_Page.flexipage-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | collapsed 8 | false 9 | 10 | 11 | enableActionsConfiguration 12 | false 13 | 14 | 15 | enableActionsInNative 16 | false 17 | 18 | 19 | hideChatterActions 20 | false 21 | 22 | 23 | numVisibleActions 24 | 3 25 | 26 | force:highlightsPanel 27 | highlightsId 28 | 29 | 30 | header 31 | Region 32 | 33 | 34 | 35 | 36 | deliveries 37 | c_deliveries1 38 | 39 | 40 | main 41 | Region 42 | 43 | Account Record Page 44 | Account 45 | 48 | RecordPage 49 | 50 | -------------------------------------------------------------------------------- /force-app/main/default/layouts/Account-Account Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Submit 4 | 5 | false 6 | false 7 | true 8 | 9 | 10 | 11 | Required 12 | Name 13 | 14 | 15 | Edit 16 | OwnerId 17 | 18 | 19 | Edit 20 | ParentId 21 | 22 | 23 | Edit 24 | AccountNumber 25 | 26 | 27 | Edit 28 | Type 29 | 30 | 31 | Edit 32 | Industry 33 | 34 | 35 | Edit 36 | AnnualRevenue 37 | 38 | 39 | 40 | 41 | Edit 42 | ShippingAddress 43 | 44 | 45 | Edit 46 | Rating 47 | 48 | 49 | Edit 50 | Phone 51 | 52 | 53 | Edit 54 | Fax 55 | 56 | 57 | Edit 58 | Website 59 | 60 | 61 | Edit 62 | Ownership 63 | 64 | 65 | 66 | 67 | 68 | false 69 | false 70 | true 71 | 72 | 73 | 74 | 75 | 76 | 77 | false 78 | false 79 | true 80 | 81 | 82 | 83 | 84 | 85 | 86 | false 87 | false 88 | true 89 | 90 | 91 | 92 | Readonly 93 | CreatedById 94 | 95 | 96 | 97 | 98 | Readonly 99 | LastModifiedById 100 | 101 | 102 | 103 | 104 | 105 | false 106 | false 107 | true 108 | 109 | 110 | 111 | Edit 112 | Description 113 | 114 | 115 | 116 | 117 | 118 | true 119 | false 120 | false 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | Record 129 | 130 | FeedItem.TextPost 131 | QuickAction 132 | 1 133 | 134 | 135 | FeedItem.ContentPost 136 | QuickAction 137 | 2 138 | 139 | 140 | NewTask 141 | QuickAction 142 | 3 143 | 144 | 145 | NewContact 146 | QuickAction 147 | 4 148 | 149 | 150 | NewCase 151 | QuickAction 152 | 5 153 | 154 | 155 | LogACall 156 | QuickAction 157 | 6 158 | 159 | 160 | NewNote 161 | QuickAction 162 | 7 163 | 164 | 165 | NewOpportunity 166 | QuickAction 167 | 8 168 | 169 | 170 | NewEvent 171 | QuickAction 172 | 9 173 | 174 | 175 | FeedItem.RypplePost 176 | QuickAction 177 | 10 178 | 179 | 180 | FeedItem.LinkPost 181 | QuickAction 182 | 11 183 | 184 | 185 | FeedItem.PollPost 186 | QuickAction 187 | 12 188 | 189 | 190 | FeedItem.QuestionPost 191 | QuickAction 192 | 13 193 | 194 | 195 | SendEmail 196 | QuickAction 197 | 14 198 | 199 | 200 | Submit 201 | StandardButton 202 | 15 203 | 204 | 205 | ChangeRecordType 206 | StandardButton 207 | 16 208 | 209 | 210 | CreateCallList 211 | StandardButton 212 | 17 213 | 214 | 215 | AccountAddToCampaign 216 | StandardButton 217 | 18 218 | 219 | 220 | Edit 221 | StandardButton 222 | 19 223 | 224 | 225 | ChangeOwnerOne 226 | StandardButton 227 | 20 228 | 229 | 230 | IncludeOffline 231 | StandardButton 232 | 21 233 | 234 | 235 | PrintableView 236 | StandardButton 237 | 22 238 | 239 | 240 | CreateSurveyInvitation 241 | StandardButton 242 | 23 243 | 244 | 245 | PartnerScorecard 246 | StandardButton 247 | 24 248 | 249 | 250 | Share 251 | StandardButton 252 | 25 253 | 254 | 255 | JigsawSearch 256 | StandardButton 257 | 26 258 | 259 | 260 | AccountHierarchy 261 | StandardButton 262 | 27 263 | 264 | 265 | XClean 266 | StandardButton 267 | 28 268 | 269 | 270 | Delete 271 | StandardButton 272 | 29 273 | 274 | 275 | CallHighlightAction 276 | ProductivityAction 277 | 30 278 | 279 | 280 | SmsHighlightAction 281 | ProductivityAction 282 | 31 283 | 284 | 285 | EmailHighlightAction 286 | ProductivityAction 287 | 32 288 | 289 | 290 | WebsiteHighlightAction 291 | ProductivityAction 292 | 33 293 | 294 | 295 | 296 | 297 | FeedItem.TextPost 298 | 299 | 300 | FeedItem.ContentPost 301 | 302 | 303 | NewTask 304 | 305 | 306 | NewContact 307 | 308 | 309 | NewCase 310 | 311 | 312 | LogACall 313 | 314 | 315 | NewNote 316 | 317 | 318 | NewOpportunity 319 | 320 | 321 | NewEvent 322 | 323 | 324 | FeedItem.RypplePost 325 | 326 | 327 | FeedItem.LinkPost 328 | 329 | 330 | FeedItem.PollPost 331 | 332 | 333 | FeedItem.QuestionPost 334 | 335 | 336 | SendEmail 337 | 338 | 339 | 340 | 341 | 342 | runtime_sales_social:socialPanel 343 | 344 | 345 | 346 | 347 | FULL_NAME 348 | CONTACT.TITLE 349 | CONTACT.EMAIL 350 | CONTACT.PHONE1 351 | RelatedContactList 352 | 353 | 354 | OPPORTUNITY.NAME 355 | OPPORTUNITY.STAGE_NAME 356 | OPPORTUNITY.AMOUNT 357 | OPPORTUNITY.CLOSE_DATE 358 | RelatedOpportunityList 359 | 360 | 361 | CASES.CASE_NUMBER 362 | NAME 363 | CASES.SUBJECT 364 | CASES.PRIORITY 365 | CASES.CREATED_DATE_DATE_ONLY 366 | CASES.STATUS 367 | OWNER_NAME 368 | RelatedCaseList 369 | 370 | 371 | TASK.SUBJECT 372 | TASK.WHO_NAME 373 | TASK.WHAT_NAME 374 | ACTIVITY.TASK 375 | TASK.DUE_DATE 376 | TASK.STATUS 377 | TASK.PRIORITY 378 | CORE.USERS.FULL_NAME 379 | RelatedActivityList 380 | 381 | 382 | TASK.SUBJECT 383 | TASK.WHO_NAME 384 | TASK.WHAT_NAME 385 | ACTIVITY.TASK 386 | TASK.DUE_DATE 387 | CORE.USERS.FULL_NAME 388 | TASK.LAST_UPDATE 389 | RelatedHistoryList 390 | 391 | 392 | RelatedNoteList 393 | 394 | 395 | ACCOUNT.NAME 396 | OPPORTUNITY.NAME 397 | PARTNER.ROLE 398 | RelatedPartnerList 399 | 400 | ParentId 401 | false 402 | false 403 | false 404 | false 405 | false 406 | 407 | 00h8A0000018tE7 408 | 4 409 | 0 410 | Default 411 | 412 | 413 | -------------------------------------------------------------------------------- /force-app/main/default/layouts/ChargingStation__c-Charging Station Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | true 7 | 8 | 9 | 10 | Required 11 | Name 12 | 13 | 14 | Edit 15 | Street_Address__c 16 | 17 | 18 | Edit 19 | City__c 20 | 21 | 22 | Edit 23 | State__c 24 | 25 | 26 | Required 27 | Location__c 28 | 29 | 30 | Edit 31 | Zip__c 32 | 33 | 34 | Required 35 | Delivery_Waypoint__c 36 | 37 | 38 | Edit 39 | Distance__c 40 | 41 | 42 | 43 | 44 | 45 | 46 | false 47 | false 48 | true 49 | 50 | 51 | 52 | Readonly 53 | CreatedById 54 | 55 | 56 | 57 | 58 | Readonly 59 | LastModifiedById 60 | 61 | 62 | 63 | 64 | 65 | false 66 | false 67 | true 68 | 69 | 70 | 71 | 72 | 73 | false 74 | false 75 | false 76 | false 77 | false 78 | 79 | -------------------------------------------------------------------------------- /force-app/main/default/layouts/DeliveryPlan__c-Delivery Plan Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Submit 4 | 5 | false 6 | false 7 | true 8 | 9 | 10 | 11 | Required 12 | Name 13 | 14 | 15 | 16 | 17 | 18 | 19 | false 20 | false 21 | true 22 | 23 | 24 | 25 | Readonly 26 | CreatedById 27 | 28 | 29 | 30 | 31 | Readonly 32 | LastModifiedById 33 | 34 | 35 | 36 | 37 | 38 | true 39 | false 40 | true 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | NAME 49 | Vehicle__c 50 | DeliveryRoute__c.DeliveryPlan__c 51 | 52 | false 53 | false 54 | false 55 | false 56 | false 57 | 58 | 00hR0000000PunE 59 | 4 60 | 0 61 | Default 62 | 63 | 64 | -------------------------------------------------------------------------------- /force-app/main/default/layouts/DeliveryRoute__c-Delivery Route Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Submit 4 | 5 | false 6 | false 7 | true 8 | 9 | 10 | 11 | Readonly 12 | Name 13 | 14 | 15 | Required 16 | DeliveryPlan__c 17 | 18 | 19 | Edit 20 | Vehicle__c 21 | 22 | 23 | 24 | 25 | 26 | 27 | false 28 | false 29 | true 30 | 31 | 32 | 33 | Readonly 34 | CreatedById 35 | 36 | 37 | 38 | 39 | Readonly 40 | LastModifiedById 41 | 42 | 43 | 44 | 45 | 46 | true 47 | false 48 | true 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | NAME 57 | Number__c 58 | Service__c 59 | DeliveryWaypoint__c.DeliveryRoute__c 60 | Number__c 61 | Asc 62 | 63 | false 64 | false 65 | false 66 | false 67 | false 68 | 69 | 00hR0000000PunJ 70 | 4 71 | 0 72 | Default 73 | 74 | 75 | -------------------------------------------------------------------------------- /force-app/main/default/layouts/DeliveryWaypoint__c-Delivery Waypoint Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Submit 4 | 5 | false 6 | false 7 | true 8 | 9 | 10 | 11 | Readonly 12 | Name 13 | 14 | 15 | Required 16 | DeliveryRoute__c 17 | 18 | 19 | Readonly 20 | Vehicle_Vin__c 21 | 22 | 23 | 24 | 25 | Edit 26 | Number__c 27 | 28 | 29 | Edit 30 | Service__c 31 | 32 | 33 | 34 | 35 | 36 | false 37 | false 38 | true 39 | 40 | 41 | 42 | Readonly 43 | CreatedById 44 | 45 | 46 | 47 | 48 | Readonly 49 | LastModifiedById 50 | 51 | 52 | 53 | 54 | 55 | true 56 | false 57 | true 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | NAME 66 | ChargingStation__c.Delivery_Waypoint__c 67 | 68 | false 69 | false 70 | false 71 | false 72 | false 73 | 74 | 00hR0000000PunO 75 | 4 76 | 0 77 | Default 78 | 79 | 80 | -------------------------------------------------------------------------------- /force-app/main/default/layouts/Service__c-Service Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | true 7 | 8 | 9 | 10 | Required 11 | Name 12 | 13 | 14 | Edit 15 | Account__c 16 | 17 | 18 | Required 19 | Location__c 20 | 21 | 22 | 23 | 24 | Edit 25 | OwnerId 26 | 27 | 28 | 29 | 30 | 31 | false 32 | false 33 | true 34 | 35 | 36 | 37 | Readonly 38 | CreatedById 39 | 40 | 41 | 42 | 43 | Readonly 44 | LastModifiedById 45 | 46 | 47 | 48 | 49 | 50 | false 51 | false 52 | true 53 | 54 | 55 | 56 | 57 | 58 | false 59 | false 60 | false 61 | false 62 | false 63 | 64 | -------------------------------------------------------------------------------- /force-app/main/default/layouts/Vehicle__c-Vehicle Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | true 7 | 8 | 9 | 10 | Required 11 | Name 12 | 13 | 14 | Required 15 | Vin__c 16 | 17 | 18 | Required 19 | Location__c 20 | 21 | 22 | 23 | 24 | Edit 25 | OwnerId 26 | 27 | 28 | 29 | 30 | 31 | false 32 | false 33 | true 34 | 35 | 36 | 37 | Readonly 38 | CreatedById 39 | 40 | 41 | 42 | 43 | Readonly 44 | LastModifiedById 45 | 46 | 47 | 48 | 49 | 50 | false 51 | false 52 | true 53 | 54 | 55 | 56 | 57 | 58 | false 59 | false 60 | false 61 | false 62 | false 63 | 64 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@salesforce/eslint-config-lwc/recommended"], 3 | "overrides": [ 4 | { 5 | "files": ["*.test.js"], 6 | "rules": { 7 | "@lwc/lwc/no-unexpected-wire-adapter-usages": "off" 8 | }, 9 | "env": { 10 | "node": true 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/deliveries/deliveries.css: -------------------------------------------------------------------------------- 1 | .spinner-container { 2 | position: relative; 3 | display: inline-block; 4 | min-height: 250px; 5 | width: 100%; 6 | } 7 | 8 | .spinner-text { 9 | text-align: center; 10 | } 11 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/deliveries/deliveries.html: -------------------------------------------------------------------------------- 1 | 2 | 58 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/deliveries/deliveries.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api } from "lwc"; 2 | import { ShowToastEvent } from "lightning/platformShowToastEvent"; 3 | import { subscribe } from "lightning/empApi"; 4 | import planDeliveryRoute from "@salesforce/apex/DeliveryRouteController.planDeliveryRoute"; 5 | import getWaypoints from "@salesforce/apex/DeliveryRouteController.getWaypoints"; 6 | import getChargingStations from "@salesforce/apex/DeliveryRouteController.getChargingStations"; 7 | 8 | const CDC_CHANNEL = "/data/DeliveryPlan__ChangeEvent"; 9 | const JOB_CHANNEL = "/event/JobCompleted__e"; 10 | 11 | const DATATABLE_COLUMNS = [ 12 | { 13 | label: "Delivery Order", 14 | fieldName: "order", 15 | type: "number", 16 | cellAttributes: { alignment: "left" } 17 | }, 18 | { label: "Name", fieldName: "name" } 19 | ]; 20 | 21 | export default class Deliveries extends LightningElement { 22 | @api recordId; 23 | error; 24 | planningRoute = false; 25 | progress = 0; 26 | progressText = ""; 27 | cdcData; 28 | cdcSubscription; 29 | jobData; 30 | jobSubscription; 31 | datatableColumns = DATATABLE_COLUMNS; 32 | datatableData = []; 33 | mapMarkers = []; 34 | centerLocation; 35 | 36 | connectedCallback() { 37 | const cdcCallback = (response) => { 38 | this.cdcData = response; 39 | const deliveryPlanId = 40 | response?.data?.payload?.ChangeEventHeader?.recordIds[0]; 41 | if (deliveryPlanId) { 42 | console.log( 43 | `Route has been planned, waiting for charging stations - DeliveryPlanId: ${deliveryPlanId}` 44 | ); 45 | this.progress = 50; 46 | this.progressText = 47 | "Route has been planned, waiting for charging stations..."; 48 | } 49 | }; 50 | 51 | const jobCompletedCallback = (response) => { 52 | this.jobData = response; 53 | const deliveryPlanId = response?.data?.payload?.DeliveryPlan_Id__c; 54 | console.log(`Job has completed - DeliveryPlanId: ${deliveryPlanId}`); 55 | if (deliveryPlanId) { 56 | this.progress = 75; 57 | this.progressText = "Job has completed, rendering delivery waypoints!"; 58 | this.loadWaypoints(deliveryPlanId); 59 | } 60 | }; 61 | 62 | if (!this.cdcSubscription) { 63 | subscribe(CDC_CHANNEL, -1, cdcCallback).then((response) => { 64 | // eslint-disable-next-line no-console 65 | console.log("Successfully subscribed to CDC"); 66 | this.cdcSubscription = response; 67 | }); 68 | } 69 | 70 | if (!this.jobSubscription) { 71 | subscribe(JOB_CHANNEL, -1, jobCompletedCallback).then((response) => { 72 | // eslint-disable-next-line no-console 73 | console.log("Successfully subscribed to Job Event"); 74 | this.jobSubscription = response; 75 | }); 76 | } 77 | } 78 | 79 | loadWaypoints(deliveryPlanId) { 80 | const tempDatatable = []; 81 | const tempMarkers = []; 82 | Promise.all([ 83 | getWaypoints({ deliveryPlanId }), 84 | getChargingStations({ deliveryPlanId }) 85 | ]) 86 | .then(([waypointsData, chargingStationsData]) => { 87 | this.progress = 100; 88 | // Extract Waypoints Data 89 | if (waypointsData) { 90 | waypointsData.forEach((entry) => { 91 | const order = entry.Number__c ? entry.Number__c : 1; 92 | tempDatatable.push({ 93 | order, 94 | name: entry.Service__r.Name 95 | }); 96 | tempMarkers.push({ 97 | title: `${order} - ${entry.Service__r.Name}`, 98 | value: "delivery" + entry.Number__c ? entry.Number__c : 1, 99 | location: { 100 | Latitude: entry.Service__r.Location__Latitude__s, 101 | Longitude: entry.Service__r.Location__Longitude__s 102 | } 103 | }); 104 | }); 105 | } 106 | 107 | // Extract Charging Stations Data 108 | if (chargingStationsData) { 109 | chargingStationsData.forEach((entry, index) => { 110 | tempMarkers.push({ 111 | title: entry.Name, 112 | value: "station" + index, 113 | description: `

${entry.Street_Address__c}

${entry.City__c}, ${entry.State__c}, ${entry.Zip__c}

`, 114 | location: { 115 | Latitude: entry.Location__Latitude__s, 116 | Longitude: entry.Location__Longitude__s 117 | }, 118 | mapIcon: { 119 | path: "M 2,1 C 0.8344723,1 0,1.7955215 0,3 L 0,14 7,14 7,6.125 C 7,6.125 7.875,6 7.875,7 l 0,4 c 0,2 1.864698,2.125 2.125,2.125 0.275652,0 2.125,-0.124975 2.125,-2.125 l 0,-4 c 0,0 1.387558,0.017377 1.375,-2.96875 l -0.75,0 0,-2 C 12.75,1.4640917 12,1.4675079 12,2 l 0,2 -1,0 0,-2 C 11,1.4538157 10.25,1.4548128 10.25,2 l 0,2 -0.75,0 c 0.012522,2.9863904 1.375,3 1.375,3 l 0,4 c 0,0.874159 -0.767136,0.875 -0.875,0.875 -0.107864,0 -0.875,-0.04279 -0.875,-0.875 l 0,-4 C 9.125,5.7190916 8,4.875 7,4.875 L 7,3 C 7,1.7775442 6.1835046,1 5,1 z M 3,4 5,4 3.75,6 5.25,6 2.5,10 2,10 3,7 1.75,7 z", 120 | fillColor: "purple", 121 | fillOpacity: 0.9, 122 | strokeWeight: 0.5, 123 | scale: 2, 124 | anchor: { x: 14, y: 14 } 125 | } 126 | }); 127 | }); 128 | } 129 | this.datatableData = tempDatatable.sort(this.sortDataTable); 130 | this.mapMarkers = tempMarkers.sort(this.sortMapMarkers); 131 | if (this.mapMarkers.length > 0) { 132 | this.centerLocation = this.mapMarkers[0].location; 133 | } 134 | this.planningRoute = false; 135 | }) 136 | .catch((error) => { 137 | this.showError(error); 138 | }); 139 | } 140 | 141 | handlePlanDeliveryRoute() { 142 | this.mapMarkers = []; 143 | this.datatableData = []; 144 | this.planningRoute = true; 145 | this.progress = 0; 146 | this.progressText = "Calculating delivery route..."; 147 | planDeliveryRoute({ accountId: this.recordId }).catch((error) => { 148 | this.showError(error); 149 | }); 150 | } 151 | 152 | sortMapMarkers(la, lb) { 153 | const locationA = la.value; 154 | const locationB = lb.value; 155 | let comparison = 0; 156 | if (locationA > locationB) { 157 | comparison = 1; 158 | } else if (locationA < locationB) { 159 | comparison = -1; 160 | } 161 | return comparison; 162 | } 163 | 164 | sortDataTable(sa, sb) { 165 | const serviceA = sa.order; 166 | const serviceB = sb.order; 167 | 168 | let comparison = 0; 169 | if (serviceA > serviceB) { 170 | comparison = 1; 171 | } else if (serviceA < serviceB) { 172 | comparison = -1; 173 | } 174 | return comparison; 175 | } 176 | 177 | showError(error) { 178 | this.error = error; 179 | this.planningRoute = false; 180 | this.mapMarkers = []; 181 | this.datatableData = []; 182 | this.showToast( 183 | "An error has occurred", 184 | error?.message || error?.body?.message, 185 | "error" 186 | ); 187 | } 188 | 189 | showToast(title, message, variant) { 190 | const event = new ShowToastEvent({ 191 | title, 192 | message, 193 | variant 194 | }); 195 | this.dispatchEvent(event); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/deliveries/deliveries.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54.0 4 | true 5 | 6 | lightning__RecordPage 7 | 8 | 9 | -------------------------------------------------------------------------------- /force-app/main/default/objectTranslations/ChargingStation__c-en_US/ChargingStation__c-en_US.objectTranslation-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | Charging Station 6 | 7 | 8 | true 9 | Charging Stations 10 | 11 | Consonant 12 | 13 | -------------------------------------------------------------------------------- /force-app/main/default/objectTranslations/ChargingStation__c-en_US/City__c.fieldTranslation-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/objectTranslations/ChargingStation__c-en_US/Delivery_Waypoint__c.fieldTranslation-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /force-app/main/default/objectTranslations/ChargingStation__c-en_US/Distance__c.fieldTranslation-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/objectTranslations/ChargingStation__c-en_US/Location__c.fieldTranslation-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/objectTranslations/ChargingStation__c-en_US/State__c.fieldTranslation-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/objectTranslations/ChargingStation__c-en_US/Street_Address__c.fieldTranslation-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/objectTranslations/ChargingStation__c-en_US/Zip__c.fieldTranslation-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/objects/ChargingStation__c/ChargingStation__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | Accept 9 | Large 10 | Default 11 | 12 | 13 | Accept 14 | Small 15 | Default 16 | 17 | 18 | CancelEdit 19 | Default 20 | 21 | 22 | CancelEdit 23 | Large 24 | Default 25 | 26 | 27 | CancelEdit 28 | Small 29 | Default 30 | 31 | 32 | Clone 33 | Default 34 | 35 | 36 | Clone 37 | Large 38 | Default 39 | 40 | 41 | Clone 42 | Small 43 | Default 44 | 45 | 46 | Delete 47 | Default 48 | 49 | 50 | Delete 51 | Large 52 | Default 53 | 54 | 55 | Delete 56 | Small 57 | Default 58 | 59 | 60 | Edit 61 | Default 62 | 63 | 64 | Edit 65 | Large 66 | Default 67 | 68 | 69 | Edit 70 | Small 71 | Default 72 | 73 | 74 | List 75 | Default 76 | 77 | 78 | List 79 | Large 80 | Default 81 | 82 | 83 | List 84 | Small 85 | Default 86 | 87 | 88 | New 89 | Default 90 | 91 | 92 | New 93 | Large 94 | Default 95 | 96 | 97 | New 98 | Small 99 | Default 100 | 101 | 102 | SaveEdit 103 | Default 104 | 105 | 106 | SaveEdit 107 | Large 108 | Default 109 | 110 | 111 | SaveEdit 112 | Small 113 | Default 114 | 115 | 116 | Tab 117 | Default 118 | 119 | 120 | Tab 121 | Large 122 | Default 123 | 124 | 125 | Tab 126 | Small 127 | Default 128 | 129 | 130 | View 131 | Default 132 | 133 | 134 | View 135 | Large 136 | Default 137 | 138 | 139 | View 140 | Small 141 | Default 142 | 143 | false 144 | SYSTEM 145 | Deployed 146 | false 147 | true 148 | false 149 | false 150 | false 151 | false 152 | false 153 | true 154 | true 155 | ControlledByParent 156 | 157 | 158 | 159 | Text 160 | 161 | Charging Stations 162 | 163 | ControlledByParent 164 | Public 165 | 166 | -------------------------------------------------------------------------------- /force-app/main/default/objects/ChargingStation__c/fields/City__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | City__c 4 | false 5 | 6 | 255 7 | false 8 | false 9 | Text 10 | false 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/objects/ChargingStation__c/fields/Delivery_Waypoint__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Delivery_Waypoint__c 4 | false 5 | 6 | DeliveryWaypoint__c 7 | Charging Stations 8 | Charging_Stations 9 | 0 10 | false 11 | false 12 | MasterDetail 13 | false 14 | 15 | -------------------------------------------------------------------------------- /force-app/main/default/objects/ChargingStation__c/fields/Distance__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Distance__c 4 | false 5 | 6 | 18 7 | false 8 | 0 9 | false 10 | Number 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/main/default/objects/ChargingStation__c/fields/Location__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Location__c 4 | true 5 | false 6 | 7 | true 8 | 9 9 | false 10 | Location 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/objects/ChargingStation__c/fields/State__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | State__c 4 | false 5 | 6 | 255 7 | false 8 | false 9 | Text 10 | false 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/objects/ChargingStation__c/fields/Street_Address__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Street_Address__c 4 | false 5 | 6 | 255 7 | false 8 | false 9 | Text 10 | false 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/objects/ChargingStation__c/fields/Zip__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Zip__c 4 | false 5 | 6 | 18 7 | false 8 | 0 9 | false 10 | Number 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/main/default/objects/DeliveryPlan__c/DeliveryPlan__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | Accept 9 | Large 10 | Default 11 | 12 | 13 | Accept 14 | Small 15 | Default 16 | 17 | 18 | CancelEdit 19 | Default 20 | 21 | 22 | CancelEdit 23 | Large 24 | Default 25 | 26 | 27 | CancelEdit 28 | Small 29 | Default 30 | 31 | 32 | Clone 33 | Default 34 | 35 | 36 | Clone 37 | Large 38 | Default 39 | 40 | 41 | Clone 42 | Small 43 | Default 44 | 45 | 46 | Delete 47 | Default 48 | 49 | 50 | Delete 51 | Large 52 | Default 53 | 54 | 55 | Delete 56 | Small 57 | Default 58 | 59 | 60 | Edit 61 | Default 62 | 63 | 64 | Edit 65 | Large 66 | Default 67 | 68 | 69 | Edit 70 | Small 71 | Default 72 | 73 | 74 | List 75 | Default 76 | 77 | 78 | List 79 | Large 80 | Default 81 | 82 | 83 | List 84 | Small 85 | Default 86 | 87 | 88 | New 89 | Default 90 | 91 | 92 | New 93 | Large 94 | Default 95 | 96 | 97 | New 98 | Small 99 | Default 100 | 101 | 102 | SaveEdit 103 | Default 104 | 105 | 106 | SaveEdit 107 | Large 108 | Default 109 | 110 | 111 | SaveEdit 112 | Small 113 | Default 114 | 115 | 116 | Tab 117 | Default 118 | 119 | 120 | Tab 121 | Large 122 | Default 123 | 124 | 125 | Tab 126 | Small 127 | Default 128 | 129 | 130 | View 131 | Default 132 | 133 | 134 | View 135 | Large 136 | Default 137 | 138 | 139 | View 140 | Small 141 | Default 142 | 143 | false 144 | SYSTEM 145 | Deployed 146 | false 147 | false 148 | false 149 | false 150 | false 151 | false 152 | true 153 | false 154 | false 155 | ReadWrite 156 | 157 | 158 | 159 | Text 160 | 161 | Delivery Plans 162 | 163 | ReadWrite 164 | Public 165 | 166 | -------------------------------------------------------------------------------- /force-app/main/default/objects/DeliveryRoute__c/DeliveryRoute__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | CancelEdit 9 | Default 10 | 11 | 12 | Clone 13 | Default 14 | 15 | 16 | Delete 17 | Default 18 | 19 | 20 | Edit 21 | Default 22 | 23 | 24 | List 25 | Default 26 | 27 | 28 | New 29 | Default 30 | 31 | 32 | SaveEdit 33 | Default 34 | 35 | 36 | Tab 37 | Default 38 | 39 | 40 | View 41 | Default 42 | 43 | false 44 | SYSTEM 45 | Deployed 46 | false 47 | false 48 | false 49 | false 50 | false 51 | false 52 | true 53 | false 54 | false 55 | 56 | 57 | R-{00000000} 58 | 59 | AutoNumber 60 | 61 | Delivery Routes 62 | 63 | ControlledByParent 64 | Public 65 | 66 | -------------------------------------------------------------------------------- /force-app/main/default/objects/DeliveryRoute__c/fields/DeliveryPlan__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | DeliveryPlan__c 4 | false 5 | 6 | DeliveryPlan__c 7 | Delivery Routes 8 | DeliveryRoutes 9 | 0 10 | false 11 | false 12 | MasterDetail 13 | false 14 | 15 | -------------------------------------------------------------------------------- /force-app/main/default/objects/DeliveryRoute__c/fields/Vehicle__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vehicle__c 4 | SetNull 5 | false 6 | 7 | Vehicle__c 8 | Delivery Routes 9 | DeliveryRoutes 10 | false 11 | false 12 | Lookup 13 | 14 | -------------------------------------------------------------------------------- /force-app/main/default/objects/DeliveryWaypoint__c/DeliveryWaypoint__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | CancelEdit 9 | Default 10 | 11 | 12 | Clone 13 | Default 14 | 15 | 16 | Delete 17 | Default 18 | 19 | 20 | Edit 21 | Default 22 | 23 | 24 | List 25 | Default 26 | 27 | 28 | New 29 | Default 30 | 31 | 32 | SaveEdit 33 | Default 34 | 35 | 36 | Tab 37 | Default 38 | 39 | 40 | View 41 | Default 42 | 43 | false 44 | SYSTEM 45 | Deployed 46 | false 47 | false 48 | false 49 | false 50 | false 51 | false 52 | true 53 | false 54 | false 55 | 56 | 57 | DW-{00000000} 58 | 59 | AutoNumber 60 | 61 | Delivery Waypoints 62 | 63 | ControlledByParent 64 | Public 65 | 66 | -------------------------------------------------------------------------------- /force-app/main/default/objects/DeliveryWaypoint__c/fields/DeliveryPlan__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | DeliveryPlan__c 4 | false 5 | DeliveryRoute__r.DeliveryPlan__r.Name 6 | BlankAsZero 7 | 8 | false 9 | false 10 | Text 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/main/default/objects/DeliveryWaypoint__c/fields/DeliveryRoute__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | DeliveryRoute__c 4 | false 5 | 6 | DeliveryRoute__c 7 | Delivery Waypoints 8 | DeliveryWaypoints 9 | 0 10 | false 11 | false 12 | MasterDetail 13 | false 14 | 15 | -------------------------------------------------------------------------------- /force-app/main/default/objects/DeliveryWaypoint__c/fields/Number__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Number__c 4 | false 5 | 6 | 6 7 | false 8 | 0 9 | false 10 | Number 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/main/default/objects/DeliveryWaypoint__c/fields/Service__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Service__c 4 | SetNull 5 | false 6 | 7 | Service__c 8 | Delivery Waypoints 9 | Delivery_Waypoints 10 | false 11 | false 12 | Lookup 13 | 14 | -------------------------------------------------------------------------------- /force-app/main/default/objects/DeliveryWaypoint__c/fields/VehicleName__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | VehicleName__c 4 | false 5 | DeliveryRoute__r.Vehicle__r.Name 6 | BlankAsZero 7 | 8 | false 9 | false 10 | Text 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/main/default/objects/DeliveryWaypoint__c/fields/Vehicle_Vin__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vehicle_Vin__c 4 | false 5 | DeliveryRoute__r.Vehicle__r.Vin__c 6 | BlankAsZero 7 | 8 | false 9 | false 10 | Text 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/main/default/objects/JobCompleted__e/JobCompleted__e.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Deployed 4 | HighVolume 5 | 6 | Jobs Completed 7 | PublishAfterCommit 8 | 9 | -------------------------------------------------------------------------------- /force-app/main/default/objects/JobCompleted__e/fields/DeliveryPlan_Id__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | DeliveryPlan_Id__c 4 | false 5 | false 6 | false 7 | false 8 | 9 | 255 10 | true 11 | Text 12 | false 13 | 14 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Service__c/Service__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | CancelEdit 9 | Default 10 | 11 | 12 | Clone 13 | Default 14 | 15 | 16 | Delete 17 | Default 18 | 19 | 20 | Edit 21 | Default 22 | 23 | 24 | List 25 | Default 26 | 27 | 28 | New 29 | Default 30 | 31 | 32 | SaveEdit 33 | Default 34 | 35 | 36 | Tab 37 | Default 38 | 39 | 40 | View 41 | Default 42 | 43 | false 44 | SYSTEM 45 | Deployed 46 | false 47 | false 48 | false 49 | false 50 | false 51 | false 52 | true 53 | false 54 | false 55 | 56 | 57 | 58 | Text 59 | 60 | Services 61 | 62 | ReadWrite 63 | Public 64 | 65 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Service__c/fields/Account__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Account__c 4 | SetNull 5 | false 6 | 7 | Account 8 | Services 9 | Services 10 | false 11 | false 12 | Lookup 13 | 14 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Service__c/fields/Location__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Location__c 4 | true 5 | false 6 | 7 | true 8 | 7 9 | false 10 | Location 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Service__c/listViews/All.listView-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | All 4 | NAME 5 | Location__Latitude__s 6 | Location__Longitude__s 7 | Everything 8 | 9 | 10 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Vehicle__c/Vehicle__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | CancelEdit 9 | Default 10 | 11 | 12 | Clone 13 | Default 14 | 15 | 16 | Delete 17 | Default 18 | 19 | 20 | Edit 21 | Default 22 | 23 | 24 | List 25 | Default 26 | 27 | 28 | New 29 | Default 30 | 31 | 32 | SaveEdit 33 | Default 34 | 35 | 36 | Tab 37 | Default 38 | 39 | 40 | View 41 | Default 42 | 43 | false 44 | SYSTEM 45 | Deployed 46 | false 47 | false 48 | false 49 | false 50 | false 51 | false 52 | true 53 | false 54 | false 55 | 56 | 57 | 58 | Text 59 | 60 | Vehicles 61 | 62 | ReadWrite 63 | Public 64 | 65 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Vehicle__c/fields/Location__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Location__c 4 | true 5 | false 6 | 7 | true 8 | 9 9 | false 10 | Location 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Vehicle__c/fields/Vin__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vin__c 4 | false 5 | 6 | 18 7 | true 8 | false 9 | Text 10 | false 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Vehicle__c/listViews/All.listView-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | All 4 | NAME 5 | Location__Latitude__s 6 | Location__Longitude__s 7 | Everything 8 | 9 | 10 | -------------------------------------------------------------------------------- /force-app/main/default/permissionsets/Functions.permissionset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Permissions for Salesforce Functions to access Salesforce org data 4 | 5 | true 6 | Account.AccountNumber 7 | true 8 | 9 | 10 | true 11 | Account.AccountSource 12 | true 13 | 14 | 15 | true 16 | Account.AnnualRevenue 17 | true 18 | 19 | 20 | true 21 | Account.BillingAddress 22 | true 23 | 24 | 25 | true 26 | Account.DandbCompanyId 27 | true 28 | 29 | 30 | true 31 | Account.Description 32 | true 33 | 34 | 35 | true 36 | Account.DunsNumber 37 | true 38 | 39 | 40 | true 41 | Account.Fax 42 | true 43 | 44 | 45 | true 46 | Account.Industry 47 | true 48 | 49 | 50 | true 51 | Account.Jigsaw 52 | true 53 | 54 | 55 | true 56 | Account.NaicsCode 57 | true 58 | 59 | 60 | true 61 | Account.NaicsDesc 62 | true 63 | 64 | 65 | true 66 | Account.NumberOfEmployees 67 | true 68 | 69 | 70 | true 71 | Account.OperatingHoursId 72 | true 73 | 74 | 75 | true 76 | Account.Ownership 77 | true 78 | 79 | 80 | true 81 | Account.ParentId 82 | true 83 | 84 | 85 | true 86 | Account.Phone 87 | true 88 | 89 | 90 | true 91 | Account.Rating 92 | true 93 | 94 | 95 | true 96 | Account.ShippingAddress 97 | true 98 | 99 | 100 | true 101 | Account.Sic 102 | true 103 | 104 | 105 | true 106 | Account.SicDesc 107 | true 108 | 109 | 110 | true 111 | Account.Site 112 | true 113 | 114 | 115 | true 116 | Account.TickerSymbol 117 | true 118 | 119 | 120 | true 121 | Account.Tradestyle 122 | true 123 | 124 | 125 | true 126 | Account.Type 127 | true 128 | 129 | 130 | true 131 | Account.Website 132 | true 133 | 134 | 135 | true 136 | Account.YearStarted 137 | true 138 | 139 | 140 | true 141 | ChargingStation__c.City__c 142 | true 143 | 144 | 145 | true 146 | ChargingStation__c.Distance__c 147 | true 148 | 149 | 150 | true 151 | ChargingStation__c.State__c 152 | true 153 | 154 | 155 | true 156 | ChargingStation__c.Street_Address__c 157 | true 158 | 159 | 160 | true 161 | ChargingStation__c.Zip__c 162 | true 163 | 164 | 165 | true 166 | DeliveryRoute__c.Vehicle__c 167 | true 168 | 169 | 170 | false 171 | DeliveryWaypoint__c.DeliveryPlan__c 172 | true 173 | 174 | 175 | true 176 | DeliveryWaypoint__c.Number__c 177 | true 178 | 179 | 180 | true 181 | DeliveryWaypoint__c.Service__c 182 | true 183 | 184 | 185 | false 186 | DeliveryWaypoint__c.VehicleName__c 187 | true 188 | 189 | 190 | false 191 | DeliveryWaypoint__c.Vehicle_Vin__c 192 | true 193 | 194 | 195 | true 196 | Service__c.Account__c 197 | true 198 | 199 | true 200 | 201 | 202 | true 203 | true 204 | true 205 | true 206 | false 207 | Account 208 | false 209 | 210 | 211 | true 212 | true 213 | true 214 | true 215 | true 216 | ChargingStation__c 217 | true 218 | 219 | 220 | true 221 | true 222 | true 223 | true 224 | true 225 | DeliveryPlan__c 226 | true 227 | 228 | 229 | true 230 | true 231 | true 232 | true 233 | true 234 | DeliveryRoute__c 235 | true 236 | 237 | 238 | true 239 | true 240 | true 241 | true 242 | true 243 | DeliveryWaypoint__c 244 | true 245 | 246 | 247 | true 248 | false 249 | false 250 | true 251 | false 252 | JobCompleted__e 253 | false 254 | 255 | 256 | true 257 | true 258 | true 259 | true 260 | true 261 | Service__c 262 | true 263 | 264 | 265 | true 266 | true 267 | true 268 | true 269 | true 270 | Vehicle__c 271 | true 272 | 273 | 274 | DeliveryPlan__c 275 | Visible 276 | 277 | 278 | Service__c 279 | Visible 280 | 281 | 282 | Vehicle__c 283 | Visible 284 | 285 | 286 | -------------------------------------------------------------------------------- /force-app/main/default/permissionsets/RoutePlanner.permissionset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RoutePlanner 5 | true 6 | 7 | 8 | true 9 | Account.ShippingAddress 10 | true 11 | 12 | 13 | true 14 | ChargingStation__c.City__c 15 | true 16 | 17 | 18 | true 19 | ChargingStation__c.Distance__c 20 | true 21 | 22 | 23 | true 24 | ChargingStation__c.State__c 25 | true 26 | 27 | 28 | true 29 | ChargingStation__c.Street_Address__c 30 | true 31 | 32 | 33 | true 34 | ChargingStation__c.Zip__c 35 | true 36 | 37 | 38 | true 39 | DeliveryRoute__c.Vehicle__c 40 | true 41 | 42 | 43 | false 44 | DeliveryWaypoint__c.DeliveryPlan__c 45 | true 46 | 47 | 48 | true 49 | DeliveryWaypoint__c.Number__c 50 | true 51 | 52 | 53 | true 54 | DeliveryWaypoint__c.Service__c 55 | true 56 | 57 | 58 | false 59 | DeliveryWaypoint__c.VehicleName__c 60 | true 61 | 62 | 63 | false 64 | DeliveryWaypoint__c.Vehicle_Vin__c 65 | true 66 | 67 | 68 | true 69 | Service__c.Account__c 70 | true 71 | 72 | false 73 | 74 | 75 | true 76 | true 77 | true 78 | true 79 | false 80 | Account 81 | false 82 | 83 | 84 | true 85 | true 86 | true 87 | true 88 | false 89 | ChargingStation__c 90 | false 91 | 92 | 93 | true 94 | true 95 | true 96 | true 97 | false 98 | DeliveryPlan__c 99 | false 100 | 101 | 102 | true 103 | true 104 | true 105 | true 106 | false 107 | DeliveryRoute__c 108 | false 109 | 110 | 111 | true 112 | true 113 | true 114 | true 115 | false 116 | DeliveryWaypoint__c 117 | false 118 | 119 | 120 | true 121 | false 122 | false 123 | true 124 | false 125 | JobCompleted__e 126 | false 127 | 128 | 129 | true 130 | true 131 | true 132 | true 133 | false 134 | Service__c 135 | false 136 | 137 | 138 | true 139 | true 140 | true 141 | true 142 | false 143 | Vehicle__c 144 | false 145 | 146 | 147 | DeliveryPlan__c 148 | Visible 149 | 150 | 151 | Service__c 152 | Visible 153 | 154 | 155 | Vehicle__c 156 | Visible 157 | 158 | 159 | -------------------------------------------------------------------------------- /force-app/main/default/permissionsets/sfdcInternalInt__sfdc_evergreen_event_source.permissionset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Permissions for Salesforce Functions' system-level, internal access to associated Salesforce org data 4 | true 5 | 6 | Cloud Integration User 7 | 8 | false 9 | true 10 | true 11 | true 12 | true 13 | FunctionInvocationRequest 14 | true 15 | 16 | 17 | true 18 | ViewAllUsers 19 | 20 | 21 | true 22 | ViewRoles 23 | 24 | 25 | true 26 | ViewSetup 27 | 28 | 29 | -------------------------------------------------------------------------------- /force-app/main/default/platformEventChannelMembers/ChangeEvents_DeliveryPlan_ChangeEvent.platformEventChannelMember-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ChangeEvents 4 | DeliveryPlan__ChangeEvent 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/tabs/DeliveryPlan__c.tab-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | Custom62: Chalkboard 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/tabs/Service__c.tab-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | Custom13: Box 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/tabs/Vehicle__c.tab-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | Custom31: Car 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/triggers/DeliveryPlanTrigger.trigger: -------------------------------------------------------------------------------- 1 | trigger DeliveryPlanTrigger on DeliveryPlan__ChangeEvent(after insert) { 2 | new DeliveryPlanTriggerHandler(Trigger.New); 3 | } 4 | -------------------------------------------------------------------------------- /force-app/main/default/triggers/DeliveryPlanTrigger.trigger-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /functions/chargingstations/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module", 4 | "ecmaVersion": 2020 5 | }, 6 | "extends": "eslint:recommended", 7 | "env": { 8 | "node": true, 9 | "es2020": true, 10 | "mocha": true 11 | }, 12 | "rules": {} 13 | } 14 | -------------------------------------------------------------------------------- /functions/chargingstations/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "./**/*.test.js", 3 | "recursive": true 4 | } 5 | -------------------------------------------------------------------------------- /functions/chargingstations/README.md: -------------------------------------------------------------------------------- 1 | # Chargingstations Function 2 | 3 | 4 | -------------------------------------------------------------------------------- /functions/chargingstations/data/payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "waypointId": "a021D000007IRunQAG", 3 | "latitude": "47.625138", 4 | "longitude": "-122.325959", 5 | "results": 2, 6 | "jobId": "0b34f931-be8d-435d-bb55-89902cb5070f" 7 | } 8 | -------------------------------------------------------------------------------- /functions/chargingstations/index.js: -------------------------------------------------------------------------------- 1 | import pg from "pg"; 2 | import * as mqtt from "mqtt"; 3 | import { createClient } from "redis"; 4 | import { request } from "undici"; 5 | const { Client } = pg; 6 | 7 | /** 8 | * ChargingStations 9 | * 10 | * Returns a list of charging stations from a given location 11 | * 12 | * The exported method is the entry point for your code when the function is invoked. 13 | * 14 | * Following parameters are pre-configured and provided to your function on execution: 15 | * @param event: represents the data associated with the occurrence of an event, and 16 | * supporting metadata about the source of that occurrence. 17 | * @param context: represents the connection to Functions and your Salesforce org. 18 | * @param logger: logging handler used to capture application logs and trace specifically 19 | * to a given execution of a function. 20 | */ 21 | export default async function (event, context, logger) { 22 | logger.info( 23 | `Invoking ChargingStations with payload ${JSON.stringify(event.data || {})}` 24 | ); 25 | 26 | const { 27 | jobId, // Used to track the status of the job 28 | waypointId, // Used to store Charging Stations back to Salesforce 29 | distance = 1, 30 | results = 10 31 | } = event.data; 32 | 33 | if (!waypointId) { 34 | throw new Error(`waypointId is required`); 35 | } 36 | 37 | if (jobId && !(await canRunJob(jobId))) { 38 | throw new Error( 39 | `Job ${jobId} already completed or doesn't exist, please schedule a new job` 40 | ); 41 | } 42 | 43 | // Get the Delivery Plan Id, Location, and Vin From Salesforce 44 | const { records } = await context.org.dataApi.query(` 45 | SELECT 46 | Vehicle_Vin__c, 47 | Service__r.Location__Latitude__s, 48 | Service__r.Location__Longitude__s, 49 | DeliveryRoute__r.DeliveryPlan__r.Id 50 | FROM DeliveryWaypoint__c WHERE Id = '${waypointId}' 51 | LIMIT 1 52 | `); 53 | 54 | if (!records || records.length === 0) { 55 | throw new Error(`No waypoint found with Id ${waypointId}`); 56 | } 57 | 58 | // Extract values from Record 59 | const [waypoint] = records; 60 | const vin = waypoint?.fields?.vehicle_vin__c; 61 | const deliveryPlanId = 62 | waypoint?.fields?.deliveryroute__r?.DeliveryPlan__r?.Id; 63 | const latitude = waypoint?.fields?.service__r?.Location__Latitude__s; 64 | const longitude = waypoint?.fields?.service__r?.Location__Longitude__s; 65 | 66 | if (!vin || !deliveryPlanId || !latitude || !longitude) { 67 | throw new Error(`Missing required fields from Delivery Waypoint`); 68 | } 69 | 70 | // Connect to PostgreSQL Database 71 | const pgClient = await pgConnect(); 72 | // Connect to MQTT Message Broker 73 | const mqttClient = await mqttConnect(); 74 | 75 | // Get closest charging stations from the given location 76 | const chargingStationsQuery = ` 77 | SELECT station_name, street_address, city, state, zip, latitude, longitude, 78 | ROUND((ST_Distance(location, ref_location) * 0.000621371192)::numeric, 3) AS distance 79 | FROM charging_stations CROSS JOIN (SELECT ST_MakePoint($1, $2)::geography AS ref_location) AS ref 80 | WHERE ST_DWithin(location, ref_location, $3) 81 | ORDER BY ST_Distance(location, ref_location) LIMIT $4 82 | `; 83 | 84 | const { rows: stations } = await pgClient.query(chargingStationsQuery, [ 85 | longitude, 86 | latitude, 87 | distance * 1609, 88 | results 89 | ]); 90 | 91 | // Store Charging Stations into Salesforce using UoW Pattern 92 | const uow = context.org.dataApi.newUnitOfWork(); 93 | for (const station of stations) { 94 | uow.registerCreate({ 95 | type: "ChargingStation__c", 96 | fields: { 97 | Delivery_Waypoint__c: waypointId, 98 | Name: station.station_name, 99 | Street_Address__c: station.street_address, 100 | City__c: station.city, 101 | State__c: station.state, 102 | Zip__c: station.zip, 103 | Location__Latitude__s: station.latitude, 104 | Location__Longitude__s: station.longitude, 105 | Distance__c: station.distance 106 | } 107 | }); 108 | } 109 | await context.org.dataApi.commitUnitOfWork(uow); 110 | 111 | // Build Response Object 112 | const response = { 113 | deliveryPlanId, 114 | waypointId, 115 | stations 116 | }; 117 | 118 | // Send Charging Stations to the Vehicle 119 | await sendChargingStations(mqttClient, { vin, stations }); 120 | 121 | // Disconnect from Database and MQTT 122 | pgClient.end(); 123 | mqttClient.end(); 124 | 125 | // Register Job Progress 126 | if (jobId) { 127 | const status = await registerJob(jobId); 128 | response.job = { 129 | jobId, 130 | status 131 | }; 132 | 133 | if (status === "completed") { 134 | logger.info(`Job ${jobId} completed`); 135 | // Sends Platform Event to Salesforce to mark the job as completed 136 | await sendPlatformEvent({ context, logger }, { deliveryPlanId }); 137 | } 138 | } 139 | 140 | return response; 141 | } 142 | 143 | // Connect to PostgreSQL Database 144 | async function pgConnect() { 145 | const DATABASE_URL = process.env.DATABASE_URL; 146 | if (!DATABASE_URL) { 147 | throw new Error("DATABASE_URL is not set"); 148 | } 149 | 150 | const client = new Client({ 151 | connectionString: DATABASE_URL, 152 | ssl: { 153 | rejectUnauthorized: false 154 | } 155 | }); 156 | 157 | await client.connect(); 158 | return client; 159 | } 160 | 161 | // Connect to MQTT Message Broker on Heroku 162 | async function mqttConnect() { 163 | return new Promise((resolve, reject) => { 164 | const MQTT_URL = process.env.MQTT_URL; 165 | if (!MQTT_URL) { 166 | reject(new Error("MQTT_URL is not set")); 167 | } 168 | 169 | const mqttClient = mqtt.connect(MQTT_URL); 170 | mqttClient.on("connect", () => { 171 | resolve(mqttClient); 172 | }); 173 | 174 | mqttClient.on("error", (err) => { 175 | mqttClient.end(); 176 | reject(err); 177 | }); 178 | }); 179 | } 180 | 181 | // Send Charging Stations to Vehicle using MQTT Message Broker 182 | async function sendChargingStations(mqttClient, { vin, stations }) { 183 | return new Promise((resolve, reject) => { 184 | mqttClient.subscribe(`/chargingstations/${vin}`, (err) => { 185 | if (err) return reject(err); 186 | 187 | mqttClient.publish( 188 | `/chargingstations/${vin}`, 189 | JSON.stringify(stations), 190 | (err) => { 191 | if (err) return reject(err); 192 | resolve(); 193 | } 194 | ); 195 | }); 196 | }); 197 | } 198 | 199 | // Connect to Redis Database 200 | async function redisConnect() { 201 | const REDIS_URL = process.env.REDIS_URL; 202 | if (!REDIS_URL) { 203 | throw new Error(`REDIS_URL is not set`); 204 | } 205 | // Connect to Redis 206 | const redisClient = createClient({ 207 | url: REDIS_URL, 208 | socket: { 209 | tls: true, 210 | rejectUnauthorized: false 211 | } 212 | }); 213 | await redisClient.connect(); 214 | return redisClient; 215 | } 216 | 217 | // Verify if a job can be run 218 | async function canRunJob(jobId) { 219 | let canRun = false; 220 | const redisClient = await redisConnect(); 221 | const exists = await redisClient.exists(`job:${jobId}`); 222 | if (exists) { 223 | const status = await redisClient.hGet(`job:${jobId}`, "status"); 224 | canRun = status !== "completed"; 225 | } 226 | await redisClient.quit(); 227 | return canRun; 228 | } 229 | 230 | // Register Job Progress 231 | async function registerJob(jobId) { 232 | const redisClient = await redisConnect(); 233 | let status = "running"; 234 | const jobs = await redisClient.hGet(`job:${jobId}`, "jobs"); 235 | const completed = await redisClient.hIncrBy(`job:${jobId}`, "completed", 1); 236 | if (+jobs === +completed) { 237 | await redisClient.hSet(`job:${jobId}`, "status", "completed"); 238 | status = "completed"; 239 | } 240 | await redisClient.quit(); 241 | return status; 242 | } 243 | 244 | // Sends Platform Event to Salesforce 245 | async function sendPlatformEvent({ context, logger }, { deliveryPlanId }) { 246 | /** 247 | * The following example demonstrates how to perform a REST API request to Salesforce 248 | * using an http client library. 249 | * 250 | * For that, we need to build the API endpoint by getting the baseUrl and apiVersion from `context.org` 251 | * and the `accessToken` from `context.org.dataApi`. 252 | * 253 | * We can also simplify this specific example by using the `context.org.dataApi` SDK to create an object. 254 | * 255 | * await context.org.dataApi.create({ 256 | * type: "Job_Completed__e", 257 | * fields: { 258 | * DeliveryPlan_Id__c: deliveryPlanId 259 | * } 260 | * }); 261 | * 262 | */ 263 | const { baseUrl, apiVersion } = context.org; 264 | const accessToken = context.org.dataApi.accessToken; 265 | const url = `${baseUrl}/services/data/v${apiVersion}/sobjects/JobCompleted__e/`; 266 | const { body, statusCode } = await request(url, { 267 | method: "POST", 268 | headers: { 269 | Authorization: `Bearer ${accessToken}`, 270 | "Content-Type": "application/json" 271 | }, 272 | body: JSON.stringify({ 273 | DeliveryPlan_Id__c: deliveryPlanId 274 | }) 275 | }); 276 | logger.info( 277 | `Platform Event to Salesforce: ${url} - Status Code: ${statusCode} - body: ${JSON.stringify( 278 | await body.json() 279 | )}` 280 | ); 281 | } 282 | -------------------------------------------------------------------------------- /functions/chargingstations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chargingstations-function", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "author": "TODO", 6 | "description": "TODO", 7 | "license": "UNLICENSED", 8 | "main": "index.js", 9 | "repository": { 10 | "type": "git" 11 | }, 12 | "engines": { 13 | "node": "^16.9" 14 | }, 15 | "scripts": { 16 | "lint": "eslint . --ext .js --config .eslintrc", 17 | "test": "mocha" 18 | }, 19 | "devDependencies": { 20 | "chai": "^4.3.4", 21 | "eslint": "^8.8.0", 22 | "mocha": "^9.1.3", 23 | "sinon": "^13.0.1" 24 | }, 25 | "dependencies": { 26 | "mqtt": "^4.3.7", 27 | "pg": "^8.7.3", 28 | "redis": "^4.0.6", 29 | "undici": "^5.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /functions/chargingstations/project.toml: -------------------------------------------------------------------------------- 1 | [_] 2 | schema-version = "0.2" 3 | 4 | [com.salesforce] 5 | schema-version = "0.1" 6 | id = "chargingstations" 7 | description = "List charging stations given a geographic location" 8 | type = "function" 9 | salesforce-api-version = "54.0" 10 | -------------------------------------------------------------------------------- /functions/chargingstations/test/index.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { createSandbox } from "sinon"; 3 | 4 | import execute from "../index.js"; 5 | 6 | /** 7 | * Chargingstations unit tests. 8 | */ 9 | 10 | describe("Unit Tests", () => { 11 | let sandbox; 12 | let mockContext; 13 | let mockLogger; 14 | let accounts; 15 | 16 | beforeEach(() => { 17 | mockContext = { 18 | org: { 19 | dataApi: { query: () => {} } 20 | }, 21 | logger: { info: () => {} } 22 | }; 23 | 24 | mockLogger = mockContext.logger; 25 | sandbox = createSandbox(); 26 | 27 | sandbox.stub(mockContext.org.dataApi, "query"); 28 | sandbox.stub(mockLogger, "info"); 29 | 30 | accounts = { 31 | totalSize: 3, 32 | done: true, 33 | records: [ 34 | { 35 | type: "Account", 36 | fields: { Name: "Global Media" } 37 | }, 38 | { 39 | type: "Account", 40 | fields: { Name: "Acme" } 41 | }, 42 | { 43 | type: "Account", 44 | fields: { Name: "salesforce.com" } 45 | } 46 | ] 47 | }; 48 | 49 | mockContext.org.dataApi.query.callsFake(() => { 50 | return Promise.resolve(accounts); 51 | }); 52 | }); 53 | 54 | afterEach(() => { 55 | sandbox.restore(); 56 | }); 57 | 58 | it("Invoke Chargingstations", async () => { 59 | const results = await execute({ data: {} }, mockContext, mockLogger); 60 | 61 | expect(mockContext.org.dataApi.query.callCount).to.be.eql(1); 62 | expect(mockLogger.info.callCount).to.be.eql(2); 63 | expect(results).to.be.not.undefined; 64 | expect(results).has.property("totalSize"); 65 | expect(results.totalSize).to.be.eql(accounts.totalSize); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /functions/jobmanager/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module", 4 | "ecmaVersion": 2017 5 | }, 6 | "env": { 7 | "es6": true 8 | }, 9 | "rules": { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /functions/jobmanager/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "./**/*.test.js", 3 | "recursive": true 4 | } 5 | -------------------------------------------------------------------------------- /functions/jobmanager/README.md: -------------------------------------------------------------------------------- 1 | # Jobmanager Function 2 | 3 | 4 | -------------------------------------------------------------------------------- /functions/jobmanager/index.js: -------------------------------------------------------------------------------- 1 | import { createClient } from "redis"; 2 | 3 | /** 4 | * JobManager 5 | * 6 | * It orchestrates the execution of jobs. 7 | * 8 | * The exported method is the entry point for your code when the function is invoked. 9 | * 10 | * Following parameters are pre-configured and provided to your function on execution: 11 | * @param event: represents the data associated with the occurrence of an event, and 12 | * supporting metadata about the source of that occurrence. 13 | * @param context: represents the connection to Functions and your Salesforce org. 14 | * @param logger: logging handler used to capture application logs and trace specifically 15 | * to a given execution of a function. 16 | */ 17 | export default async function (event, context, logger) { 18 | const REDIS_URL = process.env.REDIS_URL; 19 | if (!REDIS_URL) { 20 | throw new Error(`REDIS_URL environment variable is required`); 21 | } 22 | logger.info( 23 | `Invoking JobManager with payload ${JSON.stringify(event.data || {})}` 24 | ); 25 | 26 | // Get Request ID to keep track of Jobs 27 | const jobId = event.id; 28 | // Get the number of jobs to execute 29 | const jobs = event.data.jobs; 30 | if (!jobs) { 31 | throw new Error(`Please specify the number of jobs to execute`); 32 | } 33 | 34 | // Connect to Redis 35 | const client = createClient({ 36 | url: REDIS_URL, 37 | socket: { 38 | tls: true, 39 | rejectUnauthorized: false 40 | } 41 | }); 42 | await client.connect(); 43 | 44 | // Setup Job Information 45 | await client.hSet(`job:${jobId}`, "status", "running"); 46 | await client.hSet(`job:${jobId}`, "jobs", jobs); 47 | await client.hSet(`job:${jobId}`, "completed", 0); 48 | await client.quit(); 49 | 50 | const response = { 51 | jobId, 52 | status: "running" 53 | }; 54 | return response; 55 | } 56 | -------------------------------------------------------------------------------- /functions/jobmanager/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jobmanager-function", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "author": "TODO", 6 | "description": "TODO", 7 | "license": "UNLICENSED", 8 | "main": "index.js", 9 | "repository": { 10 | "type": "git" 11 | }, 12 | "engines": { 13 | "node": "^16.9" 14 | }, 15 | "scripts": { 16 | "lint": "eslint . --ext .js --config .eslintrc", 17 | "test": "mocha" 18 | }, 19 | "devDependencies": { 20 | "chai": "^4.3.4", 21 | "eslint": "^6.8.0", 22 | "mocha": "^8.4.0", 23 | "sinon": "^10.0.0" 24 | }, 25 | "dependencies": { 26 | "redis": "^4.0.6" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /functions/jobmanager/project.toml: -------------------------------------------------------------------------------- 1 | [_] 2 | schema-version = "0.2" 3 | 4 | [com.salesforce] 5 | schema-version = "0.1" 6 | id = "jobmanager" 7 | description = "Orchestrates the execution of asynchronous jobs." 8 | type = "function" 9 | salesforce-api-version = "54.0" 10 | -------------------------------------------------------------------------------- /functions/jobmanager/test/index.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import createSandbox from "sinon/lib/sinon/create-sandbox.js"; 3 | 4 | import execute from "../index.js"; 5 | 6 | /** 7 | * Jobmanager unit tests. 8 | */ 9 | 10 | describe("Unit Tests", () => { 11 | let sandbox; 12 | let mockContext; 13 | let mockLogger; 14 | let accounts; 15 | 16 | beforeEach(() => { 17 | mockContext = { 18 | org: { 19 | dataApi: { query: () => {} } 20 | }, 21 | logger: { info: () => {} } 22 | }; 23 | 24 | mockLogger = mockContext.logger; 25 | sandbox = createSandbox(); 26 | 27 | sandbox.stub(mockContext.org.dataApi, "query"); 28 | sandbox.stub(mockLogger, "info"); 29 | 30 | accounts = { 31 | totalSize: 3, 32 | done: true, 33 | records: [ 34 | { 35 | type: "Account", 36 | fields: { Name: "Global Media" } 37 | }, 38 | { 39 | type: "Account", 40 | fields: { Name: "Acme" } 41 | }, 42 | { 43 | type: "Account", 44 | fields: { Name: "salesforce.com" } 45 | } 46 | ] 47 | }; 48 | 49 | mockContext.org.dataApi.query.callsFake(() => { 50 | return Promise.resolve(accounts); 51 | }); 52 | }); 53 | 54 | afterEach(() => { 55 | sandbox.restore(); 56 | }); 57 | 58 | it("Invoke Jobmanager", async () => { 59 | const results = await execute({ data: {} }, mockContext, mockLogger); 60 | 61 | expect(mockContext.org.dataApi.query.callCount).to.be.eql(1); 62 | expect(mockLogger.info.callCount).to.be.eql(2); 63 | expect(results).to.be.not.undefined; 64 | expect(results).has.property("totalSize"); 65 | expect(results.totalSize).to.be.eql(accounts.totalSize); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /functions/routeplanner/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /functions/routeplanner/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /functions/routeplanner/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heroku-examples/tdx-advanced-functions/b98df851a6963f189dc52f9ebba86cae743ee393/functions/routeplanner/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /functions/routeplanner/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /functions/routeplanner/data/sample-payload.json: -------------------------------------------------------------------------------- 1 | { "accountName": "Frisco Deliveries" } 2 | -------------------------------------------------------------------------------- /functions/routeplanner/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /functions/routeplanner/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /functions/routeplanner/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 4.0.0 8 | 9 | com.salesforce.functions.demo 10 | routeplanner 11 | 1.0-SNAPSHOT 12 | 13 | routeplanner 14 | 15 | UTF-8 16 | 11 17 | 18 | 19 | 20 | 21 | com.salesforce.functions 22 | sf-fx-sdk-java 23 | 1.0.0 24 | 25 | 26 | org.slf4j 27 | slf4j-api 28 | 1.7.30 29 | 30 | 31 | junit 32 | junit 33 | 4.13.1 34 | test 35 | 36 | 37 | org.mockito 38 | mockito-core 39 | 3.6.28 40 | test 41 | 42 | 43 | org.hamcrest 44 | hamcrest 45 | 2.2 46 | test 47 | 48 | 49 | com.spotify 50 | hamcrest-pojo 51 | 1.2.0 52 | test 53 | 54 | 55 | com.graphhopper 56 | jsprit-core 57 | 1.8 58 | 59 | 60 | com.graphhopper 61 | jsprit-io 62 | 1.8 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | maven-clean-plugin 71 | 3.1.0 72 | 73 | 74 | maven-resources-plugin 75 | 3.0.2 76 | 77 | 78 | maven-compiler-plugin 79 | 3.8.0 80 | 81 | 82 | maven-surefire-plugin 83 | 2.22.1 84 | 85 | 86 | maven-jar-plugin 87 | 3.0.2 88 | 89 | 90 | maven-install-plugin 91 | 2.5.2 92 | 93 | 94 | maven-deploy-plugin 95 | 2.8.2 96 | 97 | 98 | maven-site-plugin 99 | 3.7.1 100 | 101 | 102 | maven-project-info-reports-plugin 103 | 3.0.0 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /functions/routeplanner/project.toml: -------------------------------------------------------------------------------- 1 | [_] 2 | schema-version = "0.2" 3 | 4 | [com.salesforce] 5 | schema-version = "0.1" 6 | id = "routeplanner" 7 | description = "A Function that uses the Travel Salesman Problem to calculate deliveries" 8 | type = "function" 9 | salesforce-api-version = "54.0" -------------------------------------------------------------------------------- /functions/routeplanner/src/main/java/com/salesforce/functions/demo/FunctionInput.java: -------------------------------------------------------------------------------- 1 | package com.salesforce.functions.demo; 2 | 3 | public class FunctionInput { 4 | private String accountName; 5 | 6 | public String getAccountName() { 7 | return accountName; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /functions/routeplanner/src/main/java/com/salesforce/functions/demo/FunctionOutput.java: -------------------------------------------------------------------------------- 1 | package com.salesforce.functions.demo; 2 | 3 | import com.salesforce.functions.jvm.sdk.data.RecordModificationResult; 4 | import com.salesforce.functions.jvm.sdk.data.ReferenceId; 5 | import java.util.Map; 6 | 7 | public class FunctionOutput { 8 | public final String message; 9 | public final Map result; 10 | 11 | public FunctionOutput(String message, Map result) { 12 | this.message = message; 13 | this.result = result; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /functions/routeplanner/src/main/java/com/salesforce/functions/demo/RoutePlannerFunction.java: -------------------------------------------------------------------------------- 1 | package com.salesforce.functions.demo; 2 | 3 | import com.graphhopper.jsprit.core.algorithm.VehicleRoutingAlgorithm; 4 | import com.graphhopper.jsprit.core.algorithm.box.Jsprit; 5 | import com.graphhopper.jsprit.core.algorithm.selector.SelectBest; 6 | import com.graphhopper.jsprit.core.algorithm.state.StateManager; 7 | import com.graphhopper.jsprit.core.problem.Location; 8 | import com.graphhopper.jsprit.core.problem.VehicleRoutingProblem; 9 | import com.graphhopper.jsprit.core.problem.constraint.ConstraintManager; 10 | import com.graphhopper.jsprit.core.problem.constraint.ServiceDeliveriesFirstConstraint; 11 | import com.graphhopper.jsprit.core.problem.job.Service; 12 | import com.graphhopper.jsprit.core.problem.solution.VehicleRoutingProblemSolution; 13 | import com.graphhopper.jsprit.core.problem.solution.route.VehicleRoute; 14 | import com.graphhopper.jsprit.core.problem.solution.route.activity.PickupService; 15 | import com.graphhopper.jsprit.core.problem.solution.route.activity.TourActivity; 16 | import com.graphhopper.jsprit.core.problem.vehicle.VehicleImpl; 17 | import com.graphhopper.jsprit.core.problem.vehicle.VehicleImpl.Builder; 18 | import com.graphhopper.jsprit.core.problem.vehicle.VehicleType; 19 | import com.graphhopper.jsprit.core.problem.vehicle.VehicleTypeImpl; 20 | import com.graphhopper.jsprit.core.reporting.SolutionPrinter; 21 | import com.salesforce.functions.jvm.sdk.Context; 22 | import com.salesforce.functions.jvm.sdk.InvocationEvent; 23 | import com.salesforce.functions.jvm.sdk.SalesforceFunction; 24 | import com.salesforce.functions.jvm.sdk.data.DataApi; 25 | import com.salesforce.functions.jvm.sdk.data.Record; 26 | import com.salesforce.functions.jvm.sdk.data.RecordModificationResult; 27 | import com.salesforce.functions.jvm.sdk.data.RecordQueryResult; 28 | import com.salesforce.functions.jvm.sdk.data.ReferenceId; 29 | import com.salesforce.functions.jvm.sdk.data.builder.UnitOfWorkBuilder; 30 | import com.salesforce.functions.jvm.sdk.data.error.DataApiException; 31 | import java.time.LocalDateTime; 32 | import java.time.format.DateTimeFormatter; 33 | import java.util.Collection; 34 | import java.util.List; 35 | import java.util.Map; 36 | import org.slf4j.Logger; 37 | import org.slf4j.LoggerFactory; 38 | 39 | /** Receives an account and calculates the best delivery route for different provided services */ 40 | public class RoutePlannerFunction implements SalesforceFunction { 41 | private static final Logger LOGGER = LoggerFactory.getLogger(RoutePlannerFunction.class); 42 | 43 | @Override 44 | public FunctionOutput apply(InvocationEvent event, Context context) 45 | throws Exception { 46 | 47 | String accountName = event.getData().getAccountName(); 48 | DataApi dataApi = context.getOrg().get().getDataApi(); 49 | 50 | LOGGER.info("Calculating delivery routes for Account: {}", accountName); 51 | 52 | // Query for Account 53 | List accountResults = 54 | dataApi 55 | .query(String.format("SELECT Id FROM Account WHERE Name = '%s'", accountName)) 56 | .getRecords(); 57 | 58 | if (accountResults.size() == 0) { 59 | throw new DataApiException("No account found"); 60 | } 61 | 62 | // Get Account 63 | Record account = accountResults.get(0); 64 | 65 | // Build an Unit Of Work 66 | UnitOfWorkBuilder unitOfWork = dataApi.newUnitOfWorkBuilder(); 67 | 68 | try { 69 | // Build the VRP (Vehicle Routing Problem) by querying Vehicles and Services to provide 70 | VehicleRoutingProblem.Builder vrpBuilder = VehicleRoutingProblem.Builder.newInstance(); 71 | 72 | // Query Vehicles using the Data API 73 | RecordQueryResult vehicles = 74 | dataApi.query( 75 | "SELECT Id, Name, Location__Latitude__s, Location__Longitude__s FROM Vehicle__c"); 76 | for (Record record : vehicles.getRecords()) { 77 | VehicleTypeImpl.Builder vehicleTypeBuilder = 78 | VehicleTypeImpl.Builder.newInstance("vehicleType").addCapacityDimension(0, 2); 79 | VehicleType vehicleType = vehicleTypeBuilder.build(); 80 | String id = record.getStringField("Id").get(); 81 | Double locationX = record.getDoubleField("Location__Latitude__s").get(); 82 | Double locationY = record.getDoubleField("Location__Longitude__s").get(); 83 | Builder vehicleBuilder = VehicleImpl.Builder.newInstance(id); 84 | vehicleBuilder.setStartLocation(Location.newInstance(locationX, locationY)); 85 | vehicleBuilder.setType(vehicleType); 86 | VehicleImpl vehicle = vehicleBuilder.build(); 87 | vrpBuilder.addVehicle(vehicle); 88 | } 89 | 90 | // Query Services using the Data API 91 | RecordQueryResult services = 92 | dataApi.query( 93 | String.format( 94 | "SELECT Id, Name, Location__Latitude__s, Location__Longitude__s FROM Service__c" 95 | + " WHERE Account__c = '%s'", 96 | account.getStringField("Id").get())); 97 | for (Record record : services.getRecords()) { 98 | String id = record.getStringField("Id").get(); 99 | Double locationX = record.getDoubleField("Location__Latitude__s").get(); 100 | Double locationY = record.getDoubleField("Location__Longitude__s").get(); 101 | Service service = 102 | Service.Builder.newInstance(id) 103 | .addSizeDimension(0, 1) 104 | .setLocation(Location.newInstance(locationX, locationY)) 105 | .build(); 106 | vrpBuilder.addJob(service); 107 | } 108 | 109 | VehicleRoutingProblem vrp = vrpBuilder.build(); 110 | 111 | // Calculate the best routes for the services and vehicles 112 | StateManager stateManager = new StateManager(vrp); 113 | ConstraintManager constraintManager = new ConstraintManager(vrp, stateManager); 114 | constraintManager.addConstraint( 115 | new ServiceDeliveriesFirstConstraint(), ConstraintManager.Priority.CRITICAL); 116 | VehicleRoutingAlgorithm vra = 117 | Jsprit.Builder.newInstance(vrp) 118 | .setStateAndConstraintManager(stateManager, constraintManager) 119 | .buildAlgorithm(); 120 | Collection solutions = vra.searchSolutions(); 121 | VehicleRoutingProblemSolution solution = new SelectBest().selectSolution(solutions); 122 | 123 | // Store the delivery plan using the Unit Of Work pattern 124 | Record deliveryPlan = 125 | dataApi 126 | .newRecordBuilder("DeliveryPlan__c") 127 | .withField( 128 | "Name", 129 | "Delivery Plan @ " 130 | + LocalDateTime.now() 131 | .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))) 132 | .build(); 133 | ReferenceId deliveryPlanResult = unitOfWork.registerCreate(deliveryPlan); 134 | 135 | // Store the delivery routes using the Unit Of Work pattern 136 | for (VehicleRoute route : solution.getRoutes()) { 137 | Record deliveryRoute = 138 | dataApi 139 | .newRecordBuilder("DeliveryRoute__c") 140 | .withField("Vehicle__c", route.getVehicle().getId()) 141 | .withField("DeliveryPlan__c", deliveryPlanResult) 142 | .build(); 143 | ReferenceId deliveryRouteResult = unitOfWork.registerCreate(deliveryRoute); 144 | 145 | // Store the delivery waypoints using the Unit Of Work pattern 146 | for (TourActivity activity : route.getActivities()) { 147 | Record deliveryWaypoint = 148 | dataApi 149 | .newRecordBuilder("DeliveryWaypoint__c") 150 | .withField("DeliveryRoute__c", deliveryRouteResult) 151 | .withField("Service__c", ((PickupService) activity).getJob().getId()) 152 | .withField("Number__c", activity.getIndex()) 153 | .build(); 154 | unitOfWork.registerCreate(deliveryWaypoint); 155 | } 156 | } 157 | 158 | // Commit Unit Of Work as a single transaction 159 | Map result = 160 | dataApi.commitUnitOfWork(unitOfWork.build()); 161 | 162 | // Success! 163 | SolutionPrinter.print(vrp, solution, SolutionPrinter.Print.VERBOSE); 164 | return new FunctionOutput("Routes " + solution.getRoutes().size() + " calculated.", result); 165 | } catch (Exception e) { 166 | LOGGER.error("An error has ocurred", e); 167 | throw new RuntimeException("Error occured calculating routes", e); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /functions/routeplanner/src/test/java/com/salesforce/functions/demo/FunctionTest.java: -------------------------------------------------------------------------------- 1 | package com.salesforce.functions.demo; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | public class FunctionTest { 8 | 9 | @Test 10 | public void test() throws Exception { 11 | assertEquals(true, true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /functions/routeplanner/system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=11 -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { jestConfig } = require("@salesforce/sfdx-lwc-jest/config"); 2 | 3 | module.exports = { 4 | ...jestConfig, 5 | modulePathIgnorePatterns: ["/.localdevserver"] 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "salesforce-app", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Salesforce App", 6 | "scripts": { 7 | "lint": "eslint **/lwc/**", 8 | "test": "npm run test:unit", 9 | "test:unit": "sfdx-lwc-jest", 10 | "test:unit:watch": "sfdx-lwc-jest --watch", 11 | "test:unit:debug": "sfdx-lwc-jest --debug", 12 | "test:unit:coverage": "sfdx-lwc-jest --coverage", 13 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 14 | "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 15 | "postinstall": "husky install", 16 | "precommit": "lint-staged" 17 | }, 18 | "devDependencies": { 19 | "@lwc/eslint-plugin-lwc": "^1.0.1", 20 | "@prettier/plugin-xml": "^0.13.1", 21 | "@salesforce/eslint-config-lwc": "^2.0.0", 22 | "@salesforce/eslint-plugin-aura": "^2.0.0", 23 | "@salesforce/eslint-plugin-lightning": "^0.1.1", 24 | "@salesforce/sfdx-lwc-jest": "^0.13.0", 25 | "eslint": "^7.29.0", 26 | "eslint-plugin-import": "^2.23.4", 27 | "eslint-plugin-jest": "^24.3.6", 28 | "husky": "^7.0.0", 29 | "lint-staged": "^11.0.0", 30 | "prettier": "^2.3.2", 31 | "prettier-plugin-apex": "^1.10.0" 32 | }, 33 | "lint-staged": { 34 | "**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}": [ 35 | "prettier --write" 36 | ], 37 | "**/lwc/**": [ 38 | "eslint" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scripts/apex/hello.apex: -------------------------------------------------------------------------------- 1 | // Use .apex files to store anonymous Apex. 2 | // You can execute anonymous Apex in VS Code by selecting the 3 | // apex text and running the command: 4 | // SFDX: Execute Anonymous Apex with Currently Selected Text 5 | // You can also execute the entire file by running the command: 6 | // SFDX: Execute Anonymous Apex with Editor Contents 7 | 8 | functions.Function accountFunction = functions.Function.get('tdx_advanced_functions.routeplanner'); 9 | functions.FunctionInvocation invocation = accountFunction.invoke('{ "accountName": "Frisco Deliveries" }'); 10 | String jsonResponse = invocation.getResponse(); 11 | System.debug('Got response ' + jsonResponse); 12 | // Log error, if applicable 13 | if (invocation.getStatus() == functions.FunctionInvocationStatus.ERROR) { 14 | functions.FunctionInvocationError resultError = invocation.getError(); 15 | if (resultError != null) { 16 | System.debug('Error type: ' + resultError.getType()); 17 | System.debug('Error message: ' + resultError.getMessage()); 18 | } else { 19 | System.debug('Error: UNKNOWN'); 20 | } 21 | return; 22 | } -------------------------------------------------------------------------------- /scripts/create-db.js: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import pg from "pg"; 3 | import { createReadStream } from "fs"; 4 | import { parse } from "csv-parse"; 5 | import PQueue from "p-queue"; 6 | 7 | // Keep a list of all the charging stations 8 | const stations = []; 9 | 10 | // Parse CSV file into a JSON array 11 | function parseCsv() { 12 | const mapCsvColumns = { 13 | station_name: 1, 14 | street_address: 2, 15 | city: 4, 16 | state: 5, 17 | zip: 6, 18 | latitude: 24, 19 | longitude: 25 20 | }; 21 | const csvParser = parse({ delimiter: "," }); 22 | const chargingStationsReader = createReadStream( 23 | new URL("../data/alt_fuel_stations.csv", import.meta.url), 24 | "utf8" 25 | ); 26 | chargingStationsReader.pipe(csvParser); 27 | 28 | csvParser.on("readable", () => { 29 | let record; 30 | while ((record = csvParser.read()) !== null) { 31 | stations.push({ 32 | station_name: record[mapCsvColumns.station_name], 33 | street_address: record[mapCsvColumns.street_address], 34 | city: record[mapCsvColumns.city], 35 | state: record[mapCsvColumns.state], 36 | zip: record[mapCsvColumns.zip], 37 | latitude: record[mapCsvColumns.latitude], 38 | longitude: record[mapCsvColumns.longitude] 39 | }); 40 | } 41 | }); 42 | 43 | csvParser.on("end", () => { 44 | // Drop Header Information 45 | stations.shift(); 46 | /// Create Database and Populate Data 47 | createDatabase().catch((err) => { 48 | console.error(`CVS Parser Failed with error: ${err.message}`); 49 | process.exit(1); 50 | }); 51 | }); 52 | 53 | csvParser.on("error", (err) => { 54 | console.error(`Error reading CSV: ${err.message}`); 55 | process.exit(1); 56 | }); 57 | } 58 | 59 | // Create Database and Populate Data 60 | async function createDatabase() { 61 | const DATABASE_URL = process.env.DATABASE_URL; 62 | const { Pool } = pg; 63 | const createTableQuery = ` 64 | CREATE TABLE IF NOT EXISTS charging_stations ( 65 | id BIGSERIAL PRIMARY KEY, 66 | station_name CHARACTER VARYING NOT NULL, 67 | street_address CHARACTER VARYING NOT NULL, 68 | city CHARACTER VARYING NOT NULL, 69 | state CHARACTER VARYING NOT NULL, 70 | zip CHARACTER VARYING NOT NULL, 71 | latitude FLOAT NOT NULL, 72 | longitude FLOAT NOT NULL 73 | ); 74 | CREATE EXTENSION IF NOT EXISTS postgis; 75 | SELECT AddGeometryColumn('charging_stations', 'location', 4326, 'POINT', 2); 76 | `; 77 | 78 | // Create Database Client 79 | const client = new Pool({ 80 | connectionString: DATABASE_URL, 81 | max: 10, 82 | ssl: { 83 | rejectUnauthorized: false 84 | } 85 | }); 86 | await client.connect(); 87 | 88 | // Create Table 89 | await client.query(createTableQuery); 90 | console.log(`Created Table: charging_stations`); 91 | 92 | // Create Promise Queue 93 | const queue = new PQueue({ concurrency: 10000 }); 94 | 95 | // Count Active Jobs 96 | let count = 0; 97 | queue.on("active", () => { 98 | console.log( 99 | `Working on record #${++count}. Size: ${queue.size} Pending: ${ 100 | queue.pending 101 | }` 102 | ); 103 | }); 104 | 105 | // Create an array of Promises to insert charging stations 106 | const recordTasks = stations.map( 107 | (station) => async () => 108 | client.query( 109 | `INSERT INTO charging_stations (station_name, street_address, city, state, zip, latitude, longitude) VALUES ($1, $2, $3, $4, $5, $6, $7)`, 110 | [ 111 | station.station_name, 112 | station.street_address, 113 | station.city, 114 | station.state, 115 | station.zip, 116 | station.latitude, 117 | station.longitude 118 | ] 119 | ) 120 | ); 121 | 122 | // Add all the jobs to the queue 123 | await queue.addAll(recordTasks); 124 | 125 | console.log(`All records are inserterd into the database`); 126 | 127 | // Update the location column with GEO data 128 | const updatePointsQuery = `UPDATE charging_stations SET location = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)`; 129 | await client.query(updatePointsQuery); 130 | 131 | // Create GeoSpatial Index 132 | const createIndexQuery = `CREATE INDEX idx_charging_stations ON charging_stations USING GIST (location)`; 133 | await client.query(createIndexQuery); 134 | 135 | // Close the connection 136 | client.end(); 137 | process.exit(0); 138 | } 139 | 140 | parseCsv(); 141 | -------------------------------------------------------------------------------- /scripts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "scripts", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "csv-parse": "^5.0.4", 13 | "dotenv": "^16.0.0", 14 | "p-queue": "^7.2.0", 15 | "pg": "^8.7.3" 16 | } 17 | }, 18 | "node_modules/buffer-writer": { 19 | "version": "2.0.0", 20 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", 21 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", 22 | "engines": { 23 | "node": ">=4" 24 | } 25 | }, 26 | "node_modules/csv-parse": { 27 | "version": "5.0.4", 28 | "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.0.4.tgz", 29 | "integrity": "sha512-5AIdl8l6n3iYQYxan5djB5eKDa+vBnhfWZtRpJTcrETWfVLYN0WSj3L9RwvgYt+psoO77juUr8TG8qpfGZifVQ==" 30 | }, 31 | "node_modules/dotenv": { 32 | "version": "16.0.0", 33 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", 34 | "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==", 35 | "engines": { 36 | "node": ">=12" 37 | } 38 | }, 39 | "node_modules/eventemitter3": { 40 | "version": "4.0.7", 41 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", 42 | "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" 43 | }, 44 | "node_modules/p-queue": { 45 | "version": "7.2.0", 46 | "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-7.2.0.tgz", 47 | "integrity": "sha512-Kvv7p13M46lTYLQ/PsZdaj/1Vj6u/8oiIJgyQyx4oVkOfHdd7M2EZvXigDvcsSzRwanCzQirV5bJPQFoSQt5MA==", 48 | "dependencies": { 49 | "eventemitter3": "^4.0.7", 50 | "p-timeout": "^5.0.2" 51 | }, 52 | "engines": { 53 | "node": ">=12" 54 | }, 55 | "funding": { 56 | "url": "https://github.com/sponsors/sindresorhus" 57 | } 58 | }, 59 | "node_modules/p-timeout": { 60 | "version": "5.0.2", 61 | "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.0.2.tgz", 62 | "integrity": "sha512-sEmji9Yaq+Tw+STwsGAE56hf7gMy9p0tQfJojIAamB7WHJYJKf1qlsg9jqBWG8q9VCxKPhZaP/AcXwEoBcYQhQ==", 63 | "engines": { 64 | "node": ">=12" 65 | }, 66 | "funding": { 67 | "url": "https://github.com/sponsors/sindresorhus" 68 | } 69 | }, 70 | "node_modules/packet-reader": { 71 | "version": "1.0.0", 72 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", 73 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" 74 | }, 75 | "node_modules/pg": { 76 | "version": "8.7.3", 77 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz", 78 | "integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==", 79 | "dependencies": { 80 | "buffer-writer": "2.0.0", 81 | "packet-reader": "1.0.0", 82 | "pg-connection-string": "^2.5.0", 83 | "pg-pool": "^3.5.1", 84 | "pg-protocol": "^1.5.0", 85 | "pg-types": "^2.1.0", 86 | "pgpass": "1.x" 87 | }, 88 | "engines": { 89 | "node": ">= 8.0.0" 90 | }, 91 | "peerDependencies": { 92 | "pg-native": ">=2.0.0" 93 | }, 94 | "peerDependenciesMeta": { 95 | "pg-native": { 96 | "optional": true 97 | } 98 | } 99 | }, 100 | "node_modules/pg-connection-string": { 101 | "version": "2.5.0", 102 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", 103 | "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" 104 | }, 105 | "node_modules/pg-int8": { 106 | "version": "1.0.1", 107 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 108 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", 109 | "engines": { 110 | "node": ">=4.0.0" 111 | } 112 | }, 113 | "node_modules/pg-pool": { 114 | "version": "3.5.1", 115 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz", 116 | "integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==", 117 | "peerDependencies": { 118 | "pg": ">=8.0" 119 | } 120 | }, 121 | "node_modules/pg-protocol": { 122 | "version": "1.5.0", 123 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", 124 | "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" 125 | }, 126 | "node_modules/pg-types": { 127 | "version": "2.2.0", 128 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 129 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 130 | "dependencies": { 131 | "pg-int8": "1.0.1", 132 | "postgres-array": "~2.0.0", 133 | "postgres-bytea": "~1.0.0", 134 | "postgres-date": "~1.0.4", 135 | "postgres-interval": "^1.1.0" 136 | }, 137 | "engines": { 138 | "node": ">=4" 139 | } 140 | }, 141 | "node_modules/pgpass": { 142 | "version": "1.0.5", 143 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", 144 | "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", 145 | "dependencies": { 146 | "split2": "^4.1.0" 147 | } 148 | }, 149 | "node_modules/postgres-array": { 150 | "version": "2.0.0", 151 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 152 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", 153 | "engines": { 154 | "node": ">=4" 155 | } 156 | }, 157 | "node_modules/postgres-bytea": { 158 | "version": "1.0.0", 159 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 160 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=", 161 | "engines": { 162 | "node": ">=0.10.0" 163 | } 164 | }, 165 | "node_modules/postgres-date": { 166 | "version": "1.0.7", 167 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", 168 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", 169 | "engines": { 170 | "node": ">=0.10.0" 171 | } 172 | }, 173 | "node_modules/postgres-interval": { 174 | "version": "1.2.0", 175 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 176 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 177 | "dependencies": { 178 | "xtend": "^4.0.0" 179 | }, 180 | "engines": { 181 | "node": ">=0.10.0" 182 | } 183 | }, 184 | "node_modules/split2": { 185 | "version": "4.1.0", 186 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", 187 | "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", 188 | "engines": { 189 | "node": ">= 10.x" 190 | } 191 | }, 192 | "node_modules/xtend": { 193 | "version": "4.0.2", 194 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 195 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 196 | "engines": { 197 | "node": ">=0.4" 198 | } 199 | } 200 | }, 201 | "dependencies": { 202 | "buffer-writer": { 203 | "version": "2.0.0", 204 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", 205 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" 206 | }, 207 | "csv-parse": { 208 | "version": "5.0.4", 209 | "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.0.4.tgz", 210 | "integrity": "sha512-5AIdl8l6n3iYQYxan5djB5eKDa+vBnhfWZtRpJTcrETWfVLYN0WSj3L9RwvgYt+psoO77juUr8TG8qpfGZifVQ==" 211 | }, 212 | "dotenv": { 213 | "version": "16.0.0", 214 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", 215 | "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==" 216 | }, 217 | "eventemitter3": { 218 | "version": "4.0.7", 219 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", 220 | "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" 221 | }, 222 | "p-queue": { 223 | "version": "7.2.0", 224 | "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-7.2.0.tgz", 225 | "integrity": "sha512-Kvv7p13M46lTYLQ/PsZdaj/1Vj6u/8oiIJgyQyx4oVkOfHdd7M2EZvXigDvcsSzRwanCzQirV5bJPQFoSQt5MA==", 226 | "requires": { 227 | "eventemitter3": "^4.0.7", 228 | "p-timeout": "^5.0.2" 229 | } 230 | }, 231 | "p-timeout": { 232 | "version": "5.0.2", 233 | "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.0.2.tgz", 234 | "integrity": "sha512-sEmji9Yaq+Tw+STwsGAE56hf7gMy9p0tQfJojIAamB7WHJYJKf1qlsg9jqBWG8q9VCxKPhZaP/AcXwEoBcYQhQ==" 235 | }, 236 | "packet-reader": { 237 | "version": "1.0.0", 238 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", 239 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" 240 | }, 241 | "pg": { 242 | "version": "8.7.3", 243 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz", 244 | "integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==", 245 | "requires": { 246 | "buffer-writer": "2.0.0", 247 | "packet-reader": "1.0.0", 248 | "pg-connection-string": "^2.5.0", 249 | "pg-pool": "^3.5.1", 250 | "pg-protocol": "^1.5.0", 251 | "pg-types": "^2.1.0", 252 | "pgpass": "1.x" 253 | } 254 | }, 255 | "pg-connection-string": { 256 | "version": "2.5.0", 257 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", 258 | "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" 259 | }, 260 | "pg-int8": { 261 | "version": "1.0.1", 262 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 263 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" 264 | }, 265 | "pg-pool": { 266 | "version": "3.5.1", 267 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz", 268 | "integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==", 269 | "requires": {} 270 | }, 271 | "pg-protocol": { 272 | "version": "1.5.0", 273 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", 274 | "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" 275 | }, 276 | "pg-types": { 277 | "version": "2.2.0", 278 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 279 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 280 | "requires": { 281 | "pg-int8": "1.0.1", 282 | "postgres-array": "~2.0.0", 283 | "postgres-bytea": "~1.0.0", 284 | "postgres-date": "~1.0.4", 285 | "postgres-interval": "^1.1.0" 286 | } 287 | }, 288 | "pgpass": { 289 | "version": "1.0.5", 290 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", 291 | "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", 292 | "requires": { 293 | "split2": "^4.1.0" 294 | } 295 | }, 296 | "postgres-array": { 297 | "version": "2.0.0", 298 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 299 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" 300 | }, 301 | "postgres-bytea": { 302 | "version": "1.0.0", 303 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 304 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" 305 | }, 306 | "postgres-date": { 307 | "version": "1.0.7", 308 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", 309 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" 310 | }, 311 | "postgres-interval": { 312 | "version": "1.2.0", 313 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 314 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 315 | "requires": { 316 | "xtend": "^4.0.0" 317 | } 318 | }, 319 | "split2": { 320 | "version": "4.1.0", 321 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", 322 | "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==" 323 | }, 324 | "xtend": { 325 | "version": "4.0.2", 326 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 327 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "create-db.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "csv-parse": "^5.0.4", 14 | "dotenv": "^16.0.0", 15 | "p-queue": "^7.2.0", 16 | "pg": "^8.7.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/soql/account.soql: -------------------------------------------------------------------------------- 1 | // Use .soql files to store SOQL queries. 2 | // You can execute queries in VS Code by selecting the 3 | // query text and running the command: 4 | // SFDX: Execute SOQL Query with Currently Selected Text 5 | 6 | SELECT Id, Name FROM Account 7 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app", 5 | "default": true 6 | } 7 | ], 8 | "name": "tdx_advanced_functions", 9 | "namespace": "", 10 | "sfdcLoginUrl": "https://login.salesforce.com", 11 | "sourceApiVersion": "54.0" 12 | } 13 | --------------------------------------------------------------------------------