Files
test_deploy/internal/integration/vkusvill.go
Alex Shevchuk d84487d238 1
2025-08-18 17:12:04 +03:00

345 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}