diff --git a/packages/app-store/salesforce/README.md b/packages/app-store/salesforce/README.md index 760e44d0c6..1d79b7cb77 100644 --- a/packages/app-store/salesforce/README.md +++ b/packages/app-store/salesforce/README.md @@ -1,8 +1,20 @@ -## Working With GraphQL +# Creating a Salesforce test org +You must have the [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli) installed to create a test org. Once installed, you can create a test org using the following command `yarn scratch-org:create` + +This will create a scratch org with the configuration specified in the `project-scratch-def.json` file. + +To open a browser tab to the org, run `yarn scratch-org:start` + +# Working With GraphQL This package utilizes [`GraphQL Codegen`](https://the-guild.dev/graphql/codegen#graphql-codegen) to generate types and queries from the Salesforce schema. ### Generating GraphQL Schema Currently v63 of the Salesforce graphql endpoint throws an error when trying to generate files. This is due to the `Setup__JoinInput` type not generating any fields. To work around this, the `schema.json` file comes from the [Salesforce graphql introspection query](https://www.postman.com/salesforce-developers/salesforce-developers/request/sy8qaf9/introspection-query). This file is then converted to a SDL file using the [graphql-introspection-json-to-sdl](https://github.com/Calcom/graphql-introspection-json-to-sdl) package. You can generate the SDL file by running `yarn generate:schema`. ### Generating Queries -When working with graphql files ensure that `yarn codegen:watch` is running in the background. This will generate the types and queries from the SDL file. \ No newline at end of file +When working with graphql files ensure that `yarn codegen:watch` is running in the background. This will generate the types and queries from the SDL file. + +# Developing the SFDC package +The SFDC package is written using Apex. To develop this package, you need to have the Salesforce CLI installed. Then you can run `yarn sfdc:deploy:preview` to see what changes will be deployed to the scratch org. Running `yarn sfdc:deploy` will deploy the changes to the scratch org. + +Note that if you want to call your local development instances you need to change the "Named Credential" on the scratch org settings to point the `CalCom_Development` credential to the local instance. diff --git a/packages/app-store/salesforce/api/index.ts b/packages/app-store/salesforce/api/index.ts index eb12c1b4ed..44c6f48858 100644 --- a/packages/app-store/salesforce/api/index.ts +++ b/packages/app-store/salesforce/api/index.ts @@ -1,2 +1,3 @@ export { default as add } from "./add"; export { default as callback } from "./callback"; +export { default as "user-sync" } from "./user-sync"; diff --git a/packages/app-store/salesforce/api/user-sync.ts b/packages/app-store/salesforce/api/user-sync.ts new file mode 100644 index 0000000000..2f4081bec0 --- /dev/null +++ b/packages/app-store/salesforce/api/user-sync.ts @@ -0,0 +1,25 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import logger from "@calcom/lib/logger"; + +const log = logger.getSubLogger({ prefix: ["[salesforce/user-sync]"] }); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { instanceUrl, orgId, salesforceUserId, email, changedFields, timestamp } = req.body; + + log.info("Received user sync request", { + instanceUrl, + orgId, + salesforceUserId, + timestamp, + }); + + // TODO: Validate instanceUrl + orgId against stored credentials + // TODO: Sync changedFields to Cal.com user + + return res.status(200).json({ success: true }); +} diff --git a/packages/app-store/salesforce/package.json b/packages/app-store/salesforce/package.json index 749802f2b4..16813b4605 100644 --- a/packages/app-store/salesforce/package.json +++ b/packages/app-store/salesforce/package.json @@ -7,7 +7,11 @@ "scripts": { "generate:schema": "graphql-introspection-json-to-sdl ./schema.json > ./schema.graphql", "codegen": "graphql-codegen --config codegen.ts", - "codegen:watch": "graphql-codegen --config codegen.ts --watch" + "codegen:watch": "graphql-codegen --config codegen.ts --watch", + "scratch-org:create": "sf org create scratch --definition-file scratch-org-def.json --alias calcom-test --duration-days 7 --set-default", + "scratch-org:start": "sf org open --target-org calcom-test", + "sfdc:deploy": "cd sfdc-package && sf project deploy start", + "sfdc:deploy:preview": "cd sfdc-package && sf project deploy preview" }, "dependencies": { "@calcom/lib": "workspace:*", diff --git a/packages/app-store/salesforce/scratch-org-def.json b/packages/app-store/salesforce/scratch-org-def.json new file mode 100644 index 0000000000..5134172dd3 --- /dev/null +++ b/packages/app-store/salesforce/scratch-org-def.json @@ -0,0 +1,6 @@ +{ + "orgName": "Cal.com Test Org", + "edition": "Developer", + "features": [], + "settings": {} +} diff --git a/packages/app-store/salesforce/sfdc-package/.forceignore b/packages/app-store/salesforce/sfdc-package/.forceignore new file mode 100644 index 0000000000..7b5b5a71fd --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/.forceignore @@ -0,0 +1,12 @@ +# List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status +# More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm +# + +package.xml + +# LWC configuration files +**/jsconfig.json +**/.eslintrc.json + +# LWC Jest +**/__tests__/** \ No newline at end of file diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls new file mode 100644 index 0000000000..17e8349831 --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls @@ -0,0 +1,54 @@ +public with sharing class CalComCalloutQueueable implements Queueable, Database.AllowsCallouts { + + private static final String ENDPOINT_PATH = '/api/integrations/salesforce/user-sync'; + + private List payloads; + + public CalComCalloutQueueable(List payloads) { + this.payloads = payloads; + } + + public void execute(QueueableContext context) { + String namedCredential = getNamedCredential(); + + for (UserChangePayload payload : payloads) { + sendPayload(namedCredential, payload); + } + } + + private String getNamedCredential() { + Boolean isSandbox = [SELECT IsSandbox FROM Organization LIMIT 1].IsSandbox; + return isSandbox ? 'callout:CalCom_Development' : 'callout:CalCom_Production'; + } + + private void sendPayload(String namedCredential, UserChangePayload payload) { + try { + HttpRequest req = new HttpRequest(); + req.setEndpoint(namedCredential + ENDPOINT_PATH); + req.setMethod('POST'); + req.setHeader('Content-Type', 'application/json'); + req.setBody(JSON.serialize(payload)); + req.setTimeout(30000); + + Http http = new Http(); + HttpResponse res = http.send(req); + + if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) { + System.debug(LoggingLevel.INFO, 'Successfully sent user update to Cal.com for user: ' + payload.salesforceUserId); + } else { + System.debug(LoggingLevel.ERROR, 'Failed to send user update to Cal.com. Status: ' + res.getStatusCode() + ' Body: ' + res.getBody()); + } + } catch (Exception e) { + System.debug(LoggingLevel.ERROR, 'Exception sending user update to Cal.com: ' + e.getMessage()); + } + } + + public class UserChangePayload { + public Id salesforceUserId; + public String email; + public String instanceUrl; + public String orgId; + public Map changedFields; + public String timestamp; + } +} diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls-meta.xml b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls-meta.xml new file mode 100644 index 0000000000..1e7de94088 --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls new file mode 100644 index 0000000000..f0c7b1368a --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls @@ -0,0 +1,46 @@ +public with sharing class UserUpdateHandler { + + public static void handleAfterUpdate(Map oldMap, Map newMap) { + List payloads = new List(); + + for (Id userId : newMap.keySet()) { + User oldUser = oldMap.get(userId); + User newUser = newMap.get(userId); + + Map changedFields = getChangedFields(oldUser, newUser); + + if (!changedFields.isEmpty()) { + CalComCalloutQueueable.UserChangePayload payload = new CalComCalloutQueueable.UserChangePayload(); + payload.salesforceUserId = userId; + payload.email = newUser.Email; + payload.instanceUrl = URL.getOrgDomainUrl().toExternalForm(); + payload.orgId = UserInfo.getOrganizationId(); + payload.changedFields = changedFields; + payload.timestamp = Datetime.now().formatGMT('yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\''); + payloads.add(payload); + } + } + + if (!payloads.isEmpty()) { + System.enqueueJob(new CalComCalloutQueueable(payloads)); + } + } + + private static Map getChangedFields(User oldUser, User newUser) { + Map changedFields = new Map(); + + Map oldFields = oldUser.getPopulatedFieldsAsMap(); + Map newFields = newUser.getPopulatedFieldsAsMap(); + + for (String fieldName : newFields.keySet()) { + Object oldValue = oldFields.get(fieldName); + Object newValue = newFields.get(fieldName); + + if (oldValue != newValue) { + changedFields.put(fieldName, newValue); + } + } + + return changedFields; + } +} diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls-meta.xml b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls-meta.xml new file mode 100644 index 0000000000..1e7de94088 --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Development.namedCredential-meta.xml b/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Development.namedCredential-meta.xml new file mode 100644 index 0000000000..0c1d1c556d --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Development.namedCredential-meta.xml @@ -0,0 +1,12 @@ + + + CalCom_Development + + https://app.cal.com + Anonymous + Custom + false + true + false + Enabled + diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Production.namedCredential-meta.xml b/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Production.namedCredential-meta.xml new file mode 100644 index 0000000000..831dbb1ca9 --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Production.namedCredential-meta.xml @@ -0,0 +1,12 @@ + + + CalCom_Production + + https://app.cal.com + Anonymous + Custom + false + true + false + Enabled + diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger b/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger new file mode 100644 index 0000000000..7ed18f8617 --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger @@ -0,0 +1,3 @@ +trigger UserUpdateTrigger on User (after update) { + UserUpdateHandler.handleAfterUpdate(Trigger.oldMap, Trigger.newMap); +} diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger-meta.xml b/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger-meta.xml new file mode 100644 index 0000000000..36cb41a89a --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/packages/app-store/salesforce/sfdc-package/sfdx-project.json b/packages/app-store/salesforce/sfdc-package/sfdx-project.json new file mode 100644 index 0000000000..591afc134d --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/sfdx-project.json @@ -0,0 +1,12 @@ +{ + "packageDirectories": [ + { + "path": "force-app", + "default": true + } + ], + "name": "calcom-user-sync", + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "64.0" +}