345 lines
8.9 KiB
Go
345 lines
8.9 KiB
Go
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
|
||
}
|
||
}
|