574 lines
16 KiB
Go
574 lines
16 KiB
Go
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
|
|
}
|