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 }