Merge branch 'contacts' into 'staging'

Contacts

See merge request backend/quiz/amocrm!16
This commit is contained in:
Mikhail 2024-06-21 18:52:06 +00:00
commit 4d591d9fa1
6 changed files with 553 additions and 13 deletions

2
go.mod

@ -12,7 +12,7 @@ require (
github.com/twmb/franz-go v1.16.1
go.uber.org/zap v1.27.0
google.golang.org/protobuf v1.33.0
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240619180437-6d449201156e
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240621172746-5cbeb2b88f0a
)
require (

6
go.sum

@ -136,3 +136,9 @@ penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240619123238-dd5
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240619123238-dd521165c55a/go.mod h1:n66zm88Dh12+idyfqh0vU5nd9BZYxM6Pv0XYnmy0398=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240619180437-6d449201156e h1:ymGwJmQls96uNdY1301zLwJtC8YqSinZN0Rs+UGvYgA=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240619180437-6d449201156e/go.mod h1:n66zm88Dh12+idyfqh0vU5nd9BZYxM6Pv0XYnmy0398=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240621103513-9616b086fa4a h1:RPuYgZri2A15IF/kOshgstLdf6L0tV9m4FhFQUfzW/A=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240621103513-9616b086fa4a/go.mod h1:n66zm88Dh12+idyfqh0vU5nd9BZYxM6Pv0XYnmy0398=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240621150128-4b70022ad675 h1:w9I3PSvR3XsxL48fgyi65zLhbnpJXUknKz3ZqkCltzI=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240621150128-4b70022ad675/go.mod h1:n66zm88Dh12+idyfqh0vU5nd9BZYxM6Pv0XYnmy0398=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240621172746-5cbeb2b88f0a h1:all9W8RrLcr+47k6dAQgdyjVGuXbO3H29AkMGMTcJ28=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240621172746-5cbeb2b88f0a/go.mod h1:n66zm88Dh12+idyfqh0vU5nd9BZYxM6Pv0XYnmy0398=

@ -0,0 +1,72 @@
package models
type CreateContactReq struct {
Name string `json:"name"` // Название контакта
FirstName string `json:"first_name"` // Имя контакта
LastName string `json:"last_name"` // Фамилия контакта
ResponsibleUserID int32 `json:"responsible_user_id"` // ID пользователя, ответственного за контакт
CreatedBy int64 `json:"created_by"` // ID пользователя, создавший контакт
UpdatedBy int64 `json:"updated_by"` // ID пользователя, изменивший контакт
CreatedAt int64 `json:"created_at"` // Дата создания контакта, передается в Unix Timestamp
UpdatedAt int64 `json:"updated_at"` // Дата изменения контакта, передается в Unix Timestamp
CustomFieldsValues []FieldsValues `json:"custom_fields_values"`
TagsToAdd []Tag `json:"tags_to_add"`
Embed Embedd `json:"_embedded"`
RequestID string `json:"request_id"`
}
type ContactResponse struct {
Links struct {
Self struct {
Href string `json:"href"`
} `json:"self"`
} `json:"_links"`
Embedded struct {
Contacts []struct {
ID int32 `json:"id"`
RequestID string `json:"request_id"`
Links struct {
Self struct {
Href string `json:"href"`
} `json:"self"`
} `json:"_links"`
} `json:"contacts"`
} `json:"_embedded"`
}
type LinkedContactReq struct {
EntityID int32 `json:"entity_id"` // ID главной сущности
ToEntityID int32 `json:"to_entity_id"` // ID связанной сущности
ToEntityType string `json:"to_entity_type"` // Тип связанной сущности (leads, contacts, companies, customers, catalog_elements)
Metadata struct {
CatalogID int `json:"catalog_id"` // ID каталога
Quantity int `json:"quantity"` // Количество прикрепленных элементов каталогов
IsMain bool `json:"is_main"` // Является ли контакт главным
UpdatedBy int `json:"updated_by"` // ID пользователя, от имени которого осуществляется прикрепление
PriceID int `json:"price_id"` // ID поля типа Цена, которое будет установлено для привязанного элемента в контексте сущности
} `json:"metadata"`
}
type LinkedContactResponse struct {
TotalItems int `json:"_total_items"`
Links struct {
Self struct {
Href string `json:"href"`
} `json:"self"`
} `json:"_links"`
Embedded struct {
Links []struct {
EntityID int `json:"entity_id"`
EntityType string `json:"entity_type"`
ToEntityID int `json:"to_entity_id"`
ToEntityType string `json:"to_entity_type"`
Metadata struct {
Quantity int `json:"quantity"`
CatalogID int `json:"catalog_id"`
IsMain bool `json:"is_main"`
UpdatedBy int `json:"updated_by"`
PriceID int `json:"price_id"`
} `json:"metadata"`
} `json:"links"`
} `json:"_embedded"`
}

@ -98,6 +98,7 @@ type Embedd struct {
}
type Contact struct {
ID int32 `json:"id"`
Name string `json:"first_name"`
ResponsibleUserID int32 `json:"responsible_user_id"` // ID пользователя, ответственного за сделку, в нашем случае PerformerID
CreatedBy int32 `json:"created_by"` // id пользователя amoid который создает сделку (тот кто подключил интеграцию)

@ -7,10 +7,12 @@ import (
"amocrm/pkg/amoClient"
"context"
"encoding/json"
"errors"
"fmt"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/repository/amo"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/utils"
"strconv"
@ -287,12 +289,7 @@ func (wc *PostDeals) constructField(ctx context.Context, allAnswers []model.Resu
if err != nil {
return nil, nil, nil, nil, err
}
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
var contactID int32
contactRuleMap := result.FieldsRule.Contact.ContactRuleMap
contactFields = tools.AddContactFields(contactFields, resultInfo.Name, model.TypeContactName, contactRuleMap)
@ -301,14 +298,78 @@ func (wc *PostDeals) constructField(ctx context.Context, allAnswers []model.Resu
contactFields = tools.AddContactFields(contactFields, resultInfo.Email, model.TypeContactEmail, contactRuleMap)
contactFields = tools.AddContactFields(contactFields, resultInfo.Address, model.TypeContactAddress, contactRuleMap)
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
var fields []string
if resultInfo.Phone != "" {
fields = append(fields, resultInfo.Phone)
}
if resultInfo.Email != "" {
fields = append(fields, resultInfo.Email)
}
existContactData, err := wc.amoRepo.AmoRepo.GetExistingContactAmo(ctx, result.AmoAccountID, fields)
if err != nil && !errors.Is(err, pj_errors.ErrNotFound) {
return nil, nil, nil, nil, err
}
if errors.Is(err, pj_errors.ErrNotFound) {
contactResp, err := wc.amoClient.CreateContact(models.CreateContactReq{
Name: resultInfo.Name,
ResponsibleUserID: result.PerformerID,
CreatedBy: 0,
UpdatedBy: 0,
CreatedAt: dateCreating,
CustomFieldsValues: contactFields,
}, result.SubDomain, result.AccessToken)
if err != nil {
return nil, nil, nil, nil, err
}
for _, c := range contactResp.Embedded.Contacts {
contactID = c.ID
}
if resultInfo.Phone != "" {
_, err = wc.amoRepo.AmoRepo.InsertContactAmo(ctx, model.ContactAmo{
AccountID: result.AmoAccountID,
AmoID: contactID,
Field: resultInfo.Phone,
})
if err != nil {
return nil, nil, nil, nil, err
}
}
if resultInfo.Email != "" {
_, err = wc.amoRepo.AmoRepo.InsertContactAmo(ctx, model.ContactAmo{
AccountID: result.AmoAccountID,
AmoID: contactID,
Field: resultInfo.Email,
})
if err != nil {
return nil, nil, nil, nil, err
}
}
} else if existContactData != nil && len(existContactData) > 0 {
contactID, err = wc.chooseAndCreateContact(ctx, result, resultInfo, existContactData, dateCreating, contactFields, contactRuleMap)
if err != nil {
return nil, nil, nil, nil, err
}
}
return leadFields, []models.Contact{
{
Name: name,
ResponsibleUserID: result.PerformerID,
CreatedBy: 0,
UpdatedBy: 0,
CreatedAt: dateCreating,
CustomFieldsValues: contactFields,
ID: contactID,
//Name: name,
//ResponsibleUserID: result.PerformerID,
//CreatedBy: 0,
//UpdatedBy: 0,
//CreatedAt: dateCreating,
//CustomFieldsValues: contactFields,
},
}, []models.Company{
{
@ -333,6 +394,283 @@ func (wc *PostDeals) constructField(ctx context.Context, allAnswers []model.Resu
}, nil
}
func (wc *PostDeals) chooseAndCreateContact(ctx context.Context, result model.AmoUsersTrueResults, resultInfo model.ResultContent, existingContacts map[int32][]model.ContactAmo, dateCreating int64, contactFields []models.FieldsValues, contactRuleMap map[string]int) (int32, error) {
// 1 ищем контакт в котором совпадает и телефон и емайл
if resultInfo.Phone != "" && resultInfo.Email != "" {
phoneMatchedContacts := make(map[int32]bool)
for _, contactVariants := range existingContacts {
for _, contact := range contactVariants {
if contact.Field == resultInfo.Phone {
phoneMatchedContacts[contact.AmoID] = true
}
}
}
for _, contactVariants := range existingContacts {
for _, contact := range contactVariants {
if contact.Field == resultInfo.Email {
if _, ok := phoneMatchedContacts[contact.AmoID]; ok {
return contact.AmoID, nil
}
}
}
}
var phoneContactID, emailContactID int32
var phoneID int64 /*emailID*/
for _, contactVariants := range existingContacts {
for _, contact := range contactVariants {
if contact.Field == resultInfo.Phone {
phoneContactID = contact.AmoID
phoneID = contact.ID
}
if contact.Field == resultInfo.Email {
emailContactID = contact.AmoID
//emailID = contact.ID
}
}
}
if phoneContactID != 0 && emailContactID != 0 && phoneContactID != emailContactID {
// делаем обновление телефона там где уже есть email
var valuePhone []models.FieldsValues
valuePhone = tools.AddContactFields(valuePhone, resultInfo.Phone, model.TypeContactPhone, contactRuleMap)
_, err := wc.amoClient.UpdateContact(models.CreateContactReq{
CustomFieldsValues: valuePhone,
}, result.SubDomain, result.AccessToken, emailContactID)
if err != nil {
return 0, err
}
err = wc.amoRepo.AmoRepo.UpdateAmoContact(ctx, phoneID, resultInfo.Phone, emailContactID)
if err != nil {
return 0, err
}
_, err = wc.amoClient.LinkedContactToContact(models.LinkedContactReq{
EntityID: emailContactID,
ToEntityID: phoneContactID,
ToEntityType: string(model.ContactsType),
}, result.SubDomain, result.AccessToken)
if err != nil {
return 0, err
}
return emailContactID, nil
}
}
// 2 ищем контакт только по телефону
if resultInfo.Phone != "" {
for _, contactVariants := range existingContacts {
for _, contact := range contactVariants {
if contact.Field == resultInfo.Phone {
// нашли контакт по телефону
emailExists := false
for _, variant := range existingContacts[contact.AmoID] {
if variant.Field != contact.Field {
if variant.Field != "" {
emailExists = true
break
}
}
}
if !emailExists && resultInfo.Email != "" {
// email пустой обновляем контакт добавляя email, если не пустой
var valueEmail []models.FieldsValues
valueEmail = tools.AddContactFields(valueEmail, resultInfo.Email, model.TypeContactEmail, contactRuleMap)
_, err := wc.amoClient.UpdateContact(models.CreateContactReq{
CustomFieldsValues: valueEmail,
}, result.SubDomain, result.AccessToken, contact.AmoID)
if err != nil {
return 0, err
}
_, err = wc.amoRepo.AmoRepo.InsertContactAmo(ctx, model.ContactAmo{
AccountID: result.AmoAccountID,
AmoID: contact.AmoID,
Field: resultInfo.Email,
})
if err != nil {
return 0, err
}
return contact.AmoID, nil
}
if emailExists && resultInfo.Email != "" {
// email не пустой значит это новый контакт создаем если наш email тоже не пустой
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
resp, err := wc.amoClient.CreateContact(models.CreateContactReq{
Name: name,
ResponsibleUserID: result.PerformerID,
CreatedBy: 0,
UpdatedBy: 0,
CreatedAt: dateCreating,
CustomFieldsValues: contactFields,
}, result.SubDomain, result.AccessToken)
if err != nil {
return 0, err
}
var contactID int32
for _, c := range resp.Embedded.Contacts {
contactID = c.ID
}
_, err = wc.amoRepo.AmoRepo.InsertContactAmo(ctx, model.ContactAmo{
AccountID: result.AmoAccountID,
AmoID: contactID,
Field: resultInfo.Phone,
})
if err != nil {
return 0, err
}
_, err = wc.amoRepo.AmoRepo.InsertContactAmo(ctx, model.ContactAmo{
AccountID: result.AmoAccountID,
AmoID: contactID,
Field: resultInfo.Email,
})
if err != nil {
return 0, err
}
return contactID, nil
}
// если пустой то это нужный контакт возвращаем его id, так как если мейл пустой у нас но номер совпадает а в бд не пустой значит оно нам надо
return contact.AmoID, nil
}
}
}
}
// 3 ищем контакт только по email
if resultInfo.Email != "" {
for _, contactVariants := range existingContacts {
for _, contact := range contactVariants {
if contact.Field == resultInfo.Email {
// нашли контакт по email
phoneExists := false
for _, variant := range existingContacts[contact.AmoID] {
if variant.Field != contact.Field {
if variant.Field != "" {
phoneExists = true
break
}
}
}
if !phoneExists && resultInfo.Phone != "" {
// телефон пустой обновляем контакт добавляя телефон, если не пустой
var valuePhone []models.FieldsValues
valuePhone = tools.AddContactFields(valuePhone, resultInfo.Phone, model.TypeContactPhone, contactRuleMap)
_, err := wc.amoClient.UpdateContact(models.CreateContactReq{
CustomFieldsValues: valuePhone,
}, result.SubDomain, result.AccessToken, contact.AmoID)
if err != nil {
return 0, err
}
_, err = wc.amoRepo.AmoRepo.InsertContactAmo(ctx, model.ContactAmo{
AccountID: result.AmoAccountID,
AmoID: contact.AmoID,
Field: resultInfo.Phone,
})
if err != nil {
return 0, err
}
return contact.AmoID, nil
}
if phoneExists && resultInfo.Phone != "" {
// телефон не пустой значит это новый контакт создаем если наш телефон не пустой
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
resp, err := wc.amoClient.CreateContact(models.CreateContactReq{
Name: name,
ResponsibleUserID: result.PerformerID,
CreatedBy: 0,
UpdatedBy: 0,
CreatedAt: dateCreating,
CustomFieldsValues: contactFields,
}, result.SubDomain, result.AccessToken)
if err != nil {
return 0, err
}
var contactID int32
for _, c := range resp.Embedded.Contacts {
contactID = c.ID
}
_, err = wc.amoRepo.AmoRepo.InsertContactAmo(ctx, model.ContactAmo{
AccountID: result.AmoAccountID,
AmoID: contactID,
Field: resultInfo.Phone,
})
if err != nil {
return 0, err
}
_, err = wc.amoRepo.AmoRepo.InsertContactAmo(ctx, model.ContactAmo{
AccountID: result.AmoAccountID,
AmoID: contactID,
Field: resultInfo.Email,
})
if err != nil {
return 0, err
}
return contactID, nil
}
// если пустой то это нужный контакт возвращаем его id, так как если телефон пустой у нас но мейл совпадает а в бд не пустой значит оно нам надо
return contact.AmoID, nil
}
}
}
}
// если дошлю до сюда то это новый контакт с новым email and phone
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
resp, err := wc.amoClient.CreateContact(models.CreateContactReq{
Name: name,
ResponsibleUserID: result.PerformerID,
CreatedBy: 0,
UpdatedBy: 0,
CreatedAt: dateCreating,
CustomFieldsValues: contactFields,
}, result.SubDomain, result.AccessToken)
if err != nil {
return 0, err
}
var contactID int32
for _, c := range resp.Embedded.Contacts {
contactID = c.ID
}
if resultInfo.Phone != "" {
_, err = wc.amoRepo.AmoRepo.InsertContactAmo(ctx, model.ContactAmo{
AccountID: result.AmoAccountID,
AmoID: contactID,
Field: resultInfo.Phone,
})
if err != nil {
return 0, err
}
}
if resultInfo.Email != "" {
_, err = wc.amoRepo.AmoRepo.InsertContactAmo(ctx, model.ContactAmo{
AccountID: result.AmoAccountID,
AmoID: contactID,
Field: resultInfo.Email,
})
if err != nil {
return 0, err
}
}
return contactID, nil
}
func (wc *PostDeals) Stop(_ context.Context) error {
return nil
}

@ -721,3 +721,126 @@ func (a *Amo) createPart(uploadData models.UploadSession, file *os.File) (*model
return nil, nil
}
func (a *Amo) CreateContact(req models.CreateContactReq, domain, accessToken string) (*models.ContactResponse, error) {
for {
if a.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/api/v4/contacts", domain)
bodyBytes, err := json.Marshal(req)
if err != nil {
a.logger.Error("error marshal req in Creating Contact:", zap.Error(err))
return nil, err
}
agent := a.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
a.logger.Error("error sending request in Creating Contact", zap.Error(err))
}
return nil, fmt.Errorf("request failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from Creating Contact: %s", string(resBody))
a.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
var resp models.ContactResponse
err = json.Unmarshal(resBody, &resp)
if err != nil {
a.logger.Error("error unmarshal response body in Creating Contact:", zap.Error(err))
return nil, err
}
return &resp, nil
}
time.Sleep(a.rateLimiter.Interval)
}
}
func (a *Amo) UpdateContact(req models.CreateContactReq, domain, accessToken string, idContact int32) (*models.ContactResponse, error) {
for {
if a.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/api/v4/contacts/%d", domain, idContact)
bodyBytes, err := json.Marshal(req)
if err != nil {
a.logger.Error("error marshal req in Update Contact:", zap.Error(err))
return nil, err
}
agent := a.fiberClient.Patch(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
a.logger.Error("error sending request in Update Contact", zap.Error(err))
}
return nil, fmt.Errorf("request failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from Update Contact: %s", string(resBody))
a.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
var resp models.ContactResponse
err = json.Unmarshal(resBody, &resp)
if err != nil {
a.logger.Error("error unmarshal response body in Update Contact:", zap.Error(err))
return nil, err
}
return &resp, nil
}
time.Sleep(a.rateLimiter.Interval)
}
}
func (a *Amo) LinkedContactToContact(req models.LinkedContactReq, domain, accessToken string) (*models.LinkedContactResponse, error) {
for {
if a.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/api/v4/contacts/link", domain)
bodyBytes, err := json.Marshal(req)
if err != nil {
a.logger.Error("error marshal req in Linked Contact To Contact:", zap.Error(err))
return nil, err
}
agent := a.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
a.logger.Error("error sending request in Linked Contact To Contact", zap.Error(err))
}
return nil, fmt.Errorf("request failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from Linked Contact To Contact: %s", string(resBody))
a.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
var resp models.LinkedContactResponse
err = json.Unmarshal(resBody, &resp)
if err != nil {
a.logger.Error("error unmarshal response body in Linked Contact To Contact:", zap.Error(err))
return nil, err
}
return &resp, nil
}
time.Sleep(a.rateLimiter.Interval)
}
}