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

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
}