package integration import ( "context" "encoding/json" "fmt" "log/slog" "net/http" "net/url" "strings" "time" rmodel "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/request_model" ) const ( vkusvillMolvaUtmSource = "molva" dmYFormat = "02.01.2006" // datetime format stored in DB YmdFormat = "2006-01-02" // datetime format used in vkusvill ) var ( // TODO: use reflect to set json tags vkusvillVacancyIdFieldKey = "vacancy_id" vkusvillNeededFields = map[string]struct{}{ vkusvillVacancyIdFieldKey: {}, } ) type ( VkusvillSecretsConfig struct { ApiToken string } ) type ( vkusvillSourceLid struct { Url string `json:"url"` } vkusvillSaveCandidateRequest struct { Status int `json:"status"` // see status mapping LastName string `json:"last_name"` // required Name string `json:"name"` // required SecondName string `json:"second_name"` // required Birthday string `json:"birthday"` // YYYY-mm-dd Phone []string `json:"phone"` VacancyId int `json:"vacancy_id"` // see vacancy to id mapping CityJob string `json:"city_job"` // unsupported now MetroArea string `json:"metro_area"` // unsupported now UtmSource string `json:"utm_source"` SourceLid vkusvillSourceLid `json:"source_lid"` } ) func (c *client) handleVkusvillIntegration( ctx context.Context, request HandleVacancy, vacancyExtraFields *VacancyExtraFields, companyMetadata *CompanyMetadata, companyExtraFieldsTemplate *CompanyExtraFieldsTemplate, ) error { host, ok := (*companyMetadata)["host"] if !ok { return fmt.Errorf("host not found in metadata") } method, ok := (*companyMetadata)["method"] if !ok { return fmt.Errorf("request method not found in metadata") } lastName, name, secondName := splitName(request.Candidate.Name) extraFields, err := parseVacancyExtraFields(vacancyExtraFields, companyExtraFieldsTemplate) if err != nil { return fmt.Errorf("error extracting vacancy id: %w", err) } vacancyId, ok := extraFields[vkusvillVacancyIdFieldKey].(int) if !ok { return fmt.Errorf("vacancy id field is not of expected type (int)") } sourceUrl, err := url.Parse(request.SourceLid) if err != nil { return fmt.Errorf("error parsing source lid: %w", err) } query := sourceUrl.Query() // TODO: think what we potentially want (maybe request id for tracing etc.) query.Set("candidate_id", request.Candidate.Id) sourceUrl.RawQuery = query.Encode() saveCandidateReq := vkusvillSaveCandidateRequest{ Status: 0, LastName: lastName, Name: name, SecondName: secondName, Birthday: formatDate(request.Candidate.Birthday, dmYFormat, YmdFormat), Phone: []string{request.Candidate.Phone}, VacancyId: vacancyId, CityJob: "", MetroArea: "", UtmSource: vkusvillMolvaUtmSource, SourceLid: vkusvillSourceLid{ Url: sourceUrl.String(), }, } resp, err := c.vkusvillSaveCandidate(method, host, &saveCandidateReq) if err != nil { return fmt.Errorf("error making request: %w", err) } defer resp.Body.Close() // TODO: think about following: // у Вкусвилла ошибки возвращаются в теле ответа. Соответственно, нужно // а) проверить наличие ошибки // б) если ошибка есть, создать алерт об этом (например, в админку). Вообще надо подумать над этим сценарием return c.vkusvillHandleResponse(resp) } type ( VkusvillSaveCandidateResponse struct { //nolint:tagliatelle // used in vkusvill Error bool `json:"ERR"` //nolint:tagliatelle // used in vkusvill Data json.RawMessage `json:"DATA"` } VkusvillErrorMessage struct { Field string `json:"field"` Message string `json:"message"` } VkusvillErrorDetails struct { Data []VkusvillErrorMessage } VkusvillSuccessDetails struct { // TODO: maybe add more fields } ) func (r *VkusvillSaveCandidateResponse) GetErrorDetails() (*VkusvillErrorDetails, error) { var errDetails VkusvillErrorDetails if err := json.Unmarshal(r.Data, &errDetails); err != nil { return nil, err } return &errDetails, nil } func (r *VkusvillSaveCandidateResponse) GetSuccessDetails() (*VkusvillSuccessDetails, error) { var successDetails VkusvillSuccessDetails if err := json.Unmarshal(r.Data, &successDetails); err != nil { return nil, err } return &successDetails, nil } func (c *client) vkusvillHandleResponse(resp *http.Response) error { const handlerName = "integration.vkusvillHandleResponse" var vkusvillResp VkusvillSaveCandidateResponse if err := json.NewDecoder(resp.Body).Decode(&vkusvillResp); err != nil { return fmt.Errorf("error decoding vkusvill response: %w", err) } if vkusvillResp.Error { errDetails, err := vkusvillResp.GetErrorDetails() if err != nil { return fmt.Errorf("error getting vkusvill error details: %w", err) } return fmt.Errorf("error saving candidate in vkusvill HRM: %v", errDetails) } successDetails, err := vkusvillResp.GetSuccessDetails() if err != nil { return fmt.Errorf("error getting vkusvill success details: %w", err) } // TODO: remove after testing c.logger.Debug("vkusvill response body", slog.String("body", string(vkusvillResp.Data)), slog.String("handler", handlerName)) c.logger.Debug("vkusvill success details", slog.Any("success_details", successDetails), slog.String("handler", handlerName)) return nil } func splitName(name string) (string, string, string) { splittedName := strings.SplitN(name, " ", 3) var ( lastName, firstName, secondName string ) if len(splittedName) > 1 { lastName = splittedName[0] } if len(splittedName) > 2 { firstName = splittedName[1] } if len(splittedName) > 3 { secondName = splittedName[2] } return lastName, firstName, secondName } func formatDate(date, fromFmt, toFmt string) string { parsed, err := time.Parse(fromFmt, date) if err != nil { return date } return parsed.Format(toFmt) } //nolint:gocognit // TODO: refactor func parseVacancyExtraFields( vacancyExtraFields *VacancyExtraFields, companyExtraFieldsTemplate *CompanyExtraFieldsTemplate, ) (map[string]any, error) { resMap := make(map[string]any) for _, vacXField := range vacancyExtraFields.Fields { if _, ok := vkusvillNeededFields[vacXField.SystemName]; !ok { continue } if len(vacXField.Value) == 0 { // TODO: Maybe here we need to check whether corresponding field template // has required flag set to true continue } if vacXField.IsValidValue { // get corresponding value from company template for _, comFieldTemplate := range companyExtraFieldsTemplate.Fields { if comFieldTemplate.SystemName != vacXField.SystemName { continue } if vacXField.MultipleChoice { var ( nameValMap = make(map[string]string, len(comFieldTemplate.ValidValues)) res = make([]any, len(vacXField.Value)) err error ) for _, validVal := range comFieldTemplate.ValidValues { nameValMap[validVal.DisplayName] = validVal.Value } for i, val := range vacXField.Value { res[i], err = convertFieldtype(vacXField.Type, nameValMap[val]) if err != nil { return nil, fmt.Errorf("error converting field value: %w", err) } } resMap[vacXField.SystemName] = res } else { for _, validVal := range comFieldTemplate.ValidValues { if validVal.DisplayName != vacXField.Value[0] { continue } res, err := convertFieldtype(vacXField.Type, validVal.Value) if err != nil { return nil, fmt.Errorf("error converting field value: %w", err) } resMap[vacXField.SystemName] = res } } } continue } if vacXField.MultipleChoice { var ( res = make([]any, len(vacXField.Value)) err error ) for i, v := range vacXField.Value { res[i], err = convertFieldtype(vacXField.Type, v) if err != nil { return nil, fmt.Errorf("error converting field value: %w", err) } } resMap[vacXField.SystemName] = res } else { res, err := convertFieldtype(vacXField.Type, vacXField.Value[0]) if err != nil { return nil, fmt.Errorf("error converting field value: %w", err) } resMap[vacXField.SystemName] = res } } return resMap, nil } func VkusvillGetCandidateStatus(status int) (rmodel.SubmissionStatus, error) { switch status { case 0: // Статус не установлен return rmodel.SubStatusUnspecified, nil case 666: // Архивный return rmodel.SubStatusCancelled, nil case 49: // Без вакансии return rmodel.SubStatusCancelled, nil case 50: // На рассмотрении return rmodel.SubStatusOnInterview, nil case 51: // Отклоненный return rmodel.SubStatusRejected, nil case 115: // Зарезервированный return rmodel.SubStatusApproved, nil default: return rmodel.SubStatusUnspecified, ErrUnknownCandidateStatus } }