495 lines
14 KiB
Go
495 lines
14 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 SubmissionStatusIdToString(status int32) dbtypes.SubmissionStatus {
|
|
switch status {
|
|
case 0:
|
|
return dbtypes.SubStatusUnspecified
|
|
case 1:
|
|
return dbtypes.SubStatusNew
|
|
case 2:
|
|
return dbtypes.SubStatusPending
|
|
case 3:
|
|
return dbtypes.SubStatusOnInterview
|
|
case 4:
|
|
return dbtypes.SubStatusRejected
|
|
case 5:
|
|
return dbtypes.SubStatusCancelled
|
|
case 6:
|
|
return dbtypes.SubStatusApproved
|
|
default:
|
|
return dbtypes.SubStatusUnspecified
|
|
}
|
|
}
|
|
|
|
func SubmissionStatusStringToId(status dbtypes.SubmissionStatus) int32 {
|
|
switch status {
|
|
case dbtypes.SubStatusUnspecified:
|
|
return 0
|
|
case dbtypes.SubStatusNew:
|
|
return 1
|
|
case dbtypes.SubStatusPending:
|
|
return 2
|
|
case dbtypes.SubStatusOnInterview:
|
|
return 3
|
|
case dbtypes.SubStatusRejected:
|
|
return 4
|
|
case dbtypes.SubStatusCancelled:
|
|
return 5
|
|
case dbtypes.SubStatusApproved:
|
|
return 6
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
//nolint:gocognit // not so hard
|
|
func (c *client) GetSubmissionList(
|
|
ctx context.Context,
|
|
request *dbtypes.SubmissionListGetRequest,
|
|
) (*dbtypes.SubmissionListGetResponse, error) {
|
|
if request == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var (
|
|
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
|
|
|
submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
|
|
)
|
|
|
|
getSubmissions := psql.Select(
|
|
"id", "uid", "vacancy_id", "submission_status_id",
|
|
"cv", "name", "phone", "email", "birthday", "info",
|
|
).From(submissionsTable).
|
|
Limit(request.PageSize).
|
|
Offset(countOffset(request.Page, request.PageSize))
|
|
|
|
if request.Filters != nil {
|
|
if request.Filters.AgentId != nil {
|
|
getSubmissions = getSubmissions.Where(squirrel.Eq{"uid": *request.Filters.AgentId})
|
|
}
|
|
|
|
if request.Filters.VacancyId != nil {
|
|
getSubmissions = getSubmissions.Where(squirrel.Eq{"vacancy_id": *request.Filters.VacancyId})
|
|
}
|
|
|
|
if request.Filters.Status != nil && *request.Filters.Status != dbtypes.SubStatusUnspecified {
|
|
getSubmissions = getSubmissions.Where(squirrel.Eq{"submission_status_id": SubmissionStatusStringToId(*request.Filters.Status)})
|
|
}
|
|
}
|
|
|
|
query, args, err := getSubmissions.ToSql()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error building get submissions query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
rows, err := c.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error executing get submissions query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
defer rows.Close()
|
|
|
|
submissionsList := &dbtypes.SubmissionListGetResponse{
|
|
Submissions: make([]dbtypes.Submission, 0, request.PageSize),
|
|
}
|
|
|
|
for rows.Next() {
|
|
var (
|
|
cvLink, info sql.NullString
|
|
statusId int32
|
|
candidateName string
|
|
|
|
sub = dbtypes.Submission{
|
|
AgentInfo: new(dbtypes.AgentInfo),
|
|
VacancyInfo: new(dbtypes.VacancyInfo),
|
|
CandidateInfo: new(dbtypes.CandidateInfo),
|
|
}
|
|
)
|
|
|
|
if err := rows.Scan(
|
|
&sub.Id, &sub.AgentInfo.Id, &sub.VacancyInfo.Id, &statusId,
|
|
&cvLink, &candidateName, &sub.CandidateInfo.PhoneNumber, &sub.CandidateInfo.Email,
|
|
&sub.CandidateInfo.Birthday, &info,
|
|
); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, dberrors.ErrNotFound
|
|
}
|
|
|
|
return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
sub.CandidateInfo.LastName, sub.CandidateInfo.FirstName, sub.CandidateInfo.MiddleName = splitName(candidateName)
|
|
|
|
if cvLink.Valid {
|
|
sub.CandidateInfo.CvLink = &cvLink.String
|
|
}
|
|
|
|
if info.Valid {
|
|
sub.CandidateInfo.Resume = &info.String
|
|
}
|
|
|
|
sub.Status = SubmissionStatusIdToString(statusId)
|
|
|
|
submissionsList.Submissions = append(submissionsList.Submissions, sub)
|
|
}
|
|
|
|
return submissionsList, nil
|
|
}
|
|
|
|
func splitName(name string) (string, string, string) {
|
|
var (
|
|
parts = strings.SplitN(name, " ", 3)
|
|
lastName, firstName, middleName string
|
|
)
|
|
|
|
if len(parts) > 0 {
|
|
lastName = parts[0]
|
|
}
|
|
|
|
if len(parts) > 1 {
|
|
firstName = parts[1]
|
|
}
|
|
|
|
if len(parts) > 2 {
|
|
middleName = parts[2]
|
|
}
|
|
|
|
return lastName, firstName, middleName
|
|
}
|
|
|
|
func (c *client) CreateSubmission(
|
|
ctx context.Context,
|
|
request *dbtypes.SubmissionCreateRequest,
|
|
) (*dbtypes.SubmissionCreateResponse, error) {
|
|
if request == nil {
|
|
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
|
}
|
|
|
|
if request.CandidateInfo == nil {
|
|
return nil, fmt.Errorf("%w: request.CandidateInfo is nil", dberrors.ErrBadRequest)
|
|
}
|
|
|
|
var (
|
|
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
|
|
|
submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
|
|
)
|
|
|
|
var (
|
|
// TODO: use normal uuid after DB reengineering
|
|
subId = fmt.Sprintf("%sSUB", strings.ReplaceAll(uuid.NewString(), "-", ""))
|
|
fullName = formatName(request.CandidateInfo.LastName, request.CandidateInfo.FirstName, request.CandidateInfo.MiddleName)
|
|
)
|
|
|
|
createSubmission := psql.Insert(submissionsTable).
|
|
Columns(
|
|
"id", "uid", "vacancy_id", "submission_status_id",
|
|
"cv", "name", "phone", "email", "birthday", "info",
|
|
).
|
|
Values(
|
|
subId, request.AgentId, request.VacancyId, SubmissionStatusStringToId(dbtypes.SubStatusNew),
|
|
request.CandidateInfo.CvLink, fullName, request.CandidateInfo.PhoneNumber,
|
|
request.CandidateInfo.Email, request.CandidateInfo.Birthday, request.CandidateInfo.Resume,
|
|
)
|
|
|
|
query, args, err := createSubmission.ToSql()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error building create submission query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
res, err := c.db.ExecContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error executing create submission query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
rowsAffected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error getting rows affected for create submission query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return nil, dberrors.ErrInternal
|
|
}
|
|
|
|
return &dbtypes.SubmissionCreateResponse{
|
|
Id: subId,
|
|
}, nil
|
|
}
|
|
|
|
// NOTE: lastName and firstName are required to be non-empty
|
|
func formatName(lastName, firstName, middleName string) string {
|
|
name := fmt.Sprintf("%s %s", lastName, firstName)
|
|
|
|
if middleName != "" {
|
|
name = fmt.Sprintf("%s %s", name, middleName)
|
|
}
|
|
|
|
return name
|
|
}
|
|
|
|
//nolint:funlen,gocognit // TODO: refactor
|
|
func (c *client) UpdateSubmissionStatus(
|
|
ctx context.Context,
|
|
request *dbtypes.SubmissionStatusUpdateRequest,
|
|
) (*dbtypes.SubmissionStatusUpdateResponse, error) {
|
|
if request == nil {
|
|
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
|
}
|
|
|
|
var (
|
|
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
|
|
|
submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
|
|
)
|
|
|
|
updateSubmissionStatus := psql.Update(submissionsTable).
|
|
Set("submission_status_id", SubmissionStatusStringToId(request.Status)).
|
|
Where(squirrel.Eq{"id": request.Id})
|
|
|
|
query, args, err := updateSubmissionStatus.ToSql()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error building 'update submission status' query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
if request.Status != dbtypes.SubStatusApproved {
|
|
res, err := c.db.ExecContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error executing 'update submission status' query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
rowsAffected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error getting rows affected for 'update submission status' query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return nil, dberrors.ErrNotFound
|
|
}
|
|
|
|
return &dbtypes.SubmissionStatusUpdateResponse{}, nil
|
|
}
|
|
|
|
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() }()
|
|
|
|
vacancy, agentId, err := c.getSubmissionMetadata(ctx, tx, request.Id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting vacancy info: %w", err)
|
|
}
|
|
|
|
if vacancy.AgentReward == nil {
|
|
return nil, fmt.Errorf("%w: agent reward is unknown", dberrors.ErrForbidden)
|
|
}
|
|
|
|
if vacancy.IsArchived {
|
|
return nil, fmt.Errorf("%w: vacancy is closed", dberrors.ErrForbidden)
|
|
}
|
|
|
|
if vacancy.CurrentCandidates+1 > vacancy.RequiredCandidates {
|
|
return nil, fmt.Errorf("%w: vacancy quota exceeded", dberrors.ErrForbidden)
|
|
}
|
|
|
|
if err := c.addSubmissionCandidate(ctx, tx, vacancy.Id); err != nil {
|
|
return nil, fmt.Errorf("%w: error adding submission candidate: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
payload := dbtypes.TransactionPayload{
|
|
Origin: "submission",
|
|
CompanyId: vacancy.Company.Id,
|
|
CompanyName: vacancy.Company.Name,
|
|
VacancyId: vacancy.Id,
|
|
VacancyName: vacancy.Name,
|
|
}
|
|
|
|
molvaAgentTransId := uuid.NewString()
|
|
|
|
if _, err := c.createTransactionWithDriver(ctx, tx, &dbtypes.TransactionCreateRequest{
|
|
OwnerId: agentId,
|
|
Amount: int64(*vacancy.AgentReward),
|
|
Currency: "RUB",
|
|
RequestId: molvaAgentTransId,
|
|
Payload: &payload,
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("error creating molva -> agent transaction: %w", err)
|
|
}
|
|
|
|
distMolvaTransId := uuid.NewString()
|
|
|
|
if _, err := c.createTransactionWithDriver(ctx, tx, &dbtypes.TransactionCreateRequest{
|
|
OwnerId: vacancy.Company.Id,
|
|
Amount: -int64(*vacancy.AgentReward),
|
|
Currency: "RUB",
|
|
RequestId: distMolvaTransId,
|
|
Payload: &payload,
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("error creating dist -> molva transaction: %w", err)
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, fmt.Errorf("%w: error committing transaction: %w", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
res, err := tx.ExecContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error executing 'update submission status' query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
rowsAffected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error getting rows affected for 'update submission status' query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return nil, dberrors.ErrNotFound
|
|
}
|
|
|
|
return &dbtypes.SubmissionStatusUpdateResponse{}, nil
|
|
}
|
|
|
|
func (c *client) getSubmissionMetadata(
|
|
ctx context.Context,
|
|
driver Driver,
|
|
submissionId string,
|
|
) (*dbtypes.Vacancy, string, error) {
|
|
var (
|
|
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
|
|
|
submissionsTable = fmt.Sprintf("%s.%s s", c.config.Schema, SubmissionsTableName)
|
|
companiesTable = fmt.Sprintf("%s.%s c", c.config.Schema, CompaniesTableName)
|
|
vacanciesTable = fmt.Sprintf("%s.%s v", c.config.Schema, VacanciesTableName)
|
|
vacancyCandidatesTable = fmt.Sprintf("%s.%s vc", c.config.Schema, VacancyCandidatesTableName)
|
|
)
|
|
|
|
getMetadata := psql.Select(
|
|
"s.uid", "v.id", "v.name", "v.company_id", "c.name", "v.agent_reward", "v.is_archived",
|
|
"vc.current_candidates_amt", "vc.required_candidates_amt",
|
|
).From(submissionsTable).
|
|
InnerJoin(fmt.Sprintf("%s on s.vacancy_id = v.id", vacanciesTable)).
|
|
InnerJoin(fmt.Sprintf("%s on v.id = vc.vacancy_id", vacancyCandidatesTable)).
|
|
InnerJoin(fmt.Sprintf("%s on v.company_id = c.id", companiesTable)).
|
|
Where(squirrel.Eq{"s.id": submissionId}).
|
|
Suffix("FOR UPDATE OF vc")
|
|
|
|
query, args, err := getMetadata.ToSql()
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("%w: error building 'get submission metadata' query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
row := driver.QueryRowContext(ctx, query, args...)
|
|
|
|
var (
|
|
agentId string
|
|
vacancy dbtypes.Vacancy
|
|
)
|
|
|
|
if err := row.Scan(
|
|
&agentId,
|
|
&vacancy.Id, &vacancy.Name, &vacancy.Company.Id, &vacancy.Company.Name,
|
|
&vacancy.AgentReward, &vacancy.IsArchived,
|
|
&vacancy.CurrentCandidates, &vacancy.RequiredCandidates,
|
|
); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, "", dberrors.ErrNotFound
|
|
}
|
|
|
|
return nil, "", fmt.Errorf("%w: error scanning row for 'get submission metadata' query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
return &vacancy, agentId, nil
|
|
}
|
|
|
|
func (c *client) addSubmissionCandidate(
|
|
ctx context.Context,
|
|
driver Driver,
|
|
vacancyId string,
|
|
) error {
|
|
var (
|
|
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
|
|
|
vacancyCandisTable = fmt.Sprintf("%s.%s", c.config.Schema, VacancyCandidatesTableName)
|
|
)
|
|
|
|
addSubmissionCandidate := psql.Update(vacancyCandisTable).
|
|
Set("current_candidates_amt", squirrel.Expr("current_candidates_amt + 1")).
|
|
Where(squirrel.Eq{"vacancy_id": vacancyId})
|
|
|
|
query, args, err := addSubmissionCandidate.ToSql()
|
|
if err != nil {
|
|
return fmt.Errorf("%w: error building 'add submission candidate' query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
res, err := driver.ExecContext(ctx, query, args...)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: error executing 'add submission candidate' query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
rowsAffected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("%w: error getting rows affected for 'add submission candidate' query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return dberrors.ErrInternal
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *client) DeleteSubmission(
|
|
ctx context.Context,
|
|
request *dbtypes.SubmissionDeleteRequest,
|
|
) (*dbtypes.SubmissionDeleteResponse, error) {
|
|
if request == nil {
|
|
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
|
}
|
|
|
|
var (
|
|
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
|
|
|
submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
|
|
)
|
|
|
|
deleteSubmission := psql.Update(submissionsTable).
|
|
Set("submission_status_id", SubmissionStatusStringToId(dbtypes.SubStatusCancelled)).
|
|
Where(squirrel.Eq{"id": request.Id})
|
|
|
|
query, args, err := deleteSubmission.ToSql()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error building delete submission query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
res, err := c.db.ExecContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error executing delete submission query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
rowsAffected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: error getting rows affected for delete submission query: %v", dberrors.ErrInternal, err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return nil, dberrors.ErrNotFound
|
|
}
|
|
|
|
return &dbtypes.SubmissionDeleteResponse{}, nil
|
|
}
|