package pgdb import ( "context" "database/sql" "errors" "fmt" "strings" dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors" dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types" "github.com/Masterminds/squirrel" "github.com/google/uuid" ) func VacancyStatusIdToString(status int32) dbtypes.VacancyStatus { switch status { case 0: return dbtypes.VacUnspecified case 1: return dbtypes.VacNew case 2: return dbtypes.VacPending case 3: return dbtypes.VacApproved case 4: return dbtypes.VacRejected default: return dbtypes.VacUnspecified } } func VacancyStatusStringToId(status dbtypes.VacancyStatus) int32 { switch status { case dbtypes.VacUnspecified: return 0 case dbtypes.VacNew: return 1 case dbtypes.VacPending: return 2 case dbtypes.VacApproved: return 3 case dbtypes.VacRejected: return 4 default: return 0 } } //nolint:funlen,gocognit // maybe refactor later func (c *client) GetVacancyList( ctx context.Context, request *dbtypes.VacancyListGetRequest, ) (*dbtypes.VacancyListGetResponse, error) { if request == nil { return nil, nil } var ( psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) vacancyTable = fmt.Sprintf("%s.%s v", c.config.Schema, VacanciesTableName) companyTable = fmt.Sprintf("%s.%s c", c.config.Schema, CompaniesTableName) ) getVacList := psql.Select( "v.id", "v.company_id", "c.name", "v.name", "v.address", "v.work_format", "v.agent_reward", "v.region", "v.salary_top", "v.salary_bottom", "v.requirements", "v.responsibilities", "v.additional_info", "v.is_archived", "v.target_action", "v.target_action_amount", "v.publish_date", "v.moderation_status_id", "v.required_candidates", "v.current_candidates", "v.additional_fields", ). From(vacancyTable). LeftJoin(fmt.Sprintf("%s on v.company_id = c.id", companyTable)). Limit(request.PageSize). Offset(countOffset(request.Page, request.PageSize)) getVacList = c.setVacancyListFilters(getVacList, request.Filters) query, args, err := getVacList.ToSql() if err != nil { return nil, fmt.Errorf("%w: error building get vacancy list query: %v", dberrors.ErrInternal, err) } rows, err := c.db.QueryContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("%w: error executing get vacancy list query: %v", dberrors.ErrInternal, err) } defer rows.Close() var ( res dbtypes.VacancyListGetResponse vacancyIds = make([]string, 0, request.PageSize) ) for rows.Next() { var ( status int32 agentReward sql.NullInt32 comName, reqs, resps, extraInfo, extraFields sql.NullString vacancy dbtypes.Vacancy ) if err := rows.Scan( &vacancy.Id, &vacancy.Company.Id, &comName, &vacancy.Name, &vacancy.Address, &vacancy.WorkFormat, &agentReward, &vacancy.Region, &vacancy.SalaryTop, &vacancy.SalaryBottom, &reqs, &resps, &extraInfo, &vacancy.IsArchived, &vacancy.TargetAction.Action, &vacancy.TargetAction.Duration, &vacancy.CreatedAt, &status, &vacancy.RequiredCandidates, &vacancy.CurrentCandidates, &extraFields, ); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, dberrors.ErrNotFound } return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err) } if agentReward.Valid { vacancy.AgentReward = &agentReward.Int32 } if comName.Valid { vacancy.Company.Name = comName.String } if reqs.Valid { vacancy.Requirements = &reqs.String } if resps.Valid { vacancy.Responsibilities = &resps.String } if extraInfo.Valid { vacancy.ExtraInfo = &extraInfo.String } if extraFields.Valid { vacancy.ExtraFields = &extraFields.String } vacancy.Moderation.Status = VacancyStatusIdToString(status) res.Vacancies = append(res.Vacancies, vacancy) vacancyIds = append(vacancyIds, vacancy.Id) } descriptions, err := c.getVacancyModerationDescriptionHistory(ctx, c.db, vacancyIds) if err != nil { if errors.Is(err, dberrors.ErrNotFound) { return &res, nil } return nil, fmt.Errorf("error getting descriptions for vacancies: %w", err) } for i, vacancy := range res.Vacancies { res.Vacancies[i].Moderation.DescriptionHistory = descriptions[vacancy.Id] } return &res, nil } func (c *client) setVacancyListFilters( query squirrel.SelectBuilder, filters *dbtypes.VacancyListFilters, ) squirrel.SelectBuilder { if filters.DistributorId != nil { query = query.Where(squirrel.Eq{"c.uid": *filters.DistributorId}) } if filters.CompanyId != nil { query = query.Where(squirrel.Eq{"v.company_id": *filters.CompanyId}) } if filters.VacancyId != nil { query = query.Where(squirrel.Eq{"v.id": *filters.VacancyId}) } if filters.Region != nil { query = query.Where(squirrel.Eq{"v.region": *filters.Region}) } if filters.SalaryBottom != nil { query = query.Where(squirrel.Or{ squirrel.GtOrEq{"v.salary_bottom": *filters.SalaryBottom}, squirrel.Eq{"v.salary_bottom": nil}, }) } if filters.SalaryTop != nil { query = query.Where(squirrel.Or{ squirrel.LtOrEq{"v.salary_top": *filters.SalaryTop}, squirrel.Eq{"v.salary_top": nil}, }) } if filters.IsArchived != nil { query = query.Where(squirrel.Eq{"v.is_archived": *filters.IsArchived}) } if filters.Status != nil { query = query.Where(squirrel.Eq{"v.moderation_status_id": VacancyStatusStringToId(*filters.Status)}) } return query } func (c *client) getVacancyModerationDescriptionHistory( ctx context.Context, driver Driver, vacancyIds []string, ) (map[string][]dbtypes.VacancyModerationDescription, error) { if len(vacancyIds) == 0 { return nil, nil } var ( psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) vacancyModerDescriptionsTable = fmt.Sprintf("%s.%s", c.config.Schema, VacancyDescriptionHistoryTableName) ) getVacancyModerationDescriptionHistory := psql.Select( "vacancy_id", "description", "created_at", ). From(vacancyModerDescriptionsTable). Where(squirrel.Eq{"vacancy_id": vacancyIds}). OrderBy("vacancy_id", "created_at ASC") query, args, err := getVacancyModerationDescriptionHistory.ToSql() if err != nil { return nil, fmt.Errorf("%w: error building getVacancyModerationDescriptionHistory query: %v", dberrors.ErrInternal, err) } rows, err := driver.QueryContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("%w: error executing getVacancyModerationDescriptionHistory query: %v", dberrors.ErrInternal, err) } defer rows.Close() descriptionsMap := make(map[string][]dbtypes.VacancyModerationDescription) for rows.Next() { var ( vacancyId string description dbtypes.VacancyModerationDescription ) if err := rows.Scan( &vacancyId, &description.Description, &description.CreatedAt, ); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, dberrors.ErrNotFound } return nil, fmt.Errorf("%w: error scanning row for getVacancyModerationDescriptionHistory: %v", dberrors.ErrInternal, err) } descriptionsMap[vacancyId] = append(descriptionsMap[vacancyId], description) } return descriptionsMap, nil } func (c *client) CreateVacancy( ctx context.Context, request *dbtypes.VacancyCreateRequest, ) (*dbtypes.VacancyCreateResponse, error) { if request == nil { return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest) } var ( psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName) ) // TODO: use normal uuid after DB reengineering vacancyId := fmt.Sprintf("%sVAC", strings.ReplaceAll(uuid.NewString(), "-", "")) createVacancy := psql.Insert(vacancyTable). Columns( "id", "company_id", "name", "address", "work_format", "agent_reward", "salary_top", "salary_bottom", "requirements", "responsibilities", "additional_info", "region", "is_archived", "target_action", "target_action_amount", "moderation_status_id", "required_candidates", "publish_date", "current_candidates", "additional_fields", ). Values( vacancyId, request.CompanyId, request.Name, request.Address, request.WorkFormat, request.AgentReward, request.SalaryTop, request.SalaryBottom, request.Requirements, request.Responsibilities, request.ExtraInfo, request.Region, false, request.TargetAction.Action, request.TargetAction.Duration, VacancyStatusStringToId(dbtypes.VacNew), request.RequiredCandidates, squirrel.Expr("now()"), request.CurrentCandidates, request.ExtraFields, ) query, args, err := createVacancy.ToSql() if err != nil { return nil, fmt.Errorf("%w: error building create vacancy query: %v", dberrors.ErrInternal, err) } res, err := c.db.ExecContext(ctx, query, args...) if err != nil { // TODO: process conflict via err.(*pq.Error).Code return nil, fmt.Errorf("%w: error executing create vacancy query: %v", dberrors.ErrInternal, err) } rowsAffected, err := res.RowsAffected() if err != nil { return nil, fmt.Errorf("%w: error getting rows affected for create vacancy query: %v", dberrors.ErrInternal, err) } if rowsAffected == 0 { return nil, dberrors.ErrInternal } return &dbtypes.VacancyCreateResponse{Id: vacancyId}, nil } func (c *client) UpdateVacancy( ctx context.Context, request *dbtypes.VacancyUpdateRequest, ) (*dbtypes.VacancyUpdateResponse, error) { if request == nil { return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest) } tx, err := c.db.BeginTx(ctx, nil) if err != nil { return nil, fmt.Errorf("%w: error starting transaction: %v", dberrors.ErrInternal, err) } defer func() { _ = tx.Rollback() }() status, err := c.getVacancyModerStatusForUpdate(ctx, tx, request.Id) if err != nil { return nil, fmt.Errorf("error getting vacancy moder status: %w", err) } if status == dbtypes.VacApproved { return nil, dberrors.ErrForbidden } res, err := c.updateVacancy(ctx, tx, request) if err != nil { return nil, fmt.Errorf("error updating vacancy: %w", err) } if err := tx.Commit(); err != nil { return nil, fmt.Errorf("error committing transaction: %w", err) } return res, nil } func (c *client) getVacancyModerStatusForUpdate( ctx context.Context, driver Driver, vacancyId string, ) (dbtypes.VacancyStatus, error) { var ( psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName) ) getVacancyModerStatus := psql.Select( "moderation_status_id", ). From(vacancyTable). Where(squirrel.Eq{"id": vacancyId}). Suffix("FOR UPDATE") query, args, err := getVacancyModerStatus.ToSql() if err != nil { return "", fmt.Errorf("%w: error building get vacancy moder status query: %v", dberrors.ErrInternal, err) } row := driver.QueryRowContext(ctx, query, args...) var ( statusId int32 ) if err := row.Scan(&statusId); err != nil { if errors.Is(err, sql.ErrNoRows) { return "", dberrors.ErrNotFound } return "", fmt.Errorf("%w: error scanning row for get vacancy moder status query: %v", dberrors.ErrInternal, err) } return VacancyStatusIdToString(statusId), nil } func (c *client) updateVacancy( ctx context.Context, driver Driver, request *dbtypes.VacancyUpdateRequest, ) (*dbtypes.VacancyUpdateResponse, error) { var ( psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName) ) updateVacancy := psql.Update(vacancyTable). Where(squirrel.Eq{"id": request.Id}) if request.Name != nil { updateVacancy = updateVacancy.Set("name", *request.Name) } if request.Address != nil { updateVacancy = updateVacancy.Set("address", *request.Address) } if request.WorkFormat != nil { updateVacancy = updateVacancy.Set("work_format", *request.WorkFormat) } if request.AgentReward != nil { updateVacancy = updateVacancy.Set("agent_reward", *request.AgentReward) } if request.SalaryTop != nil { updateVacancy = updateVacancy.Set("salary_top", *request.SalaryTop) } if request.SalaryBottom != nil { updateVacancy = updateVacancy.Set("salary_bottom", *request.SalaryBottom) } if request.Requirements != nil { updateVacancy = updateVacancy.Set("requirements", *request.Requirements) } if request.Responsibilities != nil { updateVacancy = updateVacancy.Set("responsibilities", *request.Responsibilities) } if request.ExtraInfo != nil { updateVacancy = updateVacancy.Set("extra_info", *request.ExtraInfo) } if request.Region != nil { updateVacancy = updateVacancy.Set("region", *request.Region) } if request.TargetAction.Action != nil { updateVacancy = updateVacancy.Set("target_action", *request.TargetAction.Action) } if request.TargetAction.Duration != nil { updateVacancy = updateVacancy.Set("target_action_amount", *request.TargetAction.Duration) } if request.RequiredCandidates != nil { updateVacancy = updateVacancy.Set("required_candidates", *request.RequiredCandidates) } if request.ExtraFields != nil { updateVacancy = updateVacancy.Set("additional_fields", *request.ExtraFields) } query, args, err := updateVacancy.ToSql() if err != nil { return nil, fmt.Errorf("%w: error building update vacancy query: %v", dberrors.ErrInternal, err) } res, err := driver.ExecContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("%w: error executing update vacancy query: %v", dberrors.ErrInternal, err) } rowsAffected, err := res.RowsAffected() if err != nil { return nil, fmt.Errorf("%w: error getting rows affected for update vacancy query: %v", dberrors.ErrInternal, err) } if rowsAffected == 0 { return nil, dberrors.ErrInternal } return &dbtypes.VacancyUpdateResponse{}, nil } func (c *client) DeleteVacancy( ctx context.Context, request *dbtypes.VacancyDeleteRequest, ) (*dbtypes.VacancyDeleteResponse, error) { if request == nil { return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest) } var ( psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName) ) deleteVacancy := psql.Update(vacancyTable). Set("is_archived", true). Where(squirrel.Eq{"id": request.Id}) query, args, err := deleteVacancy.ToSql() if err != nil { return nil, fmt.Errorf("%w: error building delete vacancy query: %v", dberrors.ErrInternal, err) } res, err := c.db.ExecContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("%w: error executing delete vacancy query: %v", dberrors.ErrInternal, err) } rowsAffected, err := res.RowsAffected() if err != nil { return nil, fmt.Errorf("%w: error getting rows affected for delete vacancy query: %v", dberrors.ErrInternal, err) } if rowsAffected == 0 { return nil, dberrors.ErrNotFound } return &dbtypes.VacancyDeleteResponse{}, nil } func (c *client) SendVacancyToModeration( ctx context.Context, request *dbtypes.SendVacancyToModerationRequest, ) (*dbtypes.SendVacancyToModerationResponse, error) { if request == nil { return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest) } var ( psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName) ) sendVacancyToModeration := psql.Update(vacancyTable). Set("moderation_status_id", VacancyStatusStringToId(dbtypes.VacPending)). Where(squirrel.Eq{"id": request.Id}) query, args, err := sendVacancyToModeration.ToSql() if err != nil { return nil, fmt.Errorf("%w: error building send vacancy to moderation query: %v", dberrors.ErrInternal, err) } res, err := c.db.ExecContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("%w: error executing send vacancy to moderation query: %v", dberrors.ErrInternal, err) } rowsAffected, err := res.RowsAffected() if err != nil { return nil, fmt.Errorf("%w: error getting rows affected for send vacancy to moderation query: %v", dberrors.ErrInternal, err) } if rowsAffected == 0 { return nil, dberrors.ErrNotFound } return &dbtypes.SendVacancyToModerationResponse{}, nil }