From 34af3653fef4e44571a5b7e183b92916bd0c92fe Mon Sep 17 00:00:00 2001 From: Johannes Degn Date: Mon, 4 Sep 2023 16:27:07 +0200 Subject: [PATCH 1/3] Add support to company CRM api calls --- company.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ crm.go | 5 ++++ 2 files changed, 92 insertions(+) create mode 100644 company.go diff --git a/company.go b/company.go new file mode 100644 index 0000000..e9d4381 --- /dev/null +++ b/company.go @@ -0,0 +1,87 @@ +package hubspot + +const ( + companyBasePath = "companies" +) + +// CompanyService is an interface of company endpoints of the HubSpot API. +// HubSpot companies store information about organizations. +// It can also be associated with other CRM objects such as deal and contact. +// Reference: https://developers.hubspot.com/docs/api/crm/companies +type CompanyService interface { + Get(companyID string, company interface{}, option *RequestQueryOption) (*ResponseResource, error) + Create(company interface{}) (*ResponseResource, error) + Update(companyID string, company interface{}) (*ResponseResource, error) + Delete(companyID string) error + AssociateAnotherObj(companyID string, conf *AssociationConfig) (*ResponseResource, error) +} + +// CompanyServiceOp handles communication with the product related methods of the HubSpot API. +type CompanyServiceOp struct { + companyPath string + client *Client +} + +var _ CompanyService = (*CompanyServiceOp)(nil) + +type Company struct { + Address *HsStr `json:"address,omitempty"` + // TODO: default properties here +} + +var defaultCompanyFields = []string{ + "address", + // TODO: default props here +} + +// Get gets a Company. +// In order to bind the get content, a structure must be specified as an argument. +// Also, if you want to gets a custom field, you need to specify the field name. +// If you specify a non-existent field, it will be ignored. +// e.g. &hubspot.RequestQueryOption{ Properties: []string{"custom_a", "custom_b"}} +func (s *CompanyServiceOp) Get(companyID string, company interface{}, option *RequestQueryOption) (*ResponseResource, error) { + resource := &ResponseResource{Properties: company} + if err := s.client.Get(s.companyPath+"/"+companyID, resource, option.setupProperties(defaultCompanyFields)); err != nil { + return nil, err + } + return resource, nil +} + +// Create creates a new company. +// In order to bind the created content, a structure must be specified as an argument. +// When using custom fields, please embed hubspot.Company in your own structure. +func (s *CompanyServiceOp) Create(company interface{}) (*ResponseResource, error) { + req := &RequestPayload{Properties: company} + resource := &ResponseResource{Properties: company} + if err := s.client.Post(s.companyPath, req, resource); err != nil { + return nil, err + } + return resource, nil +} + +// Update updates a company. +// In order to bind the updated content, a structure must be specified as an argument. +// When using custom fields, please embed hubspot.Company in your own structure. +func (s *CompanyServiceOp) Update(companyID string, company interface{}) (*ResponseResource, error) { + req := &RequestPayload{Properties: company} + resource := &ResponseResource{Properties: company} + if err := s.client.Patch(s.companyPath+"/"+companyID, req, resource); err != nil { + return nil, err + } + return resource, nil +} + +// Delete deletes a company. +func (s *CompanyServiceOp) Delete(companyID string) error { + return s.client.Delete(s.companyPath+"/"+companyID, nil) +} + +// AssociateAnotherObj associates Company with another HubSpot objects. +// If you want to associate a custom object, please use a defined value in HubSpot. +func (s *CompanyServiceOp) AssociateAnotherObj(companyID string, conf *AssociationConfig) (*ResponseResource, error) { + resource := &ResponseResource{Properties: &Company{}} + if err := s.client.Put(s.companyPath+"/"+companyID+"/"+conf.makeAssociationPath(), nil, resource); err != nil { + return nil, err + } + return resource, nil +} diff --git a/crm.go b/crm.go index 59ec2e4..209e11f 100644 --- a/crm.go +++ b/crm.go @@ -10,6 +10,7 @@ const ( type CRM struct { Contact ContactService + Company CompanyService Deal DealService Imports CrmImportsService Schemas CrmSchemasService @@ -24,6 +25,10 @@ func newCRM(c *Client) *CRM { contactPath: fmt.Sprintf("%s/%s/%s", crmPath, objectsBasePath, contactBasePath), client: c, }, + Company: &CompanyServiceOp{ + companyPath: fmt.Sprintf("%s/%s/%s", crmPath, objectsBasePath, companyBasePath), + client: c, + }, Deal: &DealServiceOp{ dealPath: fmt.Sprintf("%s/%s/%s", crmPath, objectsBasePath, dealBasePath), client: c, From 306c4b77b3e3620bab556d71569c16fa83f0c8e0 Mon Sep 17 00:00:00 2001 From: Johannes Degn Date: Wed, 18 Oct 2023 16:20:03 +0200 Subject: [PATCH 2/3] Add associations and test cases --- association.go | 7 + company_test.go | 397 ++++++++++++++++++++++++++++++++++++++++++++++++ export_test.go | 1 + 3 files changed, 405 insertions(+) create mode 100644 company_test.go diff --git a/association.go b/association.go index 7966c07..ad85b5c 100644 --- a/association.go +++ b/association.go @@ -20,6 +20,7 @@ type ObjectType string const ( ObjectTypeContact ObjectType = "contacts" ObjectTypeDeal ObjectType = "deals" + ObjectTypeCompany ObjectType = "company" ) // AssociationType is the name of the key used to associate the objects together. @@ -38,6 +39,9 @@ const ( AssociationTypeDealToEngagement AssociationType = "deal_to_engagement" AssociationTypeDealToLineItem AssociationType = "deal_to_line_item" AssociationTypeDealToTicket AssociationType = "deal_to_ticket" + + AssociationTypeCompanyToContact AssociationType = "company_to_contact" + AssociationTypeCompanyToDeal AssociationType = "company_to_deal" ) type AssociationConfig struct { @@ -57,6 +61,9 @@ type Associations struct { Deals struct { Results []AssociationResult `json:"results"` } `json:"deals"` + Companies struct { + Results []AssociationResult `json:"results"` + } `json:"companies"` } type AssociationResult struct { diff --git a/company_test.go b/company_test.go new file mode 100644 index 0000000..15b3e5d --- /dev/null +++ b/company_test.go @@ -0,0 +1,397 @@ +package hubspot_test + +import ( + "net/http" + "reflect" + "testing" + + "github.com/belong-inc/go-hubspot" + "github.com/google/go-cmp/cmp" +) + +func TestCompanyServiceOp_Create(t *testing.T) { + company := &hubspot.Company{ + Address: hubspot.NewString("example address"), + } + + type fields struct { + companyPath string + client *hubspot.Client + } + type args struct { + company interface{} + } + tests := []struct { + name string + fields fields + args args + want *hubspot.ResponseResource + wantErr error + }{ + { + name: "Successfully create a company", + fields: fields{ + companyPath: hubspot.ExportCompanyBasePath, + client: hubspot.NewMockClient(&hubspot.MockConfig{ + Status: http.StatusCreated, + Header: http.Header{}, + Body: []byte(`{"id":"company001","properties":{"city":"Cambridge","createdate":"2019-10-30T03:30:17.883Z","domain":"biglytics.net","hs_lastmodifieddate":"2019-12-07T16:50:06.678Z","industry":"Technology","name":"Biglytics","phone":"(877)929-0687","state":"Massachusetts"},"createdAt":"2019-10-30T03:30:17.883Z","updatedAt":"2019-12-07T16:50:06.678Z","archived":false}`), + }), + }, + args: args{ + company: company, + }, + want: &hubspot.ResponseResource{ + ID: "company001", + Archived: false, + Properties: &hubspot.Company{ + Address: hubspot.NewString("example address"), + }, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + }, + wantErr: nil, + }, + { + name: "Received invalid request", + fields: fields{ + companyPath: hubspot.ExportCompanyBasePath, + client: hubspot.NewMockClient(&hubspot.MockConfig{ + Status: http.StatusBadRequest, + Header: http.Header{}, + Body: []byte(`{"message": "Invalid input (details will vary based on the error)","correlationId": "aeb5f871-7f07-4993-9211-075dc63e7cbf","category": "VALIDATION_ERROR","links": {"knowledge-base": "https://www.hubspot.com/products/service/knowledge-base"}}`), + }), + }, + args: args{ + company: company, + }, + want: nil, + wantErr: &hubspot.APIError{ + HTTPStatusCode: http.StatusBadRequest, + Message: "Invalid input (details will vary based on the error)", + CorrelationID: "aeb5f871-7f07-4993-9211-075dc63e7cbf", + Category: "VALIDATION_ERROR", + Links: hubspot.ErrLinks{ + KnowledgeBase: "https://www.hubspot.com/products/service/knowledge-base", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.fields.client.CRM.Company.Create(tt.args.company) + if !reflect.DeepEqual(tt.wantErr, err) { + t.Errorf("Create() error mismatch: want %s got %s", tt.wantErr, err) + return + } + if diff := cmp.Diff(tt.want, got, cmpTimeOption); diff != "" { + t.Errorf("Create() response mismatch (-want +got):%s", diff) + } + }) + } +} + +func TestCompanyServiceOp_Update(t *testing.T) { + company := &hubspot.Company{ + Address: hubspot.NewString("example address"), + } + + type fields struct { + companyPath string + client *hubspot.Client + } + type args struct { + companyID string + company interface{} + } + tests := []struct { + name string + fields fields + args args + want *hubspot.ResponseResource + wantErr error + }{ + { + name: "Successfully update a company", + fields: fields{ + companyPath: hubspot.ExportCompanyBasePath, + client: hubspot.NewMockClient(&hubspot.MockConfig{ + Status: http.StatusCreated, + Header: http.Header{}, + Body: []byte(`{"id":"company001","properties":{"city":"Cambridge","createdate":"2019-10-30T03:30:17.883Z","domain":"biglytics.net","hs_lastmodifieddate":"2019-12-07T16:50:06.678Z","industry":"Technology","name":"Biglytics","phone":"(877)929-0687","state":"Massachusetts"},"createdAt":"2019-10-30T03:30:17.883Z","updatedAt":"2019-12-07T16:50:06.678Z","archived":false}`), + }), + }, + args: args{ + company: company, + }, + want: &hubspot.ResponseResource{ + ID: "company001", + Archived: false, + Properties: &hubspot.Company{ + Address: hubspot.NewString("example address"), + }, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + }, + wantErr: nil, + }, + { + name: "Received invalid request", + fields: fields{ + companyPath: hubspot.ExportCompanyBasePath, + client: hubspot.NewMockClient(&hubspot.MockConfig{ + Status: http.StatusBadRequest, + Header: http.Header{}, + Body: []byte(`{"message": "Invalid input (details will vary based on the error)","correlationId": "aeb5f871-7f07-4993-9211-075dc63e7cbf","category": "VALIDATION_ERROR","links": {"knowledge-base": "https://www.hubspot.com/products/service/knowledge-base"}}`), + }), + }, + args: args{ + companyID: "company001", + company: company, + }, + want: nil, + wantErr: &hubspot.APIError{ + HTTPStatusCode: http.StatusBadRequest, + Message: "Invalid input (details will vary based on the error)", + CorrelationID: "aeb5f871-7f07-4993-9211-075dc63e7cbf", + Category: "VALIDATION_ERROR", + Links: hubspot.ErrLinks{ + KnowledgeBase: "https://www.hubspot.com/products/service/knowledge-base", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.fields.client.CRM.Company.Update(tt.args.companyID, tt.args.company) + if !reflect.DeepEqual(tt.wantErr, err) { + t.Errorf("Update() error mismatch: want %s got %s", tt.wantErr, err) + return + } + if diff := cmp.Diff(tt.want, got, cmpTimeOption); diff != "" { + t.Errorf("Update() response mismatch (-want +got):%s", diff) + } + }) + } +} + +func TestCompanyServiceOp_Get(t *testing.T) { + type CustomFields struct { + hubspot.Company + } + + type fields struct { + companyPath string + client *hubspot.Client + } + type args struct { + companyID string + company interface{} + option *hubspot.RequestQueryOption + } + tests := []struct { + name string + fields fields + args args + want *hubspot.ResponseResource + wantErr error + }{ + { + name: "Successfully get a company", + fields: fields{ + companyPath: hubspot.ExportCompanyBasePath, + client: hubspot.NewMockClient(&hubspot.MockConfig{ + Status: http.StatusOK, + Header: http.Header{}, + Body: []byte(`{"id":"company001","properties":{"city":"Cambridge","createdate":"2019-10-30T03:30:17.883Z","domain":"biglytics.net","hs_lastmodifieddate":"2019-12-07T16:50:06.678Z","industry":"Technology","name":"Biglytics","phone":"(877)929-0687","state":"Massachusetts","address":"address"},"createdAt":"2019-10-30T03:30:17.883Z","updatedAt":"2019-12-07T16:50:06.678Z"}`), + }), + }, + args: args{ + companyID: "company001", + company: &hubspot.Company{}, + }, + want: &hubspot.ResponseResource{ + ID: "company001", + Archived: false, + Properties: &hubspot.Company{ + Address: hubspot.NewString("address"), + }, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + }, + wantErr: nil, + }, + { + name: "Received invalid request", + fields: fields{ + companyPath: hubspot.ExportCompanyBasePath, + client: hubspot.NewMockClient(&hubspot.MockConfig{ + Status: http.StatusBadRequest, + Header: http.Header{}, + Body: []byte(`{"message": "Invalid input (details will vary based on the error)","correlationId": "aeb5f871-7f07-4993-9211-075dc63e7cbf","category": "VALIDATION_ERROR","links": {"knowledge-base": "https://www.hubspot.com/products/service/knowledge-base"}}`), + }), + }, + args: args{ + companyID: "company001", + company: &hubspot.Company{}, + }, + want: nil, + wantErr: &hubspot.APIError{ + HTTPStatusCode: http.StatusBadRequest, + Message: "Invalid input (details will vary based on the error)", + CorrelationID: "aeb5f871-7f07-4993-9211-075dc63e7cbf", + Category: "VALIDATION_ERROR", + Links: hubspot.ErrLinks{ + KnowledgeBase: "https://www.hubspot.com/products/service/knowledge-base", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.fields.client.CRM.Company.Get(tt.args.companyID, tt.args.company, tt.args.option) + if !reflect.DeepEqual(tt.wantErr, err) { + t.Errorf("Get() error mismatch: want %s got %s", tt.wantErr, err) + return + } + if diff := cmp.Diff(tt.want, got, cmpTimeOption); diff != "" { + t.Errorf("Get() response mismatch (-want +got):%s", diff) + } + }) + } +} + +func TestCompanyServiceOp_Delete(t *testing.T) { + type fields struct { + companyPath string + client *hubspot.Client + } + type args struct { + companyID string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "Successfully delete a company", + fields: fields{ + companyPath: hubspot.ExportCompanyBasePath, + client: hubspot.NewMockClient(&hubspot.MockConfig{ + Status: http.StatusNoContent, + Header: http.Header{}, + }), + }, + args: args{ + companyID: "company001", + }, + wantErr: nil, + }, + { + name: "Received invalid request", + fields: fields{ + companyPath: hubspot.ExportCompanyBasePath, + client: hubspot.NewMockClient(&hubspot.MockConfig{ + Status: http.StatusBadRequest, + Header: http.Header{}, + Body: []byte(`{"message": "Invalid input (details will vary based on the error)","correlationId": "aeb5f871-7f07-4993-9211-075dc63e7cbf","category": "VALIDATION_ERROR","links": {"knowledge-base": "https://www.hubspot.com/products/service/knowledge-base"}}`), + }), + }, + args: args{ + companyID: "company001", + }, + wantErr: &hubspot.APIError{ + HTTPStatusCode: http.StatusBadRequest, + Message: "Invalid input (details will vary based on the error)", + CorrelationID: "aeb5f871-7f07-4993-9211-075dc63e7cbf", + Category: "VALIDATION_ERROR", + Links: hubspot.ErrLinks{ + KnowledgeBase: "https://www.hubspot.com/products/service/knowledge-base", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.fields.client.CRM.Company.Delete(tt.args.companyID) + if !reflect.DeepEqual(tt.wantErr, err) { + t.Errorf("Delete() error mismatch: want %s got %s", tt.wantErr, err) + return + } + }) + } +} + +func TestCompanyServiceOp_AssociateAnotherObj(t *testing.T) { + type fields struct { + companyPath string + client *hubspot.Client + } + type args struct { + companyID string + conf *hubspot.AssociationConfig + } + tests := []struct { + name string + fields fields + args args + want *hubspot.ResponseResource + wantErr error + }{ + { + name: "Successfully associated object", + fields: fields{ + companyPath: hubspot.ExportCompanyBasePath, + client: hubspot.NewMockClient(&hubspot.MockConfig{ + Status: http.StatusOK, + Header: http.Header{}, + Body: []byte(`{"id":"company001","properties":{"createdate":"2019-10-30T03:30:17.883Z","hs_object_id":"company001","lastmodifieddate":"2019-12-07T16:50:06.678Z"},"createdAt":"2019-10-30T03:30:17.883Z","updatedAt":"2019-12-07T16:50:06.678Z","associations":{"deals":{"results":[{"id":"deal001","type":"company_to_deal"}]}},"archived":false,"archivedAt":"2020-01-01T00:00:00.000Z"}`), + }), + }, + args: args{ + companyID: "company001", + conf: &hubspot.AssociationConfig{ + ToObject: hubspot.ObjectTypeDeal, + ToObjectID: "deal001", + Type: hubspot.AssociationTypeCompanyToDeal, + }, + }, + want: &hubspot.ResponseResource{ + ID: "company001", + Archived: false, + Associations: &hubspot.Associations{ + Deals: struct { + Results []hubspot.AssociationResult `json:"results"` + }{ + Results: []hubspot.AssociationResult{ + {ID: "deal001", Type: string(hubspot.AssociationTypeCompanyToDeal)}, + }, + }, + }, + Properties: &hubspot.Contact{ + HsObjectID: hubspot.NewString("company001"), + CreateDate: &createdAt, + LastModifiedDate: &updatedAt, + }, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + ArchivedAt: &archivedAt, + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.fields.client.CRM.Contact.AssociateAnotherObj(tt.args.companyID, tt.args.conf) + if !reflect.DeepEqual(tt.wantErr, err) { + t.Errorf("AssociateAnotherObj() error mismatch: want %s got %s", tt.wantErr, err) + return + } + if diff := cmp.Diff(tt.want, got, cmpTimeOption); diff != "" { + t.Errorf("AssociateAnotherObj() response mismatch (-want +got):%s", diff) + } + }) + } +} diff --git a/export_test.go b/export_test.go index fd523cb..aca5eee 100644 --- a/export_test.go +++ b/export_test.go @@ -10,6 +10,7 @@ var ( ExportBaseURL = defaultBaseURL ExportContactBasePath = contactBasePath ExportDealBasePath = dealBasePath + ExportCompanyBasePath = companyBasePath ) var ( From 9f98925cbcef5236d7ebf93d9d676801b53b9786 Mon Sep 17 00:00:00 2001 From: Johannes Degn Date: Thu, 26 Oct 2023 19:05:47 +0200 Subject: [PATCH 3/3] make fmt, make lint --- company.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/company.go b/company.go index e9d4381..207064f 100644 --- a/company.go +++ b/company.go @@ -25,13 +25,13 @@ type CompanyServiceOp struct { var _ CompanyService = (*CompanyServiceOp)(nil) type Company struct { - Address *HsStr `json:"address,omitempty"` - // TODO: default properties here + Address *HsStr `json:"address,omitempty"` + // TODO: default properties here } var defaultCompanyFields = []string{ "address", - // TODO: default props here + // TODO: default props here } // Get gets a Company.