Repository: amp-labs/connectors
Pull request #115 (2024-03-20 13:34:49)

[ENG-872] feat: Add Microsoft Dynamics 365 Sales connector
## Checklist
- [x] Ran Linter

> WARN [runner/source_code] Failed to get line 391 for file utils/credentials.go: invalid file line index0 (390) >= len(fc) (92) 
WARN [runner/source_code] Failed to get line 402 for file utils/credentials.go: invalid file line index0 (401) >= len(fc) (92) 
::error file=utils/credentials.go,line=391,col=1::getFromReader returns generic interface (T) (ireturn)
::error file=utils/credentials.go,line=402,col=1::get returns generic interface (T) (ireturn)
make: *** [Makefile:14: fix] Error 1


- [x] Catalog tests passing
- [x] Created PR from non-main branch 

## Catalog variables
Workspace: `org5bd08fdd`
Scopes: `https://org5bd08fdd.crm.dynamics.com/user_impersonation,offline_access`
As of now substitution works for BaseURL. Here user impersonation scope itself contains workspace variable.

## Notes
Acquires AccessToken from Microsoft Entra ID that has preconfigured permissioning to access Sales App APIs making it a Sales Connector.

## Testing 
All Postaman requests are compared to the Web Portals View of Dynamics365Sales to demonstrate that both of them access the same data.

### GET 
URL: http://localhost:4444/
Top level discovery endpoint. Lists all entry sets for Sales CRM.
![image](https://github.com/amp-labs/connectors/assets/60330780/8d6aef30-c91c-4403-9902-a917938a89c8)

URL: http://localhost:4444/contacts?$select=fullname

List Contacts fullnames.
![image](https://github.com/amp-labs/connectors/assets/60330780/2ca98454-4b16-440a-aeb7-8ed4f6af43f1)
![image](https://github.com/amp-labs/connectors/assets/60330780/904c9de0-5944-415b-9af6-3371626c0ae0)

### POST
URL: http://localhost:4444/leads
Create new Lead. To verify that it was created lets view this information in CRM.
![image](https://github.com/amp-labs/connectors/assets/60330780/f9bb1d53-6a50-4c78-98e8-53b7a6616642)
![image](https://github.com/amp-labs/connectors/assets/60330780/45be8cde-6646-4c60-a235-b2c65a4b319c)


### GET
After lead creation filter GET response by firstname to get entry id, this will be used in update and delete step below. URL: http://localhost:4444/leads?$count=true&$filter=contains(firstname, 'Bob')
Now request `Single Entry`.
URL: http://localhost:4444/leads(dd2f7870-3fe8-ee11-a204-0022481f9e3c)?$select=firstname,lastname,subject
![image](https://github.com/amp-labs/connectors/assets/60330780/44712043-8865-40f0-979c-513cb06cf9c7)


### PATCH
URL: http://localhost:4444/leads(dd2f7870-3fe8-ee11-a204-0022481f9e3c)
Update `Subject Name` of the lead.
![image](https://github.com/amp-labs/connectors/assets/60330780/87ee1be7-e05e-4c2a-a229-bd52b233eb10)
![image](https://github.com/amp-labs/connectors/assets/60330780/588684eb-a167-4179-ae0a-6821bc4cecea)


### DELETE
URL: http://localhost:4444/leads(dd2f7870-3fe8-ee11-a204-0022481f9e3c)
Removes lead. After which entry is not found.
![image](https://github.com/amp-labs/connectors/assets/60330780/3097b2eb-e962-4026-8bd3-94dbde2609e1)
![image](https://github.com/amp-labs/connectors/assets/60330780/d13b84c4-d605-4780-aa2e-c09fd1bf207d)

## Pagination
### GET
URL: http://localhost:4444/phonecalls?$count=true
List phone calls counting the number of entries.
![image](https://github.com/amp-labs/connectors/assets/60330780/1d1a1c6d-27d5-47e9-9d28-80ddf4462e57)
![image](https://github.com/amp-labs/connectors/assets/60330780/6e06c5db-9488-4488-bd1c-92f8fd8d5094)

### First page
URL: http://localhost:4444/phonecalls?$count=true&$select=subject
List phone calls, limit to max page size of 2 via `Prefer` header.
![image](https://github.com/amp-labs/connectors/assets/60330780/b6aeb16e-f9d9-4ddf-b840-0c9c9cbf6364)


### Next page
URL: http://localhost:4444/phonecalls?$count=true&$select=subject&$skiptoken=%3Ccookie%20pagenumber=%222%22%20pagingcookie=%22%253ccookie%2520page%253d%25221%2522%253e%253cactivityid%2520last%253d%2522%257b3D1D5314-ED24-EB11-A814-000D3A30F257%257d%2522%2520first%253d%2522%257b1D1D5314-ED24-EB11-A814-000D3A30F257%257d%2522%2520%252f%253e%253c%252fcookie%253e%22%20istracking=%22False%22%20/%3E
Follow next page. We specify the same `maxpagesize` header. The link cannot be constructed or altered(as of MS docs), we are treating it as opaque string.
![image](https://github.com/amp-labs/connectors/assets/60330780/27743b13-b7ce-4c69-bf3a-9d7340d77ac5)



Cobalt0s: @RajatPawar it is ready for review :smiley:   

Patch:
diff --git a/providers/catalog.go b/providers/catalog.go
index 840790cc..912abfd0 100644
--- a/providers/catalog.go
+++ b/providers/catalog.go
@@ -5,30 +5,31 @@ package providers
 // ================================================================================
 
 const (
-	Salesforce        Provider = "salesforce"
-	Hubspot           Provider = "hubspot"
-	LinkedIn          Provider = "linkedIn"
-	Salesloft         Provider = "salesloft"
-	Outreach          Provider = "outreach"
-	Pipedrive         Provider = "pipedrive"
-	Copper            Provider = "copper"
-	ZohoCRM           Provider = "zohoCRM"
-	Sellsy            Provider = "sellsy"
-	Attio             Provider = "attio"
-	Close             Provider = "close"
-	Keap              Provider = "keap"
-	Asana             Provider = "asana"
-	Dropbox           Provider = "dropbox"
-	Notion            Provider = "notion"
-	Gong              Provider = "gong"
-	Zoom              Provider = "zoom"
-	Intercom          Provider = "intercom"
-	DocuSign          Provider = "docuSign"
-	DocuSignDeveloper Provider = "docuSignDeveloper"
-	Calendly          Provider = "calendly"
-	AWeber            Provider = "aWeber"
-	GetResponse       Provider = "getResponse"
-	ConstantContact   Provider = "constantContact"
+	Salesforce                Provider = "salesforce"
+	Hubspot                   Provider = "hubspot"
+	LinkedIn                  Provider = "linkedIn"
+	Salesloft                 Provider = "salesloft"
+	Outreach                  Provider = "outreach"
+	Pipedrive                 Provider = "pipedrive"
+	Copper                    Provider = "copper"
+	ZohoCRM                   Provider = "zohoCRM"
+	Sellsy                    Provider = "sellsy"
+	Attio                     Provider = "attio"
+	Close                     Provider = "close"
+	Keap                      Provider = "keap"
+	Asana                     Provider = "asana"
+	Dropbox                   Provider = "dropbox"
+	Notion                    Provider = "notion"
+	Gong                      Provider = "gong"
+	Zoom                      Provider = "zoom"
+	Intercom                  Provider = "intercom"
+	DocuSign                  Provider = "docuSign"
+	DocuSignDeveloper         Provider = "docuSignDeveloper"
+	Calendly                  Provider = "calendly"
+	AWeber                    Provider = "aWeber"
+	GetResponse               Provider = "getResponse"
+	ConstantContact           Provider = "constantContact"
+	MicrosoftDynamics365Sales Provider = "microsoftDynamics365Sales"
 )
 
 // ================================================================================
@@ -489,6 +490,25 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		},
 	},
 
+	// MS Sales configuration
+	MicrosoftDynamics365Sales: {
+		AuthType: Oauth2,
+		BaseURL:  "https://{{.workspace}}.api.crm.dynamics.com",
+		OauthOpts: OauthOpts{
+			AuthURL:                   "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
+			TokenURL:                  "https://login.microsoftonline.com/common/oauth2/v2.0/token",
+			ExplicitScopesRequired:    true,
+			ExplicitWorkspaceRequired: true,
+		},
+		Support: Support{
+			BulkWrite: false,
+			Proxy:     false,
+			Read:      false,
+			Subscribe: false,
+			Write:     false,
+		},
+	},
+
 	// ConstantContact configuration
 	ConstantContact: {
 		AuthType: Oauth2,
diff --git a/scripts/oauth/token.go b/scripts/oauth/token.go
index 2089adc1..fe6893d7 100644
--- a/scripts/oauth/token.go
+++ b/scripts/oauth/token.go
@@ -265,6 +265,7 @@ func setup() *OAuthApp {
 	if state != "" {
 		app.State = state
 	}
+
 	substitutions, err := registry.GetMap("Substitutions")
 	if err != nil {
 		slog.Warn("no substitutions, ensure that the provider info doesn't have any {{variables}}")


Test patch:
diff --git a/providers/catalog_test.go b/providers/catalog_test.go
index 137fd3d0..19cdc754 100644
--- a/providers/catalog_test.go
+++ b/providers/catalog_test.go
@@ -570,6 +570,31 @@ var testCases = []struct { // nolint
 		},
 		expectedErr: nil,
 	},
+	{
+		provider:    MicrosoftDynamics365Sales,
+		description: "MS Dynamics 365 Sales provider config with valid substitutions",
+		substitutions: map[string]string{
+			"workspace": "testing",
+		},
+		expected: &ProviderInfo{
+			Support: Support{
+				Read:      false,
+				Write:     false,
+				BulkWrite: false,
+				Subscribe: false,
+				Proxy:     false,
+			},
+			AuthType: Oauth2,
+			OauthOpts: OauthOpts{
+				AuthURL:                   "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
+				TokenURL:                  "https://login.microsoftonline.com/common/oauth2/v2.0/token",
+				ExplicitScopesRequired:    true,
+				ExplicitWorkspaceRequired: true,
+			},
+			BaseURL: "https://testing.api.crm.dynamics.com",
+		},
+		expectedErr: nil,
+	},
 	{
 		provider:    ConstantContact,
 		description: "Valid ConstantContact provider config with no substitutions",