diff --git a/go.mod b/go.mod index 94a17e8..9bbd93c 100644 --- a/go.mod +++ b/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 ( diff --git a/go.sum b/go.sum index 7134012..a9a58ff 100644 --- a/go.sum +++ b/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= diff --git a/internal/models/createContact.go b/internal/models/createContact.go new file mode 100644 index 0000000..5f21680 --- /dev/null +++ b/internal/models/createContact.go @@ -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"` +} diff --git a/internal/models/createDeal.go b/internal/models/createDeal.go index 0721649..d8c26ec 100644 --- a/internal/models/createDeal.go +++ b/internal/models/createDeal.go @@ -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 который создает сделку (тот кто подключил интеграцию) diff --git a/internal/workers/post_deals_worker/deals_worker.go b/internal/workers/post_deals_worker/deals_worker.go index 90ab765..1ec2925 100644 --- a/internal/workers/post_deals_worker/deals_worker.go +++ b/internal/workers/post_deals_worker/deals_worker.go @@ -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 } diff --git a/pkg/amoClient/amo.go b/pkg/amoClient/amo.go index 86c00d4..5e158f5 100644 --- a/pkg/amoClient/amo.go +++ b/pkg/amoClient/amo.go @@ -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) + } +}