This commit is contained in:
Alex Shevchuk
2025-08-18 17:12:04 +03:00
commit d84487d238
157 changed files with 160686 additions and 0 deletions

View File

@@ -0,0 +1,494 @@
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
}