From 62903ecba99df296398db335bd9000380194c8f1 Mon Sep 17 00:00:00 2001 From: Amelia C <71268735+AmeliaCelline@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:25:18 +0800 Subject: [PATCH] feat(hubspot): add 4 tasks and modify Retrieve Association task and Get Thread task (#265) Because - need to implement more tasks for HubSpot to make the user story more complete. This commit - 4 additional tasks will be implemented TODO: - [x] Update Deal - [x] Update Ticket - [x] Get Owner (allow users to get HubSpot users information using either Owner ID or User ID) - [x] Get All (allow users to get all the object IDs they want, such as contacts, companies, etc) - [x] Fix Retrieve Association Task so that it can get more than 100+ object IDs (pagination) - [x] Fix Get Thread task so that there is no limit in the number of messages it can get from a thread (pagination) --- application/hubspot/v0/README.mdx | 119 +- application/hubspot/v0/association.go | 159 ++- application/hubspot/v0/association_test.go | 6 +- application/hubspot/v0/company.go | 32 +- application/hubspot/v0/company_test.go | 13 +- application/hubspot/v0/config/definition.json | 6 +- application/hubspot/v0/config/tasks.json | 1075 +++++++++-------- application/hubspot/v0/contact.go | 15 +- application/hubspot/v0/contact_test.go | 2 + application/hubspot/v0/custom_client.go | 9 + application/hubspot/v0/deal.go | 136 ++- application/hubspot/v0/deal_test.go | 13 +- application/hubspot/v0/get_all.go | 114 ++ application/hubspot/v0/get_all_test.go | 104 ++ application/hubspot/v0/main.go | 12 + application/hubspot/v0/owner.go | 132 ++ application/hubspot/v0/owner_test.go | 139 +++ application/hubspot/v0/thread.go | 114 +- application/hubspot/v0/thread_test.go | 3 +- application/hubspot/v0/ticket.go | 131 +- application/hubspot/v0/ticket_test.go | 26 +- 21 files changed, 1684 insertions(+), 676 deletions(-) create mode 100644 application/hubspot/v0/get_all.go create mode 100644 application/hubspot/v0/get_all_test.go create mode 100644 application/hubspot/v0/owner.go create mode 100644 application/hubspot/v0/owner_test.go diff --git a/application/hubspot/v0/README.mdx b/application/hubspot/v0/README.mdx index 834ae5da..b9e4de9f 100644 --- a/application/hubspot/v0/README.mdx +++ b/application/hubspot/v0/README.mdx @@ -12,13 +12,17 @@ It can carry out the following tasks: - [Create Contact](#create-contact) - [Get Deal](#get-deal) - [Create Deal](#create-deal) +- [Update Deal](#update-deal) - [Get Company](#get-company) - [Create Company](#create-company) - [Get Ticket](#get-ticket) - [Create Ticket](#create-ticket) +- [Update Ticket](#update-ticket) - [Get Thread](#get-thread) - [Insert Message](#insert-message) - [Retrieve Association](#retrieve-association) +- [Get Owner](#get-owner) +- [Get All](#get-all) @@ -166,6 +170,36 @@ Create new deal +### Update Deal + +Update existing deal + + +| Input | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Task ID (required) | `task` | string | `TASK_UPDATE_DEAL` | +| Deal ID (required) | `deal-id` | string | Input deal ID | +| Owner ID | `owner-id` | string | The user who is assigned to the object | +| Deal Name | `deal-name` | string | Deal name | +| Pipeline | `pipeline` | string | A pipeline is the place where you document and manage how your prospects move through the steps of your sales process. HubSpot uses interval value rather than the name displayed in the view | +| Deal Stage | `deal-stage` | string | Deal stages allow you to categorize and track the progress of the deals that you are working on. Default format is in small letters, all words are combined. Example: qualifiedtobuy. However, remember to check internal value for custom fields. | +| Amount | `amount` | number | The total amount of the deal | +| Deal Type | `deal-type` | string | The type of deal. Default format is in small letters, all words are combined. Example: newbusiness. However, remember to check internal value for custom fields. | +| Close Date | `close-date` | string | Date the deal was closed. Set automatically by HubSpot. Format is in ISO 8601. Example: 2024-07-01T11:47:40.388Z | +| Create Object -> Contact Association using contact IDs | `create-contacts-association` | array[string] | Existing contact IDs to be associated with the object | + + + +| Output | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Updated By User ID | `updated-by-user-id` | string | User ID that updated the deal | +| Updated At | `updated-at` | string | The time when the deal was updated | + + + + + + ### Get Company Get company information using company ID @@ -296,6 +330,35 @@ Create new ticket +### Update Ticket + +Update existing ticket + + +| Input | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Task ID (required) | `task` | string | `TASK_UPDATE_TICKET` | +| Owner ID | `owner-id` | string | The user who is assigned to the object | +| Ticket ID (required) | `ticket-id` | string | Input ticket ID | +| Ticket Name | `ticket-name` | string | Ticket name | +| Ticket Status | `ticket-status` | string | The pipeline stage that contains this ticket. Default format is number. Example: 1. However, remember to check internal value for custom fields. Note: In Instill AI, ticket-status is displayed as string because of the possible custom internal value. | +| Pipeline | `pipeline` | string | A pipeline organizes and tracks the progression of tickets through various stages of resolution within your support process. HubSpot uses interval value rather than the name displayed in the view | +| Categories | `categories` | array[string] | The main reason customer reached out for help. Default format is in capital letters. Example: BILLING_ISSUE. However, remember to check internal value for custom fields. | +| Priority | `priority` | string | The level of attention needed on the ticket. Default format is in capital letters. Example: MEDIUM. However, remember to check internal value for custom fields. | +| Source | `source` | string | Channel where ticket was originally submitted. Default format is in capital letters. Example: EMAIL | +| Create Object -> Contact Association using contact IDs | `create-contacts-association` | array[string] | Existing contact IDs to be associated with the object | + + + +| Output | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Updated At | `updated-at` | string | The time when the ticket was updated | + + + + + + ### Get Thread Retrieve all the messages inside a thread (conversation inbox). The messages will be sorted from most recent to least recent. Note: This task uses Conversation API from HubSpot, which is still in BETA. @@ -311,6 +374,7 @@ Retrieve all the messages inside a thread (conversation inbox). The messages wil | Output | ID | Type | Description | | :--- | :--- | :--- | :--- | | Messages | `results` | array[object] | An array of messages | +| Number of Messages | `no-of-messages` | integer | The number of messages in a thread | @@ -345,7 +409,7 @@ Insert message into a thread (only support email thread) ### Retrieve Association -Get the object IDs associated with contact ID (contact->objects). If you are trying to do the opposite (object->contacts), it is possible using the other tasks. Example: Go to get deal task to obtain deal->contacts +Get the object IDs associated with contact ID (contact->objects). If you are trying to do the opposite (object->contacts), it is possible using the other tasks. Example: Go to get deal task to obtain deal->contacts. Remember to check that the contact ID you input exists, because there won't be an error message if the contact ID doesn't exist. | Input | ID | Type | Description | @@ -359,6 +423,59 @@ Get the object IDs associated with contact ID (contact->objects). If you are try | Output | ID | Type | Description | | :--- | :--- | :--- | :--- | | Object ID Array | `object-ids` | array[string] | An array of object ID associated with the contact | +| Object IDs Length | `object-ids-length` | integer | The number of object IDs | + + + + + + +### Get Owner + +Get information about HubSpot owner using either owner ID or user ID. For more information about owner, please go to: https://developers.hubspot.com/docs/api/crm/owners + + +| Input | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Task ID (required) | `task` | string | `TASK_GET_OWNER` | +| ID Type (required) | `id-type` | string | Specify the type of ID you will use to get owner's information. | +| ID (required) | `id` | string | Can either be owner ID or user ID; according to the ID type you selected. | + + + +| Output | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| First Name | `first-name` | string | First name | +| Last Name | `last-name` | string | Last name | +| Email | `email` | string | Email | +| Owner ID | `owner-id` | string | Owner ID. Usually used to associate the owner with other objects. | +| User ID | `user-id` | string | User ID. Usually used to indicate the owner who performed the action. User ID can be seen in Update Deal task output. | +| Teams (optional) | `teams` | array[object] | The owner's teams information | +| Created At | `created-at` | string | Created at | +| Updated At | `updated-at` | string | Updated at | +| Archived | `archived` | boolean | Archived | + + + + + + +### Get All + +Get all the IDs for a specific object (e.g. all contact IDs) + + +| Input | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Task ID (required) | `task` | string | `TASK_GET_ALL` | +| Object Type (required) | `object-type` | string | The object which you want to get all IDs for | + + + +| Output | ID | Type | Description | +| :--- | :--- | :--- | :--- | +| Object ID Array | `object-ids` | array[string] | An array of object ID | +| Object IDs Length | `object-ids-length` | integer | The number of object IDs | diff --git a/application/hubspot/v0/association.go b/application/hubspot/v0/association.go index b77d0ed7..41774419 100644 --- a/application/hubspot/v0/association.go +++ b/application/hubspot/v0/association.go @@ -2,6 +2,7 @@ package hubspot import ( "fmt" + "strings" hubspot "github.com/belong-inc/go-hubspot" "github.com/instill-ai/component/base" @@ -14,8 +15,8 @@ import ( // API functions for Retrieve Association type RetrieveAssociationService interface { - GetThreadID(contactID string) (*TaskRetrieveAssociationThreadResp, error) - GetCrmID(contactID string, objectType string) (*TaskRetrieveAssociationCrmResp, error) + GetThreadID(contactID string, paging bool, pagingPath string) (*TaskRetrieveAssociationThreadResp, error) + GetCrmID(contactID string, objectType string, paging bool, pagingPath string) (interface{}, error) } type RetrieveAssociationServiceOp struct { @@ -24,28 +25,51 @@ type RetrieveAssociationServiceOp struct { client *hubspot.Client } -func (s *RetrieveAssociationServiceOp) GetThreadID(contactID string) (*TaskRetrieveAssociationThreadResp, error) { +func (s *RetrieveAssociationServiceOp) GetThreadID(contactID string, paging bool, pagingPath string) (*TaskRetrieveAssociationThreadResp, error) { resource := &TaskRetrieveAssociationThreadResp{} - if err := s.client.Get(s.retrieveThreadIDPath+contactID, resource, nil); err != nil { + + var path string + if !paging { + path = s.retrieveThreadIDPath + contactID + } else { + path = pagingPath + } + + if err := s.client.Get(path, resource, nil); err != nil { return nil, err } return resource, nil } -func (s *RetrieveAssociationServiceOp) GetCrmID(contactID string, objectType string) (*TaskRetrieveAssociationCrmResp, error) { - resource := &TaskRetrieveAssociationCrmResp{} +func (s *RetrieveAssociationServiceOp) GetCrmID(contactID string, objectType string, paging bool, pagingPath string) (interface{}, error) { - contactIDInput := TaskRetrieveAssociationCrmReqID{ContactID: contactID} + if !paging { + resource := &TaskRetrieveAssociationCrmResp{} - req := &TaskRetrieveAssociationCrmReq{} - req.Input = append(req.Input, contactIDInput) + contactIDInput := TaskRetrieveAssociationCrmReqID{ContactID: contactID} - path := s.retrieveCrmIDPath + "/" + objectType + "/batch/read" + req := &TaskRetrieveAssociationCrmReq{} + req.Input = append(req.Input, contactIDInput) + + path := s.retrieveCrmIDPath + "/" + objectType + "/batch/read" + + if err := s.client.Post(path, req, resource); err != nil { + return nil, err + } + + return resource, nil + } else { + + resource := &TaskRetrieveAssociationCrmPagingResp{} + + if err := s.client.Get(pagingPath, resource, nil); err != nil { + return nil, err + } + + return resource, nil - if err := s.client.Post(path, req, resource); err != nil { - return nil, err } - return resource, nil + } // Retrieve Association: use contact id to get the object ID associated with it @@ -55,6 +79,14 @@ type TaskRetrieveAssociationInput struct { ObjectType string `json:"object-type"` } +// This struct is used for both CRM and Threads +type taskRetrieveAssociationRespPaging struct { + Next struct { + Link string `json:"link"` + After string `json:"after"` + } `json:"next"` +} + // Retrieve Association Task is mainly divided into two: // 1. GetThreadID // 2. GetCrmID @@ -66,6 +98,7 @@ type TaskRetrieveAssociationThreadResp struct { Results []struct { ID string `json:"id"` } `json:"results"` + Paging *taskRetrieveAssociationRespPaging `json:"paging,omitempty"` } // For GetCrmID @@ -78,6 +111,10 @@ type TaskRetrieveAssociationCrmReqID struct { ContactID string `json:"id"` } +// RetrieveAssociation CRM can have 2 responses +// if it is more than 100, it will have a paging link, which user can do another API call to it. +// it has a different response format + type TaskRetrieveAssociationCrmResp struct { Results []taskRetrieveAssociationCrmRespResult `json:"results"` } @@ -86,12 +123,21 @@ type taskRetrieveAssociationCrmRespResult struct { IDArray []struct { ID string `json:"id"` } `json:"to"` + Paging *taskRetrieveAssociationRespPaging `json:"paging,omitempty"` +} + +type TaskRetrieveAssociationCrmPagingResp struct { + Results []struct { + ID string `json:"id"` + } `json:"results"` + Paging *taskRetrieveAssociationRespPaging `json:"paging,omitempty"` } // Retrieve Association Output type TaskRetrieveAssociationOutput struct { - ObjectIDs []string `json:"object-ids"` + ObjectIDs []string `json:"object-ids"` + ObjectIDsLength int `json:"object-ids-length"` } func (e *execution) RetrieveAssociation(input *structpb.Struct) (*structpb.Struct, error) { @@ -99,59 +145,110 @@ func (e *execution) RetrieveAssociation(input *structpb.Struct) (*structpb.Struc err := base.ConvertFromStructpb(input, &inputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert input to struct: %v", err) } // API calls to retrieve association for Threads and CRM objects are different var objectIDs []string - if inputStruct.ObjectType == "Threads" { - // To handle Threads - res, err := e.client.RetrieveAssociation.GetThreadID(inputStruct.ContactID) + switch inputStruct.ObjectType { + case "Contacts", "Companies", "Deals", "Tickets": + // To handle CRM objects + res, err := e.client.RetrieveAssociation.GetCrmID(inputStruct.ContactID, inputStruct.ObjectType, false, "") if err != nil { return nil, err } - if len(res.Results) == 0 { - return nil, fmt.Errorf("no object ID found") + crmRes := res.(*TaskRetrieveAssociationCrmResp) + + // no object ID associated with the contact ID, just break + if len(crmRes.Results) == 0 { + break } - objectIDs = make([]string, len(res.Results)) - for index, value := range res.Results { + // only take the first Result, because the input is only one contact id + objectIDs = make([]string, len(crmRes.Results[0].IDArray)) + for index, value := range crmRes.Results[0].IDArray { objectIDs[index] = value.ID } - } else { + // if there is a paging link, do another API call to get the rest of the object IDs + if crmRes.Results[0].Paging != nil { + for { + // need to trim because go-hubspot sdk get function will add the base URL + pagingRelativePath := strings.TrimPrefix(crmRes.Results[0].Paging.Next.Link, "https://api.hubapi.com/") - // To handle CRM objects - res, err := e.client.RetrieveAssociation.GetCrmID(inputStruct.ContactID, inputStruct.ObjectType) + res, err := e.client.RetrieveAssociation.GetCrmID(inputStruct.ContactID, inputStruct.ObjectType, true, pagingRelativePath) + + if err != nil { + return nil, err + } + + crmPagingRes := res.(*TaskRetrieveAssociationCrmPagingResp) + + for _, value := range crmPagingRes.Results { + objectIDs = append(objectIDs, value.ID) + } + + if crmPagingRes.Paging == nil || len(crmPagingRes.Results) == 0 { + break + } + } + } + + case "Threads": + res, err := e.client.RetrieveAssociation.GetThreadID(inputStruct.ContactID, false, "") if err != nil { return nil, err } if len(res.Results) == 0 { - return nil, fmt.Errorf("no object ID found") + break } - // only take the first Result, because the input is only one contact id - objectIDs = make([]string, len(res.Results[0].IDArray)) - for index, value := range res.Results[0].IDArray { + objectIDs = make([]string, len(res.Results)) + for index, value := range res.Results { objectIDs[index] = value.ID } + if res.Paging != nil { + for { + // need to trim because go-hubspot sdk get function will add the base URL + pagingRelativePath := strings.TrimPrefix(res.Paging.Next.Link, "https://api.hubapi.com/") + + res, err := e.client.RetrieveAssociation.GetThreadID(inputStruct.ContactID, true, pagingRelativePath) + + if err != nil { + return nil, err + } + + for _, value := range res.Results { + objectIDs = append(objectIDs, value.ID) + } + + if res.Paging == nil || len(res.Results) == 0 { + break + } + } + } + } + + if len(objectIDs) == 0 { + objectIDs = []string{} } outputStruct := TaskRetrieveAssociationOutput{ - ObjectIDs: objectIDs, + ObjectIDs: objectIDs, + ObjectIDsLength: len(objectIDs), } output, err := base.ConvertToStructpb(outputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert output to struct: %v", err) } return output, nil diff --git a/application/hubspot/v0/association_test.go b/application/hubspot/v0/association_test.go index cefebc5e..89937164 100644 --- a/application/hubspot/v0/association_test.go +++ b/application/hubspot/v0/association_test.go @@ -18,7 +18,7 @@ import ( type MockRetrieveAssociation struct{} -func (s *MockRetrieveAssociation) GetThreadID(contactID string) (*TaskRetrieveAssociationThreadResp, error) { +func (s *MockRetrieveAssociation) GetThreadID(contactID string, paging bool, pagingPath string) (*TaskRetrieveAssociationThreadResp, error) { var fakeThreadID TaskRetrieveAssociationThreadResp if contactID == "32027696539" { @@ -33,7 +33,7 @@ func (s *MockRetrieveAssociation) GetThreadID(contactID string) (*TaskRetrieveAs return &fakeThreadID, nil } -func (s *MockRetrieveAssociation) GetCrmID(contactID string, objectType string) (*TaskRetrieveAssociationCrmResp, error) { +func (s *MockRetrieveAssociation) GetCrmID(contactID string, objectType string, paging bool, pagingPath string) (interface{}, error) { var fakeCrmID TaskRetrieveAssociationCrmResp if contactID == "32027696539" { @@ -74,6 +74,7 @@ func TestComponent_ExecuteRetrieveAssociationTask(t *testing.T) { ObjectIDs: []string{ "7509711154", }, + ObjectIDsLength: 1, }, }, { @@ -86,6 +87,7 @@ func TestComponent_ExecuteRetrieveAssociationTask(t *testing.T) { ObjectIDs: []string{ "12345678900", }, + ObjectIDsLength: 1, }, }, } diff --git a/application/hubspot/v0/company.go b/application/hubspot/v0/company.go index f90d3174..2d0c12f5 100644 --- a/application/hubspot/v0/company.go +++ b/application/hubspot/v0/company.go @@ -1,7 +1,9 @@ package hubspot import ( + "fmt" "strconv" + "strings" hubspot "github.com/belong-inc/go-hubspot" "github.com/instill-ai/component/base" @@ -47,7 +49,7 @@ type TaskGetCompanyOutput struct { AnnualRevenue float64 `json:"annual-revenue,omitempty"` TotalRevenue float64 `json:"total-revenue,omitempty"` LinkedinPage string `json:"linkedin-page,omitempty"` - AssociatedContactIDs []string `json:"associated-contact-ids,omitempty"` + AssociatedContactIDs []string `json:"associated-contact-ids"` } func (e *execution) GetCompany(input *structpb.Struct) (*structpb.Struct, error) { @@ -56,13 +58,17 @@ func (e *execution) GetCompany(input *structpb.Struct) (*structpb.Struct, error) err := base.ConvertFromStructpb(input, &inputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert input to struct: %v", err) } res, err := e.client.CRM.Company.Get(inputStruct.CompanyID, &TaskGetCompanyResp{}, &hubspot.RequestQueryOption{Associations: []string{"contacts"}}) if err != nil { - return nil, err + if strings.Contains(err.Error(), "404") { + return nil, fmt.Errorf("404: unable to read response from hubspot: no company was found") + } else { + return nil, err + } } companyInfo := res.Properties.(*TaskGetCompanyResp) @@ -71,12 +77,20 @@ func (e *execution) GetCompany(input *structpb.Struct) (*structpb.Struct, error) var companyContactList []string if res.Associations != nil { + // for company, it is possible to have duplicate contacts, so need to remove all the duplicates. + + hash := make(map[string]bool) + companyContactAssociation := res.Associations.Contacts.Results - companyContactList = make([]string, len(companyContactAssociation)) - for index, value := range companyContactAssociation { - companyContactList[index] = value.ID + for _, value := range companyContactAssociation { + if _, ok := hash[value.ID]; !ok { + hash[value.ID] = true + companyContactList = append(companyContactList, value.ID) + } } + } else { + companyContactList = []string{} } // convert to outputStruct @@ -123,7 +137,7 @@ func (e *execution) GetCompany(input *structpb.Struct) (*structpb.Struct, error) output, err := base.ConvertToStructpb(outputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert output to struct: %v", err) } return output, nil @@ -176,7 +190,7 @@ func (e *execution) CreateCompany(input *structpb.Struct) (*structpb.Struct, err err := base.ConvertFromStructpb(input, &inputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert input to struct: %v", err) } var annualRevenue string @@ -215,7 +229,7 @@ func (e *execution) CreateCompany(input *structpb.Struct) (*structpb.Struct, err output, err := base.ConvertToStructpb(outputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert output to struct: %v", err) } // This section is for creating associations (company -> object) diff --git a/application/hubspot/v0/company_test.go b/application/hubspot/v0/company_test.go index 29bc6fc4..21161b45 100644 --- a/application/hubspot/v0/company_test.go +++ b/application/hubspot/v0/company_test.go @@ -76,12 +76,13 @@ func TestComponent_ExecuteGetCompanyTask(t *testing.T) { name: "ok - get company", input: "20620806729", wantResp: TaskGetCompanyOutput{ - CompanyName: "HubSpot", - CompanyDomain: "hubspot.com", - Description: "HubSpot offers a comprehensive cloud-based marketing and sales platform with integrated applications for attracting, converting, and delighting customers through inbound marketing strategies.", - PhoneNumber: "+1 888-482-7768", - Industry: "COMPUTER_SOFTWARE", - AnnualRevenue: 10000000000, + CompanyName: "HubSpot", + CompanyDomain: "hubspot.com", + Description: "HubSpot offers a comprehensive cloud-based marketing and sales platform with integrated applications for attracting, converting, and delighting customers through inbound marketing strategies.", + PhoneNumber: "+1 888-482-7768", + Industry: "COMPUTER_SOFTWARE", + AnnualRevenue: 10000000000, + AssociatedContactIDs: []string{}, }, } diff --git a/application/hubspot/v0/config/definition.json b/application/hubspot/v0/config/definition.json index d4a5de8a..f8203830 100644 --- a/application/hubspot/v0/config/definition.json +++ b/application/hubspot/v0/config/definition.json @@ -4,13 +4,17 @@ "TASK_CREATE_CONTACT", "TASK_GET_DEAL", "TASK_CREATE_DEAL", + "TASK_UPDATE_DEAL", "TASK_GET_COMPANY", "TASK_CREATE_COMPANY", "TASK_GET_TICKET", "TASK_CREATE_TICKET", + "TASK_UPDATE_TICKET", "TASK_GET_THREAD", "TASK_INSERT_MESSAGE", - "TASK_RETRIEVE_ASSOCIATION" + "TASK_RETRIEVE_ASSOCIATION", + "TASK_GET_OWNER", + "TASK_GET_ALL" ], "documentationUrl": "https://www.instill.tech/docs/component/application/hubspot", "icon": "assets/hubspot.svg", diff --git a/application/hubspot/v0/config/tasks.json b/application/hubspot/v0/config/tasks.json index 07c2b360..9fffc7a4 100644 --- a/application/hubspot/v0/config/tasks.json +++ b/application/hubspot/v0/config/tasks.json @@ -23,61 +23,41 @@ "description": "Existing contact IDs to be associated with the object", "title": "Create Object -> Contact Association using contact IDs", "type": "array", - "instillAcceptFormats": [ - "array:string" - ], + "instillAcceptFormats": ["array:string"], "items": { "type": "string" }, - "instillUpstreamTypes": [ - "value", - "reference" - ] + "instillUpstreamTypes": ["value", "reference"] }, "create-deals-association": { "description": "Existing deal IDs to be associated with the object", "title": "Create Object -> Deal Association using deal IDs", "type": "array", - "instillAcceptFormats": [ - "array:string" - ], + "instillAcceptFormats": ["array:string"], "items": { "type": "string" }, - "instillUpstreamTypes": [ - "value", - "reference" - ] + "instillUpstreamTypes": ["value", "reference"] }, "create-companies-association": { "description": "Existing company IDs to be associated with the object", "title": "Create Object -> Company Association using company IDs", "type": "array", - "instillAcceptFormats": [ - "array:string" - ], + "instillAcceptFormats": ["array:string"], "items": { "type": "string" }, - "instillUpstreamTypes": [ - "value", - "reference" - ] + "instillUpstreamTypes": ["value", "reference"] }, "create-tickets-association": { "description": "Existing ticket IDs to be associated with the object", "title": "Create Object -> Ticket Association using ticket IDs", "type": "array", - "instillAcceptFormats": [ - "array:string" - ], + "instillAcceptFormats": ["array:string"], "items": { "type": "string" }, - "instillUpstreamTypes": [ - "value", - "reference" - ] + "instillUpstreamTypes": ["value", "reference"] } }, "contact": { @@ -322,29 +302,19 @@ "input": { "description": "Input contact ID or email", "instillUIOrder": 0, - "instillEditOnNodeFields": [ - "contact-id-or-email" - ], + "instillEditOnNodeFields": ["contact-id-or-email"], "properties": { "contact-id-or-email": { "description": "Input contact ID or email. If the input has @, it will search the contact using email", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, "instillUIOrder": 0, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Contact ID or Email", "type": "string" } }, - "required": [ - "contact-id-or-email" - ], + "required": ["contact-id-or-email"], "title": "Input", "type": "object" }, @@ -406,9 +376,7 @@ "instillFormat": "string" } }, - "required": [ - "contact-id" - ], + "required": ["contact-id"], "title": "Output", "type": "object" } @@ -427,111 +395,57 @@ "properties": { "owner-id": { "$ref": "#/$defs/common/owner-id", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "email": { "$ref": "#/$defs/contact/email", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "first-name": { "$ref": "#/$defs/contact/first-name", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "last-name": { "$ref": "#/$defs/contact/last-name", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "phone-number": { "$ref": "#/$defs/contact/phone-number", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "company": { "$ref": "#/$defs/contact/company", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "job-title": { "$ref": "#/$defs/contact/job-title", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "lifecycle-stage": { "$ref": "#/$defs/contact/lifecycle-stage", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "lead-status": { "$ref": "#/$defs/contact/lead-status", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "create-deals-association": { "$ref": "#/$defs/common/create-deals-association", @@ -546,9 +460,7 @@ "instillUIOrder": 11 } }, - "required": [ - "email" - ], + "required": ["email"], "title": "Input", "type": "object" }, @@ -565,9 +477,7 @@ "instillFormat": "string" } }, - "required": [ - "contact-id" - ], + "required": ["contact-id"], "title": "Output", "type": "object" } @@ -577,29 +487,19 @@ "input": { "description": "Input deal ID", "instillUIOrder": 0, - "instillEditOnNodeFields": [ - "deal-id" - ], + "instillEditOnNodeFields": ["deal-id"], "properties": { "deal-id": { "description": "Input deal ID", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, "instillUIOrder": 0, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Deal ID", "type": "string" } }, - "required": [ - "deal-id" - ], + "required": ["deal-id"], "title": "Input", "type": "object" }, @@ -652,11 +552,7 @@ "instillUIorder": 8 } }, - "required": [ - "deal-name", - "pipeline", - "deal-stage" - ], + "required": ["deal-name", "pipeline", "deal-stage"], "title": "Output", "type": "object" } @@ -666,106 +562,56 @@ "input": { "description": "Deal information", "instillUIOrder": 0, - "instillEditOnNodeFields": [ - "deal-name", - "pipeline", - "deal-stage" - ], + "instillEditOnNodeFields": ["deal-name", "pipeline", "deal-stage"], "properties": { "owner-id": { "$ref": "#/$defs/common/owner-id", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "deal-name": { "$ref": "#/$defs/deal/deal-name", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "pipeline": { "$ref": "#/$defs/deal/pipeline", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "deal-stage": { "$ref": "#/$defs/deal/deal-stage", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "amount": { "$ref": "#/$defs/deal/amount", - "instillAcceptFormats": [ - "number" - ], + "instillAcceptFormats": ["number"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "deal-type": { "$ref": "#/$defs/deal/deal-type", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "close-date": { "$ref": "#/$defs/deal/close-date", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "create-contacts-association": { "$ref": "#/$defs/common/create-contacts-association", "instillUIOrder": 7 } }, - "required": [ - "deal-name", - "pipeline", - "deal-stage" - ], + "required": ["deal-name", "pipeline", "deal-stage"], "title": "Input", "type": "object" }, @@ -782,9 +628,105 @@ "instillFormat": "string" } }, - "required": [ - "deal-id" + "required": ["deal-id"], + "title": "Output", + "type": "object" + } + }, + "TASK_UPDATE_DEAL": { + "instillShortDescription": "Update existing deal", + "input": { + "description": "Deal information", + "instillUIOrder": 0, + "instillEditOnNodeFields": [ + "deal-id", + "deal-name", + "pipeline", + "deal-stage" ], + "properties": { + "deal-id": { + "description": "Input deal ID", + "instillAcceptFormats": ["string"], + "instillUIMultiline": false, + "instillUIOrder": 0, + "instillUpstreamTypes": ["value", "reference", "template"], + "title": "Deal ID", + "type": "string" + }, + "owner-id": { + "$ref": "#/$defs/common/owner-id", + "instillAcceptFormats": ["string"], + "instillUIMultiline": false, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "deal-name": { + "$ref": "#/$defs/deal/deal-name", + "instillAcceptFormats": ["string"], + "instillUIMultiline": true, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "pipeline": { + "$ref": "#/$defs/deal/pipeline", + "instillAcceptFormats": ["string"], + "instillUIMultiline": true, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "deal-stage": { + "$ref": "#/$defs/deal/deal-stage", + "instillAcceptFormats": ["string"], + "instillUIMultiline": true, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "amount": { + "$ref": "#/$defs/deal/amount", + "instillAcceptFormats": ["number"], + "instillUIMultiline": true, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "deal-type": { + "$ref": "#/$defs/deal/deal-type", + "instillAcceptFormats": ["string"], + "instillUIMultiline": true, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "close-date": { + "$ref": "#/$defs/deal/close-date", + "instillAcceptFormats": ["string"], + "instillUIMultiline": true, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "create-contacts-association": { + "$ref": "#/$defs/common/create-contacts-association", + "instillUIOrder": 7 + } + }, + "required": ["deal-id"], + "title": "Input", + "type": "object" + }, + "output": { + "description": "Obtain user ID that updated the deal and its update time.", + "instillUIOrder": 0, + "properties": { + "updated-by-user-id": { + "description": "User ID that updated the deal", + "instillUIOrder": 0, + "required": [], + "title": "Updated By User ID", + "type": "string", + "instillFormat": "string" + }, + "updated-at": { + "description": "The time when the deal was updated", + "instillUIOrder": 1, + "required": [], + "title": "Updated At", + "type": "string", + "instillFormat": "string" + } + }, + "required": ["updated-by-user-id", "updated-at"], "title": "Output", "type": "object" } @@ -794,29 +736,19 @@ "input": { "description": "Input company ID", "instillUIOrder": 0, - "instillEditOnNodeFields": [ - "company-id" - ], + "instillEditOnNodeFields": ["company-id"], "properties": { "company-id": { "description": "Input company ID", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, "instillUIOrder": 0, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Company ID", "type": "string" } }, - "required": [ - "company-id" - ], + "required": ["company-id"], "title": "Input", "type": "object" }, @@ -923,180 +855,94 @@ "properties": { "owner-id": { "$ref": "#/$defs/common/owner-id", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "company-name": { "$ref": "#/$defs/company/company-name", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "company-domain": { "$ref": "#/$defs/company/company-domain", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "description": { "$ref": "#/$defs/company/description", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "phone-number": { "$ref": "#/$defs/company/phone-number", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "industry": { "$ref": "#/$defs/company/industry", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "company-type": { "$ref": "#/$defs/company/company-type", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "city": { "$ref": "#/$defs/company/city", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "state": { "$ref": "#/$defs/company/state", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "country": { "$ref": "#/$defs/company/country", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "postal-code,": { "$ref": "#/$defs/company/postal-code", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "time-zone": { "$ref": "#/$defs/company/time-zone", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "annual-revenue": { "$ref": "#/$defs/company/annual-revenue", - "instillAcceptFormats": [ - "number" - ], + "instillAcceptFormats": ["number"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "linkedin-page": { "$ref": "#/$defs/company/linkedin-page", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "create-contacts-association": { "$ref": "#/$defs/common/create-contacts-association", "instillUIOrder": 15 } }, - "required": [ - "company-domain" - ], + "required": ["company-domain"], "title": "Input", "type": "object" }, @@ -1113,9 +959,7 @@ "instillFormat": "string" } }, - "required": [ - "company-id" - ], + "required": ["company-id"], "title": "Output", "type": "object" } @@ -1125,29 +969,19 @@ "input": { "description": "Input ticket ID", "instillUIOrder": 0, - "instillEditOnNodeFields": [ - "ticket-id" - ], + "instillEditOnNodeFields": ["ticket-id"], "properties": { "ticket-id": { "description": "Input ticket ID", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, "instillUIOrder": 0, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Ticket ID", "type": "string" } }, - "required": [ - "ticket-id" - ], + "required": ["ticket-id"], "title": "Input", "type": "object" }, @@ -1215,11 +1049,7 @@ "instillUIorder": 10 } }, - "required": [ - "ticket-name", - "ticket-status", - "pipeline" - ], + "required": ["ticket-name", "ticket-status", "pipeline"], "title": "Output", "type": "object" } @@ -1229,107 +1059,58 @@ "input": { "description": "Ticket information", "instillUIOrder": 0, - "instillEditOnNodeFields": [ - "ticket-name", - "ticket-status", - "pipeline" - ], + "instillEditOnNodeFields": ["ticket-name", "ticket-status", "pipeline"], "properties": { "owner-id": { "$ref": "#/$defs/common/owner-id", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "ticket-name": { "$ref": "#/$defs/ticket/ticket-name", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "ticket-status": { "$ref": "#/$defs/ticket/ticket-status", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "pipeline": { "$ref": "#/$defs/ticket/pipeline", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "categories": { "$ref": "#/$defs/ticket/categories", - "instillAcceptFormats": [ - "array:string" - ], + "instillAcceptFormats": ["array:string"], "items": { "type": "string" }, - "instillUpstreamTypes": [ - "value", - "reference" - ] + "instillUpstreamTypes": ["value", "reference"] }, "priority": { "$ref": "#/$defs/ticket/priority", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "source": { "$ref": "#/$defs/ticket/source", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ] + "instillUpstreamTypes": ["value", "reference", "template"] }, "create-contacts-association": { "$ref": "#/$defs/common/create-contacts-association", "instillUIOrder": 7 } }, - "required": [ - "ticket-name", - "ticket-status", - "pipeline" - ], + "required": ["ticket-name", "ticket-status", "pipeline"], "title": "Input", "type": "object" }, @@ -1346,9 +1127,99 @@ "instillFormat": "string" } }, - "required": [ - "ticket-id" + "required": ["ticket-id"], + "title": "Output", + "type": "object" + } + }, + "TASK_UPDATE_TICKET": { + "instillShortDescription": "Update existing ticket", + "input": { + "description": "Ticket information", + "instillUIOrder": 0, + "instillEditOnNodeFields": [ + "ticket-id", + "ticket-name", + "ticket-status", + "pipeline" ], + "properties": { + "ticket-id": { + "description": "Input ticket ID", + "instillAcceptFormats": ["string"], + "instillUIMultiline": false, + "instillUIOrder": 0, + "instillUpstreamTypes": ["value", "reference", "template"], + "title": "Ticket ID", + "type": "string" + }, + "owner-id": { + "$ref": "#/$defs/common/owner-id", + "instillAcceptFormats": ["string"], + "instillUIMultiline": false, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "ticket-name": { + "$ref": "#/$defs/ticket/ticket-name", + "instillAcceptFormats": ["string"], + "instillUIMultiline": true, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "ticket-status": { + "$ref": "#/$defs/ticket/ticket-status", + "instillAcceptFormats": ["string"], + "instillUIMultiline": true, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "pipeline": { + "$ref": "#/$defs/ticket/pipeline", + "instillAcceptFormats": ["string"], + "instillUIMultiline": true, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "categories": { + "$ref": "#/$defs/ticket/categories", + "instillAcceptFormats": ["array:string"], + "items": { + "type": "string" + }, + "instillUpstreamTypes": ["value", "reference"] + }, + "priority": { + "$ref": "#/$defs/ticket/priority", + "instillAcceptFormats": ["string"], + "instillUIMultiline": true, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "source": { + "$ref": "#/$defs/ticket/source", + "instillAcceptFormats": ["string"], + "instillUIMultiline": true, + "instillUpstreamTypes": ["value", "reference", "template"] + }, + "create-contacts-association": { + "$ref": "#/$defs/common/create-contacts-association", + "instillUIOrder": 7 + } + }, + "required": ["ticket-id"], + "title": "Input", + "type": "object" + }, + "output": { + "description": "Update time", + "instillUIOrder": 0, + "properties": { + "updated-at": { + "description": "The time when the ticket was updated", + "instillUIOrder": 0, + "required": [], + "title": "Updated At", + "type": "string", + "instillFormat": "string" + } + }, + "required": ["updated-at"], "title": "Output", "type": "object" } @@ -1358,29 +1229,19 @@ "input": { "description": "Input thread ID", "instillUIOrder": 0, - "instillEditOnNodeFields": [ - "thread-id" - ], + "instillEditOnNodeFields": ["thread-id"], "properties": { "thread-id": { "description": "Input thread ID", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, "instillUIOrder": 0, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Thread ID", "type": "string" } }, - "required": [ - "thread-id" - ], + "required": ["thread-id"], "title": "Input", "type": "object" }, @@ -1440,9 +1301,7 @@ "type": "string" } }, - "required": [ - "sender-actor-id" - ] + "required": ["sender-actor-id"] }, "recipients": { "description": "Recipients' information", @@ -1519,11 +1378,17 @@ "channel-account-id" ] } + }, + "no-of-messages": { + "description": "The number of messages in a thread", + "instillUIOrder": 1, + "required": [], + "title": "Number of Messages", + "type": "integer", + "instillFormat": "integer" } }, - "required": [ - "results" - ], + "required": ["results", "no-of-messages"], "title": "Output", "type": "object" } @@ -1544,31 +1409,19 @@ "properties": { "thread-id": { "description": "Input thread ID", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, "instillUIOrder": 0, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Thread ID", "type": "string" }, "sender-actor-id": { "description": "Input sender actor id. Example: A-12345678. To obtain this, it is recommended to use and copy the 'Get Thread task' sender output. For more information about actor id: https://developers.hubspot.com/beta-docs/guides/api/conversations/inbox-and-messages#get-actors", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, "instillUIOrder": 1, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Sender Actor ID", "type": "string" }, @@ -1576,60 +1429,37 @@ "description": "Recipients of the message", "title": "Recipients", "type": "array", - "instillAcceptFormats": [ - "array:string" - ], + "instillAcceptFormats": ["array:string"], "instillUIOrder": 2, - "instillUpstreamTypes": [ - "value", - "reference" - ], + "instillUpstreamTypes": ["value", "reference"], "items": { "type": "string" } }, "channel-account-id": { "description": "The ID of an account that is part of the channel-id channel. On an existing thread, it is recommended to copy channel-account-id of the most recent message on the thread.", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, "instillUIOrder": 3, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Channel Account ID", "type": "string" }, "subject": { "description": "The subject of the message", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, "instillUIOrder": 4, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Subject", "type": "string" }, "text": { "description": "The body of the message", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": true, "instillUIOrder": 5, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Text", "type": "string" } @@ -1658,58 +1488,39 @@ "instillFormat": "string" } }, - "required": [ - "status" - ], + "required": ["status"], "title": "Output", "type": "object" } }, "TASK_RETRIEVE_ASSOCIATION": { - "instillShortDescription": "Get the object IDs associated with contact ID (contact->objects). If you are trying to do the opposite (object->contacts), it is possible using the other tasks. Example: Go to get deal task to obtain deal->contacts", + "instillShortDescription": "Get the object IDs associated with contact ID (contact->objects). If you are trying to do the opposite (object->contacts), it is possible using the other tasks. Example: Go to get deal task to obtain deal->contacts. Remember to check that the contact ID you input exists, because there won't be an error message if the contact ID doesn't exist.", "input": { "description": "Contact ID and object type (CRM objects or Thread)", "instillUIOrder": 0, - "instillEditOnNodeFields": [ - "contact-id", - "object-type" - ], + "instillEditOnNodeFields": ["contact-id", "object-type"], "properties": { "contact-id": { "description": "Input contact ID", - "instillAcceptFormats": [ - "string" - ], + "instillAcceptFormats": ["string"], "instillUIMultiline": false, "instillUIOrder": 0, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Contact ID", "type": "string" }, "object-type": { + "enum": ["Deals", "Companies", "Tickets", "Threads"], + "example": "Deals", "description": "Input object type (CRM objects or 'Threads'). Note: CRM objects include 'Deals', 'Companies', 'Tickets', etc", - "instillAcceptFormats": [ - "string" - ], - "instillUIMultiline": false, + "instillAcceptFormats": ["string"], "instillUIOrder": 1, - "instillUpstreamTypes": [ - "value", - "reference", - "template" - ], + "instillUpstreamTypes": ["value", "reference", "template"], "title": "Object Type", "type": "string" } }, - "required": [ - "contact-id", - "object-type" - ], + "required": ["contact-id", "object-type"], "title": "Input", "type": "object" }, @@ -1728,13 +1539,227 @@ "type": "string", "description": "The object ID associated with the contact" } + }, + "object-ids-length": { + "description": "The number of object IDs", + "instillUIOrder": 1, + "required": [], + "title": "Object IDs Length", + "type": "integer", + "instillFormat": "integer" + } + }, + "required": ["object-ids", "object-ids-length"], + "title": "Output", + "type": "object" + } + }, + "TASK_GET_OWNER": { + "instillShortDescription": "Get information about HubSpot owner using either owner ID or user ID. For more information about owner, please go to: https://developers.hubspot.com/docs/api/crm/owners", + "input": { + "description": "Owner information and type", + "instillUIOrder": 0, + "instillEditOnNodeFields": ["id-type", "id"], + "properties": { + "id-type": { + "enum": ["Owner ID", "User ID"], + "example": "Owner ID", + "description": "Specify the type of ID you will use to get owner's information.", + "instillAcceptFormats": ["string"], + "instillUIOrder": 0, + "instillUpstreamTypes": ["value", "reference", "template"], + "title": "ID Type", + "type": "string" + }, + "id": { + "description": "Can either be owner ID or user ID; according to the ID type you selected.", + "instillAcceptFormats": ["string"], + "instillUIMultiline": true, + "instillUIOrder": 1, + "instillUpstreamTypes": ["value", "reference", "template"], + "title": "ID", + "type": "string" + } + }, + "required": ["id-type", "id"], + "title": "Input", + "type": "object" + }, + "output": { + "description": "Owner's detailed information", + "instillUIOrder": 0, + "properties": { + "first-name": { + "description": "First name", + "instillUIOrder": 0, + "required": [], + "title": "First Name", + "type": "string", + "instillFormat": "string" + }, + "last-name": { + "description": "Last name", + "instillUIOrder": 1, + "required": [], + "title": "Last Name", + "type": "string", + "instillFormat": "string" + }, + "email": { + "description": "Email", + "instillUIOrder": 2, + "required": [], + "title": "Email", + "type": "string", + "instillFormat": "string" + }, + "owner-id": { + "description": "Owner ID. Usually used to associate the owner with other objects.", + "instillUIOrder": 3, + "required": [], + "title": "Owner ID", + "type": "string", + "instillFormat": "string" + }, + "user-id": { + "description": "User ID. Usually used to indicate the owner who performed the action. User ID can be seen in Update Deal task output.", + "instillUIOrder": 4, + "required": [], + "title": "User ID", + "type": "string", + "instillFormat": "string" + }, + "teams": { + "description": "The owner's teams information", + "instillFormat": "array", + "instillUIOrder": 5, + "title": "Teams", + "type": "array", + "items": { + "title": "The owner's team information", + "type": "object", + "properties": { + "team-name": { + "description": "The name of the team", + "instillFormat": "string", + "instillUIOrder": 0, + "title": "Team Name", + "type": "string" + }, + "team-id": { + "description": "The ID of the team", + "instillFormat": "string", + "instillUIOrder": 1, + "title": "Team ID", + "type": "string" + }, + "team-primary": { + "description": "Indicate whether this team is the primary team of the owner", + "instillFormat": "boolean", + "instillUIOrder": 2, + "title": "Team Primary", + "type": "boolean" + } + }, + "required": ["team-name", "team-id", "team-primary"] + } + }, + "created-at": { + "description": "Created at", + "instillUIOrder": 6, + "required": [], + "title": "Created At", + "type": "string", + "instillFormat": "string" + }, + "updated-at": { + "description": "Updated at", + "instillUIOrder": 7, + "required": [], + "title": "Updated At", + "type": "string", + "instillFormat": "string" + }, + "archived": { + "description": "Archived", + "instillUIOrder": 8, + "required": [], + "title": "Archived", + "type": "boolean", + "instillFormat": "boolean" } }, "required": [ - "object-ids" + "first-name", + "last-name", + "email", + "owner-id", + "user-id", + "created-at", + "updated-at", + "archived" ], "title": "Output", "type": "object" } + }, + "TASK_GET_ALL": { + "instillShortDescription": "Get all the IDs for a specific object (e.g. all contact IDs)", + "input": { + "description": "Input", + "instillUIOrder": 0, + "instillEditOnNodeFields": ["object-type"], + "properties": { + "object-type": { + "enum": [ + "Contacts", + "Deals", + "Companies", + "Tickets", + "Threads", + "Owners" + ], + "example": "Contacts", + "description": "The object which you want to get all IDs for", + "instillAcceptFormats": ["string"], + "instillUIOrder": 0, + "instillUpstreamTypes": ["value", "reference", "template"], + "title": "Object Type", + "type": "string" + } + }, + "required": ["object-type"], + "title": "Input", + "type": "object" + }, + "output": { + "description": "All the IDs of the object", + "instillUIOrder": 0, + "properties": { + "object-ids": { + "description": "An array of object ID", + "instillUIOrder": 0, + "instillFormat": "array:string", + "title": "Object ID Array ", + "type": "array", + "items": { + "title": "Object ID", + "type": "string", + "description": "Object ID" + } + }, + "object-ids-length": { + "description": "The number of object IDs", + "instillUIOrder": 1, + "required": [], + "title": "Object IDs Length", + "type": "integer", + "instillFormat": "integer" + } + }, + "required": ["object-ids", "object-ids-length"], + "title": "Output", + "type": "object" + } } } diff --git a/application/hubspot/v0/contact.go b/application/hubspot/v0/contact.go index e05f90e5..bca06bf7 100644 --- a/application/hubspot/v0/contact.go +++ b/application/hubspot/v0/contact.go @@ -1,6 +1,7 @@ package hubspot import ( + "fmt" "strings" hubspot "github.com/belong-inc/go-hubspot" @@ -46,7 +47,7 @@ func (e *execution) GetContact(input *structpb.Struct) (*structpb.Struct, error) err := base.ConvertFromStructpb(input, &inputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert input to struct: %v", err) } uniqueKey := inputStruct.ContactIDOrEmail @@ -59,7 +60,11 @@ func (e *execution) GetContact(input *structpb.Struct) (*structpb.Struct, error) res, err := e.client.CRM.Contact.Get(uniqueKey, &TaskGetContactResp{}, &hubspot.RequestQueryOption{CustomProperties: []string{"phone"}}) if err != nil { - return nil, err + if strings.Contains(err.Error(), "404") { + return nil, fmt.Errorf("404: unable to read response from hubspot: no contact was found") + } else { + return nil, err + } } contactInfo := res.Properties.(*TaskGetContactResp) @@ -69,7 +74,7 @@ func (e *execution) GetContact(input *structpb.Struct) (*structpb.Struct, error) output, err := base.ConvertToStructpb(outputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert output to struct: %v", err) } return output, nil @@ -115,7 +120,7 @@ func (e *execution) CreateContact(input *structpb.Struct) (*structpb.Struct, err err := base.ConvertFromStructpb(input, &inputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert input to struct: %v", err) } req := TaskCreateContactReq{ @@ -143,7 +148,7 @@ func (e *execution) CreateContact(input *structpb.Struct) (*structpb.Struct, err output, err := base.ConvertToStructpb(outputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert output to struct: %v", err) } // This section is for creating associations (contact -> object) diff --git a/application/hubspot/v0/contact_test.go b/application/hubspot/v0/contact_test.go index 5f96c084..54398e36 100644 --- a/application/hubspot/v0/contact_test.go +++ b/application/hubspot/v0/contact_test.go @@ -34,6 +34,8 @@ func createMockClient() *CustomClient { Thread: &MockThread{}, RetrieveAssociation: &MockRetrieveAssociation{}, Ticket: &MockTicket{}, + GetAll: &MockGetAll{}, + Owner: &MockOwner{}, } return mockClient diff --git a/application/hubspot/v0/custom_client.go b/application/hubspot/v0/custom_client.go index 432360f3..02464110 100644 --- a/application/hubspot/v0/custom_client.go +++ b/application/hubspot/v0/custom_client.go @@ -11,6 +11,8 @@ type CustomClient struct { Thread ThreadService RetrieveAssociation RetrieveAssociationService Ticket TicketService + Owner OwnerService + GetAll GetAllService } func NewCustomClient(setAuthMethod hubspot.AuthMethod, opts ...hubspot.Option) (*CustomClient, error) { @@ -37,6 +39,13 @@ func NewCustomClient(setAuthMethod hubspot.AuthMethod, opts ...hubspot.Option) ( ticketPath: "crm/v3/objects/tickets", client: c, }, + Owner: &OwnerServiceOp{ + ownerPath: "crm/v3/owners", + client: c, + }, + GetAll: &GetAllServiceOp{ + client: c, + }, } return customC, nil diff --git a/application/hubspot/v0/deal.go b/application/hubspot/v0/deal.go index 4e23d012..21d39bf1 100644 --- a/application/hubspot/v0/deal.go +++ b/application/hubspot/v0/deal.go @@ -1,7 +1,9 @@ package hubspot import ( + "fmt" "strconv" + "strings" hubspot "github.com/belong-inc/go-hubspot" "github.com/instill-ai/component/base" @@ -15,14 +17,14 @@ type TaskGetDealInput struct { } type TaskGetDealResp struct { - OwnerID string `json:"hubspot_owner_id,omitempty"` - DealName string `json:"dealname"` - Pipeline string `json:"pipeline"` - DealStage string `json:"dealstage"` - Amount string `json:"amount,omitempty"` - DealType string `json:"dealtype,omitempty"` - CloseDate string `json:"closedate,omitempty"` - CreateDate string `json:"createdate"` + OwnerID string `json:"hubspot_owner_id,omitempty"` + DealName string `json:"dealname"` + Pipeline string `json:"pipeline"` + DealStage string `json:"dealstage"` + Amount string `json:"amount,omitempty"` + DealType string `json:"dealtype,omitempty"` + CloseDate *hubspot.HsTime `json:"closedate,omitempty"` + CreateDate *hubspot.HsTime `json:"createdate"` } type TaskGetDealOutput struct { @@ -34,7 +36,7 @@ type TaskGetDealOutput struct { DealType string `json:"deal-type,omitempty"` CreateDate string `json:"create-date"` CloseDate string `json:"close-date,omitempty"` - AssociatedContactIDs []string `json:"associated-contact-ids,omitempty"` + AssociatedContactIDs []string `json:"associated-contact-ids"` } func (e *execution) GetDeal(input *structpb.Struct) (*structpb.Struct, error) { @@ -43,7 +45,7 @@ func (e *execution) GetDeal(input *structpb.Struct) (*structpb.Struct, error) { err := base.ConvertFromStructpb(input, &inputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert input to struct: %v", err) } // get deal information @@ -51,7 +53,11 @@ func (e *execution) GetDeal(input *structpb.Struct) (*structpb.Struct, error) { res, err := e.client.CRM.Deal.Get(inputStruct.DealID, &TaskGetDealResp{}, &hubspot.RequestQueryOption{Associations: []string{"contacts"}}) if err != nil { - return nil, err + if strings.Contains(err.Error(), "404") { + return nil, fmt.Errorf("404: unable to read response from hubspot: no deal was found") + } else { + return nil, err + } } dealInfo := res.Properties.(*TaskGetDealResp) @@ -65,6 +71,8 @@ func (e *execution) GetDeal(input *structpb.Struct) (*structpb.Struct, error) { for index, value := range dealContactAssociation { dealContactList[index] = value.ID } + } else { + dealContactList = []string{} } // convert to outputStruct @@ -80,6 +88,14 @@ func (e *execution) GetDeal(input *structpb.Struct) (*structpb.Struct, error) { } } + var closeDate string + + if dealInfo.CloseDate == nil { + closeDate = "" + } else { + closeDate = dealInfo.CloseDate.String() + } + outputStruct := TaskGetDealOutput{ OwnerID: dealInfo.OwnerID, DealName: dealInfo.DealName, @@ -87,15 +103,14 @@ func (e *execution) GetDeal(input *structpb.Struct) (*structpb.Struct, error) { DealStage: dealInfo.DealStage, Amount: amount, DealType: dealInfo.DealType, - CreateDate: dealInfo.CreateDate, - CloseDate: dealInfo.CloseDate, + CreateDate: dealInfo.CreateDate.String(), + CloseDate: closeDate, AssociatedContactIDs: dealContactList, } output, err := base.ConvertToStructpb(outputStruct) - if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert output to struct: %v", err) } return output, nil @@ -135,7 +150,7 @@ func (e *execution) CreateDeal(input *structpb.Struct) (*structpb.Struct, error) err := base.ConvertFromStructpb(input, &inputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert input to struct: %v", err) } var amount string @@ -167,7 +182,7 @@ func (e *execution) CreateDeal(input *structpb.Struct) (*structpb.Struct, error) output, err := base.ConvertToStructpb(outputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert output to struct: %v", err) } // This section is for creating associations (deal -> object) @@ -181,3 +196,90 @@ func (e *execution) CreateDeal(input *structpb.Struct) (*structpb.Struct, error) return output, nil } + +// Update Deal + +type TaskUpdateDealInput struct { + DealID string `json:"deal-id"` + OwnerID string `json:"owner-id,omitempty"` + DealName string `json:"deal-name"` + Pipeline string `json:"pipeline"` + DealStage string `json:"deal-stage"` + Amount float64 `json:"amount"` + DealType string `json:"deal-type"` + CloseDate string `json:"close-date"` + CreateContactsAssociation []string `json:"create-contacts-association"` +} + +type TaskUpdateDealReq struct { + OwnerID string `json:"hubspot_owner_id,omitempty"` + DealName string `json:"dealname"` + Pipeline string `json:"pipeline"` + DealStage string `json:"dealstage"` + Amount string `json:"amount,omitempty"` + DealType string `json:"dealtype,omitempty"` + CloseDate string `json:"closedate,omitempty"` + UpdatedByUserID string `json:"hs_updated_by_user_id,omitempty"` +} + +// no response struct because it uses the req struct as well to store the response. + +type TaskUpdateDealOutput struct { + UpdatedByUserID string `json:"updated-by-user-id"` + UpdatedAt string `json:"updated-at"` //mostly just used to signal that it is updated successfully. +} + +func (e *execution) UpdateDeal(input *structpb.Struct) (*structpb.Struct, error) { + inputStruct := TaskUpdateDealInput{} + err := base.ConvertFromStructpb(input, &inputStruct) + + if err != nil { + return nil, fmt.Errorf("failed to convert input to struct: %v", err) + } + + var amount string + if inputStruct.Amount != 0 { + amount = strconv.FormatFloat(inputStruct.Amount, 'f', -1, 64) + } + + req := TaskUpdateDealReq{ + OwnerID: inputStruct.OwnerID, + DealName: inputStruct.DealName, + Pipeline: inputStruct.Pipeline, + DealStage: inputStruct.DealStage, + Amount: amount, + DealType: inputStruct.DealType, + CloseDate: inputStruct.CloseDate, + } + + res, err := e.client.CRM.Deal.Update(inputStruct.DealID, &req) + + if err != nil { + return nil, err + } + + // get the user ID which updated the deal + userID := res.Properties.(*TaskUpdateDealReq).UpdatedByUserID + + outputStruct := TaskUpdateDealOutput{ + UpdatedByUserID: userID, + UpdatedAt: res.UpdatedAt.String(), + } + + output, err := base.ConvertToStructpb(outputStruct) + + if err != nil { + return nil, fmt.Errorf("failed to convert output to struct: %v", err) + } + + // This section is for creating associations (deal -> object) + if len(inputStruct.CreateContactsAssociation) != 0 { + err := CreateAssociation(&inputStruct.DealID, &inputStruct.CreateContactsAssociation, "deal", "contact", e) + + if err != nil { + return nil, err + } + } + + return output, nil +} diff --git a/application/hubspot/v0/deal_test.go b/application/hubspot/v0/deal_test.go index cb66cdb4..94f30de5 100644 --- a/application/hubspot/v0/deal_test.go +++ b/application/hubspot/v0/deal_test.go @@ -3,6 +3,7 @@ package hubspot import ( "context" "testing" + "time" hubspot "github.com/belong-inc/go-hubspot" qt "github.com/frankban/quicktest" @@ -22,11 +23,12 @@ func (s *MockDeal) Get(dealID string, deal interface{}, option *hubspot.RequestQ var fakeDeal TaskGetDealResp if dealID == "20620806729" { + fakeDeal = TaskGetDealResp{ DealName: "Fake deal", Pipeline: "default", DealStage: "qualifiedtobuy", - CreateDate: "2024-07-09T02:22:06.140Z", + CreateDate: hubspot.NewTime(time.Date(2024, 7, 9, 0, 0, 0, 0, time.UTC)), } } @@ -72,10 +74,11 @@ func TestComponent_ExecuteGetDealTask(t *testing.T) { name: "ok - get deal", input: "20620806729", wantResp: TaskGetDealOutput{ - DealName: "Fake deal", - Pipeline: "default", - DealStage: "qualifiedtobuy", - CreateDate: "2024-07-09T02:22:06.140Z", + DealName: "Fake deal", + Pipeline: "default", + DealStage: "qualifiedtobuy", + CreateDate: "2024-07-09 00:00:00 +0000 UTC", + AssociatedContactIDs: []string{}, }, } diff --git a/application/hubspot/v0/get_all.go b/application/hubspot/v0/get_all.go new file mode 100644 index 00000000..ef0f00e4 --- /dev/null +++ b/application/hubspot/v0/get_all.go @@ -0,0 +1,114 @@ +package hubspot + +import ( + "fmt" + + hubspot "github.com/belong-inc/go-hubspot" + "github.com/instill-ai/component/base" + "google.golang.org/protobuf/types/known/structpb" +) + +// Get All is a custom feature +// Will implement it following go-hubspot sdk format + +// API function for Get All + +type GetAllService interface { + Get(objectType string, param string) (*TaskGetAllResp, error) +} + +type GetAllServiceOp struct { + client *hubspot.Client +} + +func (s *GetAllServiceOp) Get(objectType string, param string) (*TaskGetAllResp, error) { + resource := &TaskGetAllResp{} + + var relativePath string + switch objectType { + case "Companies", "Deals", "Contacts", "Tickets": + relativePath = "/crm/v3/objects/" + objectType + case "Threads": + relativePath = "/conversations/v3/conversations/threads" + case "Owners": + relativePath = "/crm/v3/owners" + } + + relativePath += param + + if err := s.client.Get(relativePath, resource, nil); err != nil { + return nil, err + } + return resource, nil +} + +// Get All + +type TaskGetAllInput struct { + ObjectType string `json:"object-type"` +} + +type TaskGetAllResp struct { + Results []taskGetAllRespResult `json:"results"` + Paging *taskGetAllRespPaging `json:"paging,omitempty"` +} + +type taskGetAllRespResult struct { + ID string `json:"id"` +} + +type taskGetAllRespPaging struct { + Next struct { + Link string `json:"link"` + After string `json:"after"` + } `json:"next"` +} + +type TaskGetAllOutput struct { + ObjectIDs []string `json:"object-ids"` + ObjectIDsLength int `json:"object-ids-length"` +} + +func (e *execution) GetAll(input *structpb.Struct) (*structpb.Struct, error) { + inputStruct := TaskGetAllInput{} + + err := base.ConvertFromStructpb(input, &inputStruct) + + if err != nil { + return nil, fmt.Errorf("failed to convert input to struct: %v", err) + } + + outputStruct := TaskGetAllOutput{} + + var param string + // need to use for loop because each API call can only retrieve 10 objects. So need to do multiple API calls to get all objects if it is more than 10. + for { + res, err := e.client.GetAll.Get(inputStruct.ObjectType, param) + + if err != nil { + return nil, err + } + + for _, result := range res.Results { + outputStruct.ObjectIDs = append(outputStruct.ObjectIDs, result.ID) + } + + if res.Paging == nil || len(res.Results) == 0 { + if len(outputStruct.ObjectIDs) == 0 { + outputStruct.ObjectIDs = []string{} + } + break + } + + param = fmt.Sprintf("?after=%s", res.Paging.Next.After) + } + + outputStruct.ObjectIDsLength = len(outputStruct.ObjectIDs) + + output, err := base.ConvertToStructpb(outputStruct) + if err != nil { + return nil, fmt.Errorf("failed to convert output to struct: %v", err) + } + + return output, nil +} diff --git a/application/hubspot/v0/get_all_test.go b/application/hubspot/v0/get_all_test.go new file mode 100644 index 00000000..92c076fd --- /dev/null +++ b/application/hubspot/v0/get_all_test.go @@ -0,0 +1,104 @@ +package hubspot + +import ( + "context" + "strings" + "testing" + + qt "github.com/frankban/quicktest" + "github.com/instill-ai/component/base" + "go.uber.org/zap" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" +) + +// mockClient is in contact_test.go + +// Mock GetAll struct and its functions +type MockGetAll struct{} + +func (s *MockGetAll) Get(objectType string, param string) (*TaskGetAllResp, error) { + var resource *TaskGetAllResp + if param == "" { + resource = &TaskGetAllResp{ + Results: []taskGetAllRespResult{ + { + ID: "11111111111", + }, + { + ID: "22222222222", + }, + }, + Paging: &taskGetAllRespPaging{ + Next: struct { + Link string `json:"link"` + After string `json:"after"` + }{ + Link: "https://api.hubapi.com/crm/v3/objects/contacts?after=xxxxxxxxx", + After: "xxxxxxxxxxx", + }, + }, + } + } else if strings.Contains(param, "after") { + resource = &TaskGetAllResp{ + Results: []taskGetAllRespResult{ + { + ID: "33333333333", + }, + { + ID: "44444444444", + }, + }, + } + } + + return resource, nil +} + +func TestComponent_ExecuteGetAllTask(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + bc := base.Component{Logger: zap.NewNop()} + connector := Init(bc) + + tc := struct { + name string + input string + wantResp TaskGetAllOutput + }{ + name: "ok - get all contacts", + input: "Contacts", + wantResp: TaskGetAllOutput{ + ObjectIDs: []string{"11111111111", "22222222222", "33333333333", "44444444444"}, + ObjectIDsLength: 4, + }, + } + + c.Run(tc.name, func(c *qt.C) { + setup, err := structpb.NewStruct(map[string]any{ + "token": bearerToken, + }) + c.Assert(err, qt.IsNil) + + e := &execution{ + ComponentExecution: base.ComponentExecution{Component: connector, SystemVariables: nil, Setup: setup, Task: taskGetAll}, + client: createMockClient(), + } + e.execute = e.GetAll + + pbInput, err := structpb.NewStruct(map[string]any{ + "object-type": tc.input, + }) + + c.Assert(err, qt.IsNil) + + res, err := e.Execute(ctx, []*structpb.Struct{pbInput}) + c.Assert(err, qt.IsNil) + + resJSON, err := protojson.Marshal(res[0]) + c.Assert(err, qt.IsNil) + + c.Check(resJSON, qt.JSONEquals, tc.wantResp) + + }) +} diff --git a/application/hubspot/v0/main.go b/application/hubspot/v0/main.go index bd59b566..471d6bb7 100644 --- a/application/hubspot/v0/main.go +++ b/application/hubspot/v0/main.go @@ -18,13 +18,17 @@ const ( taskCreateContact = "TASK_CREATE_CONTACT" taskGetDeal = "TASK_GET_DEAL" taskCreateDeal = "TASK_CREATE_DEAL" + taskUpdateDeal = "TASK_UPDATE_DEAL" taskGetCompany = "TASK_GET_COMPANY" taskCreateCompany = "TASK_CREATE_COMPANY" taskGetTicket = "TASK_GET_TICKET" taskCreateTicket = "TASK_CREATE_TICKET" + taskUpdateTicket = "TASK_UPDATE_TICKET" taskGetThread = "TASK_GET_THREAD" taskInsertMessage = "TASK_INSERT_MESSAGE" taskRetrieveAssociation = "TASK_RETRIEVE_ASSOCIATION" + taskGetOwner = "TASK_GET_OWNER" + taskGetAll = "TASK_GET_ALL" ) var ( @@ -91,6 +95,8 @@ func (c *component) CreateExecution(x base.ComponentExecution) (base.IExecution, e.execute = e.GetDeal case taskCreateDeal: e.execute = e.CreateDeal + case taskUpdateDeal: + e.execute = e.UpdateDeal case taskGetCompany: e.execute = e.GetCompany case taskCreateCompany: @@ -99,12 +105,18 @@ func (c *component) CreateExecution(x base.ComponentExecution) (base.IExecution, e.execute = e.GetTicket case taskCreateTicket: e.execute = e.CreateTicket + case taskUpdateTicket: + e.execute = e.UpdateTicket case taskGetThread: e.execute = e.GetThread case taskInsertMessage: e.execute = e.InsertMessage case taskRetrieveAssociation: e.execute = e.RetrieveAssociation + case taskGetOwner: + e.execute = e.GetOwner + case taskGetAll: + e.execute = e.GetAll default: return nil, fmt.Errorf("unsupported task") } diff --git a/application/hubspot/v0/owner.go b/application/hubspot/v0/owner.go new file mode 100644 index 00000000..c477f37b --- /dev/null +++ b/application/hubspot/v0/owner.go @@ -0,0 +1,132 @@ +package hubspot + +import ( + _ "embed" + "fmt" + "strings" + + hubspot "github.com/belong-inc/go-hubspot" + "github.com/instill-ai/component/base" + "google.golang.org/protobuf/types/known/structpb" +) + +// following go-hubspot sdk format + +// API function for Owner +// API documentation: https://developers.hubspot.com/docs/api/crm/owners + +type OwnerService interface { + Get(ownerInfo string, infoType string) (*TaskGetOwnerResp, error) +} + +type OwnerServiceOp struct { + client *hubspot.Client + ownerPath string +} + +func (s *OwnerServiceOp) Get(ownerInfo string, infoType string) (*TaskGetOwnerResp, error) { + resource := &TaskGetOwnerResp{} + if err := s.client.Get(s.ownerPath+"/"+ownerInfo+"/?idProperty="+infoType, resource, nil); err != nil { + return nil, err + } + + return resource, nil +} + +// Get Owner + +type TaskGetOwnerInputstruct struct { + IDType string `json:"id-type"` + ID string `json:"id"` +} + +type TaskGetOwnerResp struct { + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Email string `json:"email"` + OwnerID string `json:"id"` + UserID int `json:"userId"` + Teams []taskGetOwnerRespTeam `json:"teams,omitempty"` + + CreatedAt *hubspot.HsTime `json:"createdAt"` + UpdatedAt *hubspot.HsTime `json:"updatedAt"` + Archived bool `json:"archived"` +} + +type taskGetOwnerRespTeam struct { + Name string `json:"name"` + ID string `json:"id"` + Primary bool `json:"primary"` +} + +type TaskGetOwnerOutput struct { + FirstName string `json:"first-name"` + LastName string `json:"last-name"` + Email string `json:"email"` + OwnerID string `json:"owner-id"` + UserID string `json:"user-id"` //UserID can be string in other hubspot schema. I will stick to string as well so that it is consistent with other ID types + Teams []taskGetOwnerOutputTeam `json:"teams,omitempty"` + + CreatedAt string `json:"created-at"` + UpdatedAt string `json:"updated-at"` + Archived bool `json:"archived"` +} + +type taskGetOwnerOutputTeam struct { + Name string `json:"team-name"` + ID string `json:"team-id"` + Primary bool `json:"team-primary"` +} + +func (e *execution) GetOwner(input *structpb.Struct) (*structpb.Struct, error) { + + inputStruct := TaskGetOwnerInputstruct{} + + err := base.ConvertFromStructpb(input, &inputStruct) + + if err != nil { + return nil, fmt.Errorf("failed to convert input to struct: %v", err) + } + + var infoType string + switch inputStruct.IDType { + case "Owner ID": + infoType = "id" + case "User ID": + infoType = "userId" + } + + res, err := e.client.Owner.Get(inputStruct.ID, infoType) + + if err != nil { + if strings.Contains(err.Error(), "404") { + return nil, fmt.Errorf("404: unable to read response from hubspot: no owner was found") + } else { + return nil, err + } + } + + outputStruct := TaskGetOwnerOutput{ + FirstName: res.FirstName, + LastName: res.LastName, + Email: res.Email, + OwnerID: res.OwnerID, + UserID: fmt.Sprint(res.UserID), + CreatedAt: res.CreatedAt.String(), + UpdatedAt: res.UpdatedAt.String(), + Archived: res.Archived, + } + + // convert to output struct + for _, resTeam := range res.Teams { + outputTeam := taskGetOwnerOutputTeam(resTeam) + outputStruct.Teams = append(outputStruct.Teams, outputTeam) + } + + output, err := base.ConvertToStructpb(outputStruct) + if err != nil { + return nil, fmt.Errorf("failed to convert output to struct: %v", err) + } + + return output, nil +} diff --git a/application/hubspot/v0/owner_test.go b/application/hubspot/v0/owner_test.go new file mode 100644 index 00000000..0c3c1725 --- /dev/null +++ b/application/hubspot/v0/owner_test.go @@ -0,0 +1,139 @@ +package hubspot + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + hubspot "github.com/belong-inc/go-hubspot" + qt "github.com/frankban/quicktest" + "github.com/instill-ai/component/base" + "go.uber.org/zap" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" +) + +// mockClient is in contact_test.go + +// Mock Owner struct and its functions +type MockOwner struct{} + +func (s *MockOwner) Get(ownerInfo string, infoType string) (*TaskGetOwnerResp, error) { + + var fakeOwner *TaskGetOwnerResp + if infoType == "id" || infoType == "userId" { + if ownerInfo == "1111111111" || ownerInfo == "22222222" { + fakeOwner = &TaskGetOwnerResp{ + FirstName: "Random", + LastName: "Human", + Email: "randomhuman@gmail.com", + OwnerID: "1111111111", + UserID: 22222222, + CreatedAt: hubspot.NewTime(time.Date(2024, 7, 9, 0, 0, 0, 0, time.UTC)), + UpdatedAt: hubspot.NewTime(time.Date(2024, 7, 9, 0, 0, 0, 0, time.UTC)), + Archived: false, + } + } else { //if the owner id/userId is not found + + // actual response from API if the owner id/userId is not found + resp := ` + +
+ +Resource not found
+ + + + ` + + err := json.Unmarshal([]byte(resp), &fakeOwner) + + // go-hubspot sdk will return in this format + return nil, fmt.Errorf("404: unable to read response from hubspot:%v", err) + + } + } + + return fakeOwner, nil +} + +func TestComponent_ExecuteGetOwnerTask(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + bc := base.Component{Logger: zap.NewNop()} + connector := Init(bc) + + testcases := []struct { + name string + inputID string + inputType string + wantResp TaskGetOwnerOutput + wantErr string + }{ + { + name: "ok - get owner", + inputID: "1111111111", + inputType: "Owner ID", + wantResp: TaskGetOwnerOutput{ + FirstName: "Random", + LastName: "Human", + Email: "randomhuman@gmail.com", + OwnerID: "1111111111", + UserID: "22222222", + CreatedAt: "2024-07-09 00:00:00 +0000 UTC", + UpdatedAt: "2024-07-09 00:00:00 +0000 UTC", + Archived: false, + }, + wantErr: "", + }, + { + name: "nok - get owner: owner not found", + inputID: "9999999999", + inputType: "Owner ID", + wantErr: "404: unable to read response from hubspot: no owner was found", + }, + } + + for _, tc := range testcases { + c.Run(tc.name, func(c *qt.C) { + setup, err := structpb.NewStruct(map[string]any{ + "token": bearerToken, + }) + c.Assert(err, qt.IsNil) + + e := &execution{ + ComponentExecution: base.ComponentExecution{Component: connector, SystemVariables: nil, Setup: setup, Task: taskGetOwner}, + client: createMockClient(), + } + e.execute = e.GetOwner + + pbInput, err := structpb.NewStruct(map[string]any{ + "id": tc.inputID, + "id-type": tc.inputType, + }) + + c.Assert(err, qt.IsNil) + + res, err := e.Execute(ctx, []*structpb.Struct{pbInput}) + if tc.wantErr != "" { + c.Assert(err, qt.ErrorMatches, tc.wantErr) + return + } + + c.Assert(err, qt.IsNil) + + resJSON, err := protojson.Marshal(res[0]) + c.Assert(err, qt.IsNil) + + c.Check(resJSON, qt.JSONEquals, tc.wantResp) + + }) + } +} diff --git a/application/hubspot/v0/thread.go b/application/hubspot/v0/thread.go index e9cc9511..903f9444 100644 --- a/application/hubspot/v0/thread.go +++ b/application/hubspot/v0/thread.go @@ -14,7 +14,7 @@ import ( // API functions for Thread type ThreadService interface { - Get(threadID string) (*TaskGetThreadResp, error) + Get(threadID string, param string) (*TaskGetThreadResp, error) Insert(threadID string, message *TaskInsertMessageReq) (*TaskInsertMessageResp, error) } @@ -23,9 +23,9 @@ type ThreadServiceOp struct { client *hubspot.Client } -func (s *ThreadServiceOp) Get(threadID string) (*TaskGetThreadResp, error) { +func (s *ThreadServiceOp) Get(threadID string, param string) (*TaskGetThreadResp, error) { resource := &TaskGetThreadResp{} - if err := s.client.Get(s.threadPath+"/"+threadID+"/messages", resource, nil); err != nil { + if err := s.client.Get(s.threadPath+"/"+threadID+"/messages"+param, resource, nil); err != nil { return nil, err } return resource, nil @@ -51,6 +51,7 @@ type TaskGetThreadInput struct { type TaskGetThreadResp struct { Results []taskGetThreadRespResult `json:"results"` + Paging *taskGetThreadRespPaging `json:"paging,omitempty"` } type taskGetThreadRespResult struct { @@ -64,6 +65,13 @@ type taskGetThreadRespResult struct { Type string `json:"type,omitempty"` } +type taskGetThreadRespPaging struct { + Next struct { + Link string `json:"link"` + After string `json:"after"` + } `json:"next"` +} + type taskGetThreadRespUser struct { Name string `json:"name,omitempty"` DeliveryIdentifier taskGetThreadRespIdentifier `json:"deliveryIdentifier,omitempty"` @@ -78,7 +86,8 @@ type taskGetThreadRespIdentifier struct { // Get Thread Output structs type TaskGetThreadOutput struct { - Results []taskGetThreadOutputResult `json:"results"` + Results []taskGetThreadOutputResult `json:"results"` + NoOfMessages int `json:"no-of-messages"` } type taskGetThreadOutputResult struct { @@ -113,65 +122,80 @@ func (e *execution) GetThread(input *structpb.Struct) (*structpb.Struct, error) err := base.ConvertFromStructpb(input, &inputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert input to struct: %v", err) } - res, err := e.client.Thread.Get(inputStruct.ThreadID) - - if err != nil { - return nil, err - } - - // convert to output struct - + var param string outputStruct := TaskGetThreadOutput{} + for { + res, err := e.client.Thread.Get(inputStruct.ThreadID, param) - for _, value1 := range res.Results { - // this way, the output will only contain the actual messages in the thread (ignore system message from hubspot) - if value1.Type != "MESSAGE" { - continue + if err != nil { + return nil, err } - resultOutput := taskGetThreadOutputResult{ - CreatedAt: value1.CreatedAt, - Text: value1.Text, - Subject: value1.Subject, - ChannelID: value1.ChannelID, - ChannelAccountID: value1.ChannelAccountID, + if len(res.Results) == 0 { + break } - // there should only be one sender - // sender - if len(value1.Senders) > 0 { - value2 := value1.Senders[0] - userSenderOutput := taskGetThreadOutputSender{ - Name: value2.Name, - Type: value2.DeliveryIdentifier.Type, - Value: value2.DeliveryIdentifier.Value, - ActorID: value2.ActorID, + // convert to output struct + + for _, value1 := range res.Results { + // this way, the output will only contain the actual messages in the thread (ignore system message from hubspot) + if value1.Type != "MESSAGE" { + continue } - resultOutput.Sender = userSenderOutput - } - // recipient - for _, value3 := range value1.Recipients { - userRecipientOutput := taskGetThreadOutputRecipient{ - Name: value3.Name, - Type: value3.DeliveryIdentifier.Type, - Value: value3.DeliveryIdentifier.Value, + resultOutput := taskGetThreadOutputResult{ + CreatedAt: value1.CreatedAt, + Text: value1.Text, + Subject: value1.Subject, + ChannelID: value1.ChannelID, + ChannelAccountID: value1.ChannelAccountID, } - resultOutput.Recipients = append(resultOutput.Recipients, userRecipientOutput) + // there should only be one sender + // sender + if len(value1.Senders) > 0 { + value2 := value1.Senders[0] + userSenderOutput := taskGetThreadOutputSender{ + Name: value2.Name, + Type: value2.DeliveryIdentifier.Type, + Value: value2.DeliveryIdentifier.Value, + ActorID: value2.ActorID, + } + resultOutput.Sender = userSenderOutput + } + + // recipient + for _, value3 := range value1.Recipients { + userRecipientOutput := taskGetThreadOutputRecipient{ + Name: value3.Name, + Type: value3.DeliveryIdentifier.Type, + Value: value3.DeliveryIdentifier.Value, + } + + resultOutput.Recipients = append(resultOutput.Recipients, userRecipientOutput) + + } + + outputStruct.Results = append(outputStruct.Results, resultOutput) } - outputStruct.Results = append(outputStruct.Results, resultOutput) + // if there is no more messages/ page to be read, break + if res.Paging == nil || len(res.Results) == 0 { + break + } else { + param = fmt.Sprintf("?after=%s", res.Paging.Next.After) + } } + outputStruct.NoOfMessages = len(outputStruct.Results) output, err := base.ConvertToStructpb(outputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert output to struct: %v", err) } return output, nil @@ -234,7 +258,7 @@ func (e *execution) InsertMessage(input *structpb.Struct) (*structpb.Struct, err err := base.ConvertFromStructpb(input, &inputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert input to struct: %v", err) } recipients := make([]taskInsertMessageReqRecipient, len(inputStruct.Recipients)) @@ -275,7 +299,7 @@ func (e *execution) InsertMessage(input *structpb.Struct) (*structpb.Struct, err output, err := base.ConvertToStructpb(outputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert output to struct: %v", err) } return output, nil diff --git a/application/hubspot/v0/thread_test.go b/application/hubspot/v0/thread_test.go index 9694c5e6..7ddcb8ef 100644 --- a/application/hubspot/v0/thread_test.go +++ b/application/hubspot/v0/thread_test.go @@ -17,7 +17,7 @@ import ( // Mock Thread struct and its functions type MockThread struct{} -func (s *MockThread) Get(threadID string) (*TaskGetThreadResp, error) { +func (s *MockThread) Get(threadID string, param string) (*TaskGetThreadResp, error) { var fakeThread TaskGetThreadResp if threadID == "7509711154" { @@ -99,6 +99,7 @@ func TestComponent_ExecuteGetThreadTask(t *testing.T) { ChannelAccountID: "638727358", }, }, + NoOfMessages: 1, }, } diff --git a/application/hubspot/v0/ticket.go b/application/hubspot/v0/ticket.go index 38007ab0..0f01a9d8 100644 --- a/application/hubspot/v0/ticket.go +++ b/application/hubspot/v0/ticket.go @@ -1,6 +1,7 @@ package hubspot import ( + "fmt" "strings" hubspot "github.com/belong-inc/go-hubspot" @@ -15,6 +16,7 @@ import ( type TicketService interface { Get(ticketID string) (*hubspot.ResponseResource, error) Create(ticket *TaskCreateTicketReq) (*hubspot.ResponseResource, error) + Update(ticketID string, ticket *TaskUpdateTicketReq) (*hubspot.ResponseResource, error) } type TicketServiceOp struct { @@ -54,6 +56,15 @@ func (s *TicketServiceOp) Create(ticket *TaskCreateTicketReq) (*hubspot.Response return resource, nil } +func (s *TicketServiceOp) Update(ticketID string, ticket *TaskUpdateTicketReq) (*hubspot.ResponseResource, error) { + req := &hubspot.RequestPayload{Properties: ticket} + resource := &hubspot.ResponseResource{} //leave the Properties blank because we don't use any values from the properties. + if err := s.client.Patch(s.ticketPath+"/"+ticketID, req, resource); err != nil { + return nil, err + } + return resource, nil +} + // Get Ticket type TaskGetTicketInput struct { @@ -61,17 +72,17 @@ type TaskGetTicketInput struct { } type TaskGetTicketResp struct { - OwnerID string `json:"hubspot_owner_id,omitempty"` - TicketName string `json:"subject"` - TicketStatus string `json:"hs_pipeline_stage"` - Pipeline string `json:"hs_pipeline"` - Category string `json:"hs_ticket_category,omitempty"` - Priority string `json:"hs_ticket_priority,omitempty"` - Source string `json:"source_type,omitempty"` - RecordSource string `json:"hs_object_source_label,omitempty"` - CreateDate string `json:"createdate"` - LastModifiedDate string `json:"hs_lastmodifieddate"` - TicketID string `json:"hs_object_id"` + OwnerID string `json:"hubspot_owner_id,omitempty"` + TicketName string `json:"subject"` + TicketStatus string `json:"hs_pipeline_stage"` + Pipeline string `json:"hs_pipeline"` + Category string `json:"hs_ticket_category,omitempty"` + Priority string `json:"hs_ticket_priority,omitempty"` + Source string `json:"source_type,omitempty"` + RecordSource string `json:"hs_object_source_label,omitempty"` + CreateDate *hubspot.HsTime `json:"createdate"` + LastModifiedDate *hubspot.HsTime `json:"hs_lastmodifieddate"` + TicketID string `json:"hs_object_id"` } type TaskGetTicketOutput struct { @@ -79,13 +90,13 @@ type TaskGetTicketOutput struct { TicketName string `json:"ticket-name"` TicketStatus string `json:"ticket-status"` Pipeline string `json:"pipeline"` - Category []string `json:"categories,omitempty"` + Category []string `json:"categories"` Priority string `json:"priority,omitempty"` Source string `json:"source,omitempty"` RecordSource string `json:"record-source,omitempty"` CreateDate string `json:"create-date"` LastModifiedDate string `json:"last-modified-date"` - AssociatedContactIDs []string `json:"associated-contact-ids,omitempty"` + AssociatedContactIDs []string `json:"associated-contact-ids"` } func (e *execution) GetTicket(input *structpb.Struct) (*structpb.Struct, error) { @@ -95,12 +106,16 @@ func (e *execution) GetTicket(input *structpb.Struct) (*structpb.Struct, error) err := base.ConvertFromStructpb(input, &inputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert input to struct: %v", err) } res, err := e.client.Ticket.Get(inputStruct.TicketID) if err != nil { - return nil, err + if strings.Contains(err.Error(), "404") { + return nil, fmt.Errorf("404: unable to read response from hubspot: no ticket was found") + } else { + return nil, err + } } ticketInfo := res.Properties.(*TaskGetTicketResp) @@ -114,11 +129,15 @@ func (e *execution) GetTicket(input *structpb.Struct) (*structpb.Struct, error) for index, value := range ticketContactAssociation { ticketContactList[index] = value.ID } + } else { + ticketContactList = []string{} } var categoryValues []string if ticketInfo.Category != "" { categoryValues = strings.Split(ticketInfo.Category, ";") + } else { + categoryValues = []string{} } outputStruct := TaskGetTicketOutput{ @@ -130,14 +149,14 @@ func (e *execution) GetTicket(input *structpb.Struct) (*structpb.Struct, error) Priority: ticketInfo.Priority, Source: ticketInfo.Source, RecordSource: ticketInfo.RecordSource, - CreateDate: ticketInfo.CreateDate, - LastModifiedDate: ticketInfo.LastModifiedDate, + CreateDate: ticketInfo.CreateDate.String(), + LastModifiedDate: ticketInfo.LastModifiedDate.String(), AssociatedContactIDs: ticketContactList, } output, err := base.ConvertToStructpb(outputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert output to struct: %v", err) } return output, nil @@ -176,7 +195,7 @@ func (e *execution) CreateTicket(input *structpb.Struct) (*structpb.Struct, erro err := base.ConvertFromStructpb(input, &inputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert input to struct: %v", err) } req := TaskCreateTicketReq{ @@ -203,7 +222,7 @@ func (e *execution) CreateTicket(input *structpb.Struct) (*structpb.Struct, erro output, err := base.ConvertToStructpb(outputStruct) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert output to struct: %v", err) } // This section is for creating associations (ticket -> object) @@ -217,3 +236,75 @@ func (e *execution) CreateTicket(input *structpb.Struct) (*structpb.Struct, erro return output, nil } + +// Update Ticket +type TaskUpdateTicketInput struct { + TicketID string `json:"ticket-id"` + OwnerID string `json:"owner-id"` + TicketName string `json:"ticket-name"` + TicketStatus string `json:"ticket-status"` + Pipeline string `json:"pipeline"` + Category []string `json:"categories"` + Priority string `json:"priority"` + Source string `json:"source"` + CreateContactsAssociation []string `json:"create-contacts-association"` +} + +type TaskUpdateTicketReq struct { + OwnerID string `json:"hubspot_owner_id,omitempty"` + TicketName string `json:"subject"` + TicketStatus string `json:"hs_pipeline_stage"` + Pipeline string `json:"hs_pipeline"` + Category string `json:"hs_ticket_category,omitempty"` + Priority string `json:"hs_ticket_priority,omitempty"` + Source string `json:"source_type,omitempty"` +} + +type TaskUpdateTicketOutput struct { + UpdatedAt string `json:"updated-at"` //mostly just used to signal that it is updated successfully. +} // unlike UpdateDeal, UpdateTicket doesn't have UpdatedByUserID because the API response doesn't return that value for some reason. + +func (e *execution) UpdateTicket(input *structpb.Struct) (*structpb.Struct, error) { + + inputStruct := TaskUpdateTicketInput{} + err := base.ConvertFromStructpb(input, &inputStruct) + + if err != nil { + return nil, fmt.Errorf("failed to convert input to struct: %v", err) + } + + req := TaskUpdateTicketReq{ + OwnerID: inputStruct.OwnerID, + TicketName: inputStruct.TicketName, + TicketStatus: inputStruct.TicketStatus, + Pipeline: inputStruct.Pipeline, + Category: strings.Join(inputStruct.Category, ";"), + Priority: inputStruct.Priority, + Source: inputStruct.Source, + } + + res, err := e.client.Ticket.Update(inputStruct.TicketID, &req) + + if err != nil { + return nil, err + } + + outputStruct := TaskUpdateTicketOutput{UpdatedAt: res.UpdatedAt.String()} + + output, err := base.ConvertToStructpb(outputStruct) + + if err != nil { + return nil, fmt.Errorf("failed to convert output to struct: %v", err) + } + + // This section is for creating associations (ticket -> object) + if len(inputStruct.CreateContactsAssociation) != 0 { + err := CreateAssociation(&inputStruct.TicketID, &inputStruct.CreateContactsAssociation, "ticket", "contact", e) + + if err != nil { + return nil, err + } + } + + return output, nil +} diff --git a/application/hubspot/v0/ticket_test.go b/application/hubspot/v0/ticket_test.go index fdcb8f54..4d905d45 100644 --- a/application/hubspot/v0/ticket_test.go +++ b/application/hubspot/v0/ticket_test.go @@ -3,6 +3,7 @@ package hubspot import ( "context" "testing" + "time" hubspot "github.com/belong-inc/go-hubspot" qt "github.com/frankban/quicktest" @@ -22,10 +23,12 @@ func (s *MockTicket) Get(ticketID string) (*hubspot.ResponseResource, error) { var fakeTicket TaskGetTicketResp if ticketID == "2865646368" { fakeTicket = TaskGetTicketResp{ - TicketName: "HubSpot - New Query (Sample Query)", - TicketStatus: "1", - Pipeline: "0", - Category: "PRODUCT_ISSUE;BILLING_ISSUE", + TicketName: "HubSpot - New Query (Sample Query)", + TicketStatus: "1", + Pipeline: "0", + Category: "PRODUCT_ISSUE;BILLING_ISSUE", + CreateDate: hubspot.NewTime(time.Date(2024, 7, 9, 0, 0, 0, 0, time.UTC)), + LastModifiedDate: hubspot.NewTime(time.Date(2024, 7, 9, 0, 0, 0, 0, time.UTC)), } } @@ -49,6 +52,10 @@ func (s *MockTicket) Create(ticket *TaskCreateTicketReq) (*hubspot.ResponseResou return ret, nil } +func (s *MockTicket) Update(ticketID string, ticket *TaskUpdateTicketReq) (*hubspot.ResponseResource, error) { + return nil, nil +} + func TestComponent_ExecuteGetTicketTask(t *testing.T) { c := qt.New(t) ctx := context.Background() @@ -63,10 +70,13 @@ func TestComponent_ExecuteGetTicketTask(t *testing.T) { name: "ok - get ticket", input: "2865646368", wantResp: TaskGetTicketOutput{ - TicketName: "HubSpot - New Query (Sample Query)", - TicketStatus: "1", - Pipeline: "0", - Category: []string{"PRODUCT_ISSUE", "BILLING_ISSUE"}, + TicketName: "HubSpot - New Query (Sample Query)", + TicketStatus: "1", + Pipeline: "0", + Category: []string{"PRODUCT_ISSUE", "BILLING_ISSUE"}, + CreateDate: "2024-07-09 00:00:00 +0000 UTC", + LastModifiedDate: "2024-07-09 00:00:00 +0000 UTC", + AssociatedContactIDs: []string{}, }, }