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,100 @@
package pgdb
import (
"context"
"database/sql"
"errors"
"fmt"
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"
)
func (c *client) GetBalance(
ctx context.Context,
request *dbtypes.BalanceGetRequest,
) (*dbtypes.BalanceGetResponse, error) {
if request == nil {
return nil, nil
}
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
balancesTable = fmt.Sprintf("%s.%s", c.config.Schema, BalancesTableName)
)
getBalance := psql.Select(
"raw_balance", "clean_balance",
).From(balancesTable).
Where(squirrel.Eq{"owner_id": request.OwnerId})
query, args, err := getBalance.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building get balance query: %v", dberrors.ErrInternal, err)
}
row := c.db.QueryRowContext(ctx, query, args...)
var (
balance dbtypes.Balance
)
if err := row.Scan(&balance.RawBalance, &balance.CleanBalance); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, dberrors.ErrNotFound
}
return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
}
return &dbtypes.BalanceGetResponse{
Balance: &balance,
}, nil
}
func (c *client) createBalance(
ctx context.Context,
driver Driver,
request *dbtypes.BalanceCreateRequest,
) error {
if request == nil {
return fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
}
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
balancesTable = fmt.Sprintf("%s.%s", c.config.Schema, BalancesTableName)
)
createBalance := psql.Insert(balancesTable).
Columns(
"id", "owner_id", "raw_balance", "clean_balance",
).
Values(
request.Id, request.OwnerId, request.RawBalance, request.CleanBalance,
)
query, args, err := createBalance.ToSql()
if err != nil {
return fmt.Errorf("%w: error building 'create balance' query: %v", dberrors.ErrInternal, err)
}
res, err := driver.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("%w: error executing 'create balance' query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("%w: error getting rows affected for 'create balance' query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return dberrors.ErrInternal
}
return nil
}

View File

@@ -0,0 +1,313 @@
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"
)
// ALERT: intergrate safety checks
func (c *client) GetBankAccountList(
ctx context.Context,
request *dbtypes.BankAccountListGetRequest,
) (*dbtypes.BankAccountListGetResponse, error) {
if request == nil {
return nil, nil
}
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
bankAccountsTable = fmt.Sprintf("%s.%s", c.config.Schema, BankAccountsTableName)
)
getAccounts := psql.Select(
"id", "owner_id", "account_number", "bank_name", "bik",
"correspondent_account", "is_primary", "created_at", "updated_at",
).From(bankAccountsTable).
Where(squirrel.Eq{"owner_id": request.OwnerId})
query, args, err := getAccounts.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building get bank accounts query: %v", dberrors.ErrInternal, err)
}
rows, err := c.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("%w: error executing get bank accounts query: %v", dberrors.ErrInternal, err)
}
defer rows.Close()
var accounts []dbtypes.BankAccount
for rows.Next() {
var (
accountNumber, bankName, bik, correspondentAccount sql.NullString
bankAccount dbtypes.BankAccount
)
if err := rows.Scan(
&bankAccount.Id, &bankAccount.OwnerId, &accountNumber, &bankName, &bik,
&correspondentAccount, &bankAccount.IsPrimary, &bankAccount.CreatedAt, &bankAccount.UpdatedAt,
); 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 accountNumber.Valid {
bankAccount.AccountName = accountNumber.String
}
if bankName.Valid {
bankAccount.BankName = bankName.String
}
if bik.Valid {
bankAccount.Bik = bik.String
}
if correspondentAccount.Valid {
bankAccount.CorrespondentAccount = correspondentAccount.String
}
accounts = append(accounts, bankAccount)
}
return &dbtypes.BankAccountListGetResponse{
BankAccounts: accounts,
}, nil
}
func (c *client) CreateBankAccount(
ctx context.Context,
request *dbtypes.BankAccountCreateRequest,
) (*dbtypes.BankAccountCreateResponse, error) {
if request == nil {
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
}
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
bankAccountsTable = fmt.Sprintf("%s.%s", c.config.Schema, BankAccountsTableName)
)
// TODO: use normal uuid after DB reengineering
bankAccountId := fmt.Sprintf("%sBNK", strings.ReplaceAll(uuid.NewString(), "-", ""))
createBankAccount := psql.Insert(bankAccountsTable).
Columns(
"id", "owner_id", "account_number", "bank_name", "bik", "correspondent_account", "is_primary",
).
Values(
bankAccountId, request.OwnerId, request.AccountNumber, request.BankName,
request.Bik, request.CorrespondentAccount, request.IsPrimary,
)
query, args, err := createBankAccount.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building create bank account query: %v", dberrors.ErrInternal, err)
}
res, err := c.db.ExecContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("%w: error executing create bank account query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return nil, fmt.Errorf("%w: error getting rows affected for create bank account query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return nil, dberrors.ErrInternal
}
return &dbtypes.BankAccountCreateResponse{
Id: bankAccountId,
}, nil
}
func (c *client) UpdateBankAccount(
ctx context.Context,
request *dbtypes.BankAccountUpdateRequest,
) (*dbtypes.BankAccountUpdateResponse, 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() }()
result, err := c.updateBankAccount(ctx, tx, request)
if err != nil {
return nil, fmt.Errorf("error updating bank account: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("%w: error committing transaction: %w", dberrors.ErrInternal, err)
}
return result, nil
}
func (c *client) updateBankAccount(
ctx context.Context,
driver Driver,
request *dbtypes.BankAccountUpdateRequest,
) (*dbtypes.BankAccountUpdateResponse, error) {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
bankAccountsTable = fmt.Sprintf("%s.%s", c.config.Schema, BankAccountsTableName)
)
updateAccount := psql.Update(bankAccountsTable).
Where(squirrel.Eq{"id": request.Id})
if request.AccountNumber != nil {
updateAccount = updateAccount.Set("account_number", *request.AccountNumber)
}
// TODO: uncomment when DB supports it
// if request.AccountName != nil {
// updateAccount = updateAccount.Set("account_name", *request.AccountName)
// }
if request.BankName != nil {
updateAccount = updateAccount.Set("bank_name", *request.BankName)
}
if request.Bik != nil {
updateAccount = updateAccount.Set("bik", *request.Bik)
}
if request.CorrespondentAccount != nil {
updateAccount = updateAccount.Set("correspondent_account", *request.CorrespondentAccount)
}
if request.IsPrimary != nil {
if *request.IsPrimary {
if err := c.unmarkPrimaryBankAccount(ctx, driver, request.Id); err != nil {
return nil, fmt.Errorf("error unmarking currently primary bank account: %w", err)
}
}
// QnA: what if the update makes all BAs non-primary?
updateAccount = updateAccount.Set("is_primary", *request.IsPrimary)
}
query, args, err := updateAccount.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building 'update bank account' query: %v", dberrors.ErrInternal, err)
}
res, err := driver.ExecContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("%w: error executing 'update bank account' query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return nil, fmt.Errorf("%w: error getting rows affected for 'update bank account' query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return nil, dberrors.ErrNotFound
}
return &dbtypes.BankAccountUpdateResponse{}, nil
}
func (c *client) unmarkPrimaryBankAccount(
ctx context.Context,
driver Driver,
bankAccountId string,
) error {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
bankAccountsTable = fmt.Sprintf("%s.%s", c.config.Schema, BankAccountsTableName)
)
getOwnerId := psql.Select(
"owner_id", "is_primary",
).From(bankAccountsTable).
Where(squirrel.Eq{"id": bankAccountId}).
Suffix("FOR UPDATE")
query, args, err := getOwnerId.ToSql()
if err != nil {
return fmt.Errorf("%w: error building 'get owner id of bank account' query: %v", dberrors.ErrInternal, err)
}
row := driver.QueryRowContext(ctx, query, args...)
var (
ownerId string
isPrimary bool
)
if err := row.Scan(&ownerId, &isPrimary); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return dberrors.ErrNotFound
}
return fmt.Errorf("%w: error scanning row for 'get owner id of bank account' query: %v", dberrors.ErrInternal, err)
}
if isPrimary {
return nil
}
unmarkPrimaryBA := psql.Update(bankAccountsTable).
Set("is_primary", false).
Where(squirrel.Eq{
"owner_id": ownerId,
"is_primary": true,
})
query, args, err = unmarkPrimaryBA.ToSql()
if err != nil {
return fmt.Errorf("%w: error building 'unmark primary bank account' query: %v", dberrors.ErrInternal, err)
}
res, err := driver.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("%w: error executing 'unmark primary bank account' query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("%w: error getting rows affected for 'unmark primary bank account' query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return fmt.Errorf("%w: error unmarking primary bank account: no rows affected", dberrors.ErrInternal)
}
return nil
}
func (c *client) DeleteBankAccount(
ctx context.Context,
request *dbtypes.BankAccountDeleteRequest,
) (*dbtypes.BankAccountDeleteResponse, error) {
return nil, dberrors.ErrUnimplemented
}

View File

@@ -0,0 +1,107 @@
package pgdb
import (
"context"
"crypto/tls"
"crypto/x509"
"database/sql"
"fmt"
"os"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/stdlib"
"github.com/jmoiron/sqlx"
)
// Интерфейс передается в неэкспортируемые функции
// нужен для того, чтобы не зависило от того, что передаём
// транзацию или обычное соединение
type Driver interface {
// QueryContext executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
// QueryRowContext executes a query that is expected to return at most one row.
// QueryRowContext always returns a non-nil value. Errors are deferred until
// [Row]'s Scan method is called.
// If the query selects no rows, the [*Row.Scan] will return [ErrNoRows].
// Otherwise, [*Row.Scan] scans the first selected row and discards
// the rest.
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
// ExecContext executes a query without returning any rows.
// The args are for any placeholder parameters in the query.
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}
type PostgresConfig struct {
Host string
Port uint16
Username string
Password string
Database string
Schema string
SSLMode string
SSLRootCert string
}
type client struct {
config PostgresConfig
db *sqlx.DB
}
func NewClient(cfg PostgresConfig) (*client, error) {
rootCertPool := x509.NewCertPool()
caCert, err := os.ReadFile(cfg.SSLRootCert)
if err != nil {
return nil, fmt.Errorf("failed to read CA cert: %w", err)
}
if ok := rootCertPool.AppendCertsFromPEM(caCert); !ok {
return nil, fmt.Errorf("failed to append CA cert to pool")
}
tlsConfig := &tls.Config{
RootCAs: rootCertPool,
MinVersion: tls.VersionTLS12,
//nolint:gosec // TODO: set server name
InsecureSkipVerify: true,
}
config, err := pgx.ParseConfig("")
if err != nil {
return nil, fmt.Errorf("failed to parse [empty] config: %w", err)
}
config.Host = cfg.Host
config.Port = cfg.Port
config.Database = cfg.Database
config.User = cfg.Username
config.Password = cfg.Password
config.TLSConfig = tlsConfig
config.RuntimeParams = map[string]string{"sslmode": cfg.SSLMode}
db := sqlx.NewDb(stdlib.OpenDB(*config), "pgx")
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping postgres: %w", err)
}
return &client{
config: cfg,
db: db,
}, nil
}
func countOffset(page, pageSize uint64) uint64 {
if page == 0 {
page = 1
}
if pageSize == 0 {
pageSize = DefaultPaginationPageSize
}
return (page - 1) * pageSize
}

View File

@@ -0,0 +1,506 @@
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"
"github.com/lib/pq"
)
func CompanyModerationStatusIdToString(status int32) dbtypes.CompanyModerationStatus {
switch status {
case 0:
return dbtypes.CompanyModerationStatusPending
case 1:
return dbtypes.CompanyModerationStatusApproved
case 2:
return dbtypes.CompanyModerationStatusRejected
case 3:
return dbtypes.CompanyModerationStatusNew
default:
return dbtypes.CompanyModerationStatusNew
}
}
func CompanyModerationStatusStringToId(status dbtypes.CompanyModerationStatus) int32 {
switch status {
case dbtypes.CompanyModerationStatusPending:
return 0
case dbtypes.CompanyModerationStatusApproved:
return 1
case dbtypes.CompanyModerationStatusRejected:
return 2
case dbtypes.CompanyModerationStatusNew:
return 3
default:
return 3
}
}
//nolint:gocognit // TODO: refactor
func (c *client) GetCompanyList(
ctx context.Context,
request *dbtypes.CompanyListGetRequest,
) (*dbtypes.CompanyListGetResponse, error) {
if request == nil {
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
}
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
companiesTable = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
)
getComList := psql.Select(
"id", "uid", "name", "legal_person", "description", "website",
"physical_address", "legal_address", "inn", "is_active", // TODO: add KPP when DB supports it
"has_moderation_ticket", "staff", "metadata", "additional_fields_tmpl",
).
From(companiesTable).
Where(squirrel.Eq{"uid": request.Id})
query, args, err := getComList.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building get distributor company 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 distributor company list query: %v", dberrors.ErrInternal, err)
}
defer rows.Close()
var res dbtypes.CompanyListGetResponse
for rows.Next() {
var (
name, legalPerson, description, website, physicalAddress sql.NullString
legalAddress, inn, metadata, additionalFieldsTmpl sql.NullString
staff pq.StringArray
company dbtypes.Company
)
if err := rows.Scan(
&company.Id, &company.OwnerId, &name, &legalPerson, &description, &website,
&physicalAddress, &legalAddress, &inn, &company.IsActive,
&company.HasModerationTicket, &staff, &metadata, &additionalFieldsTmpl,
); 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 name.Valid {
company.Name = &name.String
}
if legalPerson.Valid {
company.LegalPerson = &legalPerson.String
}
if description.Valid {
company.Description = &description.String
}
if website.Valid {
company.Website = &website.String
}
if physicalAddress.Valid {
company.PhysicalAddress = &physicalAddress.String
}
if legalAddress.Valid {
company.LegalAddress = &legalAddress.String
}
if inn.Valid {
company.Inn = &inn.String
}
company.Staff = staff
if metadata.Valid {
company.Metadata = &metadata.String
}
if additionalFieldsTmpl.Valid {
company.ExtraFieldsTemplate = &additionalFieldsTmpl.String
}
res.Companies = append(res.Companies, company)
}
return &res, nil
}
func (c *client) GetCompanyById(
ctx context.Context,
request *dbtypes.CompanyByIdGetRequest,
) (*dbtypes.CompanyByIdGetResponse, error) {
if request == nil {
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
}
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
companiesTable = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
)
getComList := psql.Select(
"id", "uid", "name", "legal_person", "description", "website",
"physical_address", "legal_address", "inn", "is_active", // TODO: add KPP when DB supports it
"has_moderation_ticket", "staff", "metadata", "additional_fields_tmpl",
).
From(companiesTable).
Where(squirrel.And{
squirrel.Eq{"id": request.CompanyId},
squirrel.Eq{"uid": request.Id},
})
query, args, err := getComList.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building get distributor company list query: %v", dberrors.ErrInternal, err)
}
row := c.db.QueryRowContext(ctx, query, args...)
var (
name, legalPerson, description, website, physicalAddress sql.NullString
legalAddress, inn, metadata, additionalFieldsTmpl sql.NullString
staff pq.StringArray
company dbtypes.Company
)
if err := row.Scan(
&company.Id, &company.OwnerId, &name, &legalPerson, &description, &website,
&physicalAddress, &legalAddress, &inn, &company.IsActive,
&company.HasModerationTicket, &staff, &metadata, &additionalFieldsTmpl,
); 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 name.Valid {
company.Name = &name.String
}
if legalPerson.Valid {
company.LegalPerson = &legalPerson.String
}
if description.Valid {
company.Description = &description.String
}
if website.Valid {
company.Website = &website.String
}
if physicalAddress.Valid {
company.PhysicalAddress = &physicalAddress.String
}
if legalAddress.Valid {
company.LegalAddress = &legalAddress.String
}
if inn.Valid {
company.Inn = &inn.String
}
company.Staff = staff
if metadata.Valid {
company.Metadata = &metadata.String
}
if additionalFieldsTmpl.Valid {
company.ExtraFieldsTemplate = &additionalFieldsTmpl.String
}
return &dbtypes.CompanyByIdGetResponse{Company: company}, nil
}
func (c *client) CreateCompany(
ctx context.Context,
request *dbtypes.CompanyCreateRequest,
) (*dbtypes.CompanyCreateResponse, 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() }()
res, err := c.createCompany(ctx, tx, request)
if err != nil {
return nil, fmt.Errorf("error creating company: %w", err)
}
if err := c.createCompanyValidationTicket(ctx, tx, res.Id, request); err != nil {
return nil, fmt.Errorf("error creating company validation ticket: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("error committing transaction: %w", err)
}
return res, nil
}
func (c *client) createCompany(
ctx context.Context,
driver Driver,
request *dbtypes.CompanyCreateRequest,
) (*dbtypes.CompanyCreateResponse, error) {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
companyTable = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
)
// TODO: use normal uuid after DB reengineering
comId := fmt.Sprintf("%sCOM", strings.ReplaceAll(uuid.NewString(), "-", ""))
createCompany := psql.Insert(companyTable).
Columns(
"id", "uid", "is_active", "has_moderation_ticket", "metadata", "additional_fields_tmpl",
).
Values(
comId, request.OwnerId, false, true, request.Metadata, request.ExtraFieldsTemplate,
)
query, args, err := createCompany.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building create company query: %v", dberrors.ErrInternal, err)
}
res, err := driver.ExecContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("%w: error executing create company query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return nil, fmt.Errorf("%w: error getting rows affected for create company query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return nil, dberrors.ErrInternal
}
return &dbtypes.CompanyCreateResponse{Id: comId}, nil
}
func (c *client) createCompanyValidationTicket(
ctx context.Context,
driver Driver,
companyId string,
request *dbtypes.CompanyCreateRequest,
) error {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
comValTable = fmt.Sprintf("%s.%s", c.config.Schema, CompanyValidationTicketsTableName)
)
var (
ticketId = fmt.Sprintf("%sTCK", strings.ReplaceAll(uuid.NewString(), "-", ""))
)
createCompany := psql.Insert(comValTable).
Columns(
"id", "company_id", "name", "legal_person", "description", "website",
"physical_address", "legal_address", "inn", // TODO: add KPP when DB supports it
"staff", "status",
).
Values(
ticketId, companyId, request.Name, request.LegalPerson, request.Description, request.Website,
request.PhysicalAddress, request.LegalAddress, request.Inn,
request.Staff, CompanyModerationStatusStringToId(dbtypes.CompanyModerationStatusPending), // TODO: switch to status "NEW"
)
query, args, err := createCompany.ToSql()
if err != nil {
return fmt.Errorf("%w: error building create company moderation ticket query: %v", dberrors.ErrInternal, err)
}
res, err := driver.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("%w: error executing create company moderation ticket query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("%w: error getting rows affected for create company moderation ticket query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return dberrors.ErrInternal
}
return nil
}
func (c *client) UpdateCompany(
ctx context.Context,
request *dbtypes.CompanyUpdateRequest,
) (*dbtypes.CompanyUpdateResponse, 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() }()
res, err := c.updateCompany(ctx, tx, request)
if err != nil {
return nil, fmt.Errorf("error updating company: %w", err)
}
if err := c.updateCompanyValidationTicket(ctx, tx, request.Id, request); err != nil {
return nil, fmt.Errorf("error updating company validation ticket: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("error committing transaction: %w", err)
}
return res, nil
}
func (c *client) updateCompany(
ctx context.Context,
driver Driver,
request *dbtypes.CompanyUpdateRequest,
) (*dbtypes.CompanyUpdateResponse, error) {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
companyTable = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
)
updateCompany := psql.Update(companyTable).
SetMap(map[string]any{
"is_active": false,
"has_moderation_ticket": true,
"metadata": request.Metadata,
"additional_fields_tmpl": request.ExtraFields,
}).
Where(squirrel.Eq{"id": request.Id})
query, args, err := updateCompany.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building update company query: %v", dberrors.ErrInternal, err)
}
res, err := driver.ExecContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("%w: error executing update company query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return nil, fmt.Errorf("%w: error getting rows affected for update company query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return nil, dberrors.ErrInternal
}
return &dbtypes.CompanyUpdateResponse{}, nil
}
// NOTE: do we believe that every company has a moderation ticket?
func (c *client) updateCompanyValidationTicket(
ctx context.Context,
driver Driver,
companyId string,
request *dbtypes.CompanyUpdateRequest,
) error {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
comValTable = fmt.Sprintf("%s.%s", c.config.Schema, CompanyValidationTicketsTableName)
)
updateCompany := psql.Update(comValTable).
Where(squirrel.Eq{"company_id": companyId})
if request.Name != nil {
updateCompany = updateCompany.Set("name", *request.Name)
}
if request.LegalPerson != nil {
updateCompany = updateCompany.Set("legal_person", *request.LegalPerson)
}
if request.Description != nil {
updateCompany = updateCompany.Set("description", *request.Description)
}
if request.Website != nil {
updateCompany = updateCompany.Set("website", *request.Website)
}
if request.PhysicalAddress != nil {
updateCompany = updateCompany.Set("physical_address", *request.PhysicalAddress)
}
if request.LegalAddress != nil {
updateCompany = updateCompany.Set("legal_address", *request.LegalAddress)
}
if request.Inn != nil {
updateCompany = updateCompany.Set("inn", *request.Inn)
}
if len(request.Staff) > 0 {
updateCompany = updateCompany.Set("staff", request.Staff)
}
query, args, err := updateCompany.ToSql()
if err != nil {
return fmt.Errorf("%w: error building update company moderation ticket query: %v", dberrors.ErrInternal, err)
}
res, err := driver.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("%w: error executing update company moderation ticket query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("%w: error getting rows affected for update company moderation ticket query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return dberrors.ErrInternal
}
return nil
}

View File

@@ -0,0 +1,23 @@
package pgdb
const (
BalancesTableName = "balances"
BankAccountsTableName = "company_bank_accounts"
CompaniesTableName = "company"
CompanyValidationTicketsTableName = "company_validation"
SubmissionsTableName = "submission"
TransactionsTableName = "transactions"
VacanciesTableName = "vacancy"
VacancyCandidatesTableName = "vacancy_candidates"
VacancyDescriptionHistoryTableName = "vacancy_moderation_descriptions"
UsersTableName = "client"
UserValidationTableName = "client_validation"
)
const (
DefaultPaginationPageSize = 20
)
const (
PGErrUniqueViolation = "23505"
)

View File

@@ -0,0 +1,128 @@
package pgdb
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
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"
)
func (c *client) GetVacancyIntegrationInfoById(
ctx context.Context,
vacancyId string,
) (string, *dbtypes.VacancyExtraFieldsTemplate, error) {
return c.getVacancyIntegrationInfoById(ctx, c.db, vacancyId)
}
func (c *client) getVacancyIntegrationInfoById(
ctx context.Context,
driver Driver,
vacancyId string,
) (string, *dbtypes.VacancyExtraFieldsTemplate, error) {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
)
getCompanyIdQuery := psql.Select("company_id", "additional_fields").
From(vacancyTable).
Where(squirrel.Eq{"id": vacancyId})
query, args, err := getCompanyIdQuery.ToSql()
if err != nil {
return "", nil, fmt.Errorf("%w: error building query: %v", dberrors.ErrInternal, err)
}
row := driver.QueryRowContext(ctx, query, args...)
var (
companyId string
fieldsString sql.NullString
fields dbtypes.VacancyExtraFieldsTemplate
)
if err := row.Scan(&companyId, &fieldsString); 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 fieldsString.Valid {
if err := json.Unmarshal([]byte(fieldsString.String), &fields); err != nil {
return "", nil, fmt.Errorf("%w: error unmarshalling fields: %v", dberrors.ErrInternal, err)
}
}
return companyId, &fields, nil
}
func (c *client) GetCompanyMetadataById(
ctx context.Context,
companyId string,
) (*dbtypes.CompanyMetadata, *dbtypes.CompanyExtraFieldsTemplate, error) {
return c.getCompanyMetadataById(ctx, c.db, companyId)
}
func (c *client) getCompanyMetadataById(
ctx context.Context,
driver Driver,
companyId string,
) (*dbtypes.CompanyMetadata, *dbtypes.CompanyExtraFieldsTemplate, error) {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
companyTable = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
)
getCompanyMetadataQuery := psql.Select(
"metadata",
"additional_fields_tmpl",
).
From(companyTable).
Where(squirrel.Eq{"id": companyId})
query, args, err := getCompanyMetadataQuery.ToSql()
if err != nil {
return nil, nil, fmt.Errorf("%w: error building query: %v", dberrors.ErrInternal, err)
}
row := driver.QueryRowContext(ctx, query, args...)
var (
metadataString sql.NullString
metadata dbtypes.CompanyMetadata
fieldsTemplateString sql.NullString
fieldsTemplate dbtypes.CompanyExtraFieldsTemplate
)
if err := row.Scan(&metadataString, &fieldsTemplateString); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil, dberrors.ErrNotFound
}
return nil, nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
}
if metadataString.Valid {
if err := json.Unmarshal([]byte(metadataString.String), &metadata); err != nil {
return nil, nil, fmt.Errorf("%w: error unmarshalling metadata: %v", dberrors.ErrInternal, err)
}
}
if fieldsTemplateString.Valid {
if err := json.Unmarshal([]byte(fieldsTemplateString.String), &fieldsTemplate); err != nil {
return nil, nil, fmt.Errorf("%w: error unmarshalling fields template: %v", dberrors.ErrInternal, err)
}
}
return &metadata, &fieldsTemplate, nil
}

View File

@@ -0,0 +1,130 @@
package pgdb
import (
"context"
"database/sql"
"errors"
"fmt"
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/lib/pq"
)
func (c *client) GetProfileById(
ctx context.Context,
request *dbtypes.ProfileGetRequest,
) (*dbtypes.ProfileGetResponse, error) {
if request == nil {
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
}
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
usersTable = fmt.Sprintf("%s.%s", c.config.Schema, UsersTableName)
)
getProfile := psql.Select(
"uid", "name", "phone", "email",
).From(usersTable).
Where(squirrel.Eq{"uid": request.Id})
query, args, err := getProfile.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building get profile query: %v", dberrors.ErrInternal, err)
}
row := c.db.QueryRowContext(ctx, query, args...)
var (
name, phoneNumber, email sql.NullString
profile dbtypes.Profile
)
if err := row.Scan(&profile.Id, &name, &phoneNumber, &email); 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 name.Valid {
profile.Name = name.String
}
if phoneNumber.Valid {
profile.PhoneNumber = phoneNumber.String
}
if email.Valid {
profile.Email = email.String
}
return &dbtypes.ProfileGetResponse{
Profile: &profile,
}, nil
}
func (c *client) UpdateProfile(
ctx context.Context,
request *dbtypes.ProfileUpdateRequest,
) (*dbtypes.ProfileUpdateResponse, error) {
if request == nil {
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
}
if request.Name == nil && request.PhoneNumber == nil && request.Email == nil {
return nil, fmt.Errorf("%w: nothing to update", dberrors.ErrBadRequest)
}
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
usersTable = fmt.Sprintf("%s.%s", c.config.Schema, UsersTableName)
)
updateProfile := psql.Update(usersTable).
Where(squirrel.Eq{"uid": request.Id})
if request.Name != nil {
updateProfile = updateProfile.Set("name", *request.Name)
}
if request.PhoneNumber != nil {
updateProfile = updateProfile.Set("phone", *request.PhoneNumber)
}
if request.Email != nil {
updateProfile = updateProfile.Set("email", *request.Email)
}
query, args, err := updateProfile.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building update profile query: %v", dberrors.ErrInternal, err)
}
res, err := c.db.ExecContext(ctx, query, args...)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok {
if pqErr.Code == PGErrUniqueViolation {
return nil, dberrors.ErrConflict
}
}
return nil, fmt.Errorf("%w: error executing update profile query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return nil, fmt.Errorf("%w: error getting rows affected for update profile query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return nil, dberrors.ErrInternal
}
return &dbtypes.ProfileUpdateResponse{}, nil
}

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
}

View File

@@ -0,0 +1,485 @@
package pgdb
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
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"
)
func getTransactionTypeByAmount(amount int64) dbtypes.TransactionType {
if amount > 0 {
return dbtypes.TransactionTypeDeposit
}
return dbtypes.TransactionTypeWithdrawal
}
// TODO: add migration to rebind statuses
func TransactionStatusIdToString(status int32) dbtypes.TransactionStatus {
switch status {
case 0:
return dbtypes.TransactionStatusPending
case 1:
return dbtypes.TransactionStatusApproved
case 2:
return dbtypes.TransactionStatusRejected
case 3:
return dbtypes.TransactionStatusNew
default:
return dbtypes.TransactionStatusPending
}
}
func TransactionStatusStringToId(status dbtypes.TransactionStatus) int32 {
switch status {
case dbtypes.TransactionStatusNew:
return 3
case dbtypes.TransactionStatusPending:
return 0
case dbtypes.TransactionStatusApproved:
return 1
case dbtypes.TransactionStatusRejected:
return 2
default:
return 3
}
}
//nolint:gocognit // not so hard
func (c *client) GetTransactionList(
ctx context.Context,
request *dbtypes.TransactionListGetRequest,
) (*dbtypes.TransactionListGetResponse, error) {
if request == nil {
return nil, nil
}
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
transactionsTable = fmt.Sprintf("%s.%s", c.config.Schema, TransactionsTableName)
)
getTransactions := psql.Select(
"id", "owner_id", "bank_account_id", "amount", "currency", "status", "created_at", "payload",
).From(transactionsTable).
Where(squirrel.Eq{"owner_id": request.OwnerId}).
Limit(request.PageSize).
Offset(countOffset(request.Page, request.PageSize))
getTransactions, err := c.setGetTransactionsQueryFilters(getTransactions, request.Filters)
if err != nil {
return nil, fmt.Errorf("%w: error setting get transactions query filters: %v", dberrors.ErrBadRequest, err)
}
query, args, err := getTransactions.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building get transactions query: %v", dberrors.ErrInternal, err)
}
rows, err := c.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("%w: error executing get transactions query: %v", dberrors.ErrInternal, err)
}
defer rows.Close()
transList := &dbtypes.TransactionListGetResponse{
Transactions: make([]dbtypes.Transaction, 0, request.PageSize),
}
for rows.Next() {
var (
transStatus int32
ownerId string
payload, bankAccountId sql.NullString
transaction dbtypes.Transaction
)
if err := rows.Scan(
&transaction.Id, &ownerId, &bankAccountId, &transaction.Amount, &transaction.Currency,
&transStatus, &transaction.CreatedAt, &payload,
); 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 bankAccountId.Valid {
bankAccountInfo, err := c.getBankAccountInfoById(ctx, c.db, bankAccountId.String)
if err != nil {
return nil, fmt.Errorf("%w: error getting bank account info: %v", dberrors.ErrInternal, err)
}
transaction.BankAccountInfo = bankAccountInfo
ownerInfo, err := c.getOwnerInfoById(ctx, c.db, ownerId, bankAccountInfo.OwnerType)
if err != nil {
return nil, fmt.Errorf("%w: error getting owner info: %v", dberrors.ErrInternal, err)
}
transaction.OwnerInfo = ownerInfo
}
transaction.Type = getTransactionTypeByAmount(transaction.Amount)
transaction.Status = TransactionStatusIdToString(transStatus)
if payload.Valid {
var payloadData dbtypes.TransactionPayload
if err := json.Unmarshal([]byte(payload.String), &payloadData); err != nil {
return nil, fmt.Errorf("%w: error unmarshaling transaction payload: %v", dberrors.ErrInternal, err)
}
transaction.Payload = &payloadData
}
transList.Transactions = append(transList.Transactions, transaction)
}
return transList, nil
}
func (c *client) setGetTransactionsQueryFilters(
query squirrel.SelectBuilder,
filters *dbtypes.TransactionListFilters,
) (squirrel.SelectBuilder, error) {
if filters == nil {
return query, nil
}
if filters.Type != nil {
switch *filters.Type {
case dbtypes.TransactionTypeDeposit:
query = query.Where(squirrel.Gt{
"amount": 0,
})
case dbtypes.TransactionTypeWithdrawal:
query = query.Where(squirrel.Lt{
"amount": 0,
})
default:
return query, fmt.Errorf("%w: invalid transaction type: %v", dberrors.ErrBadRequest, *filters.Type)
}
}
if filters.Status != nil {
query = query.Where(squirrel.Eq{"status": TransactionStatusStringToId(*filters.Status)})
}
if filters.BankAccountId != nil {
query = query.Where(squirrel.Eq{"bank_account_id": *filters.BankAccountId})
}
return query, nil
}
func (c *client) getOwnerInfoById(
ctx context.Context,
driver Driver,
ownerId string,
ownerType string,
) (*dbtypes.TransactionOwnerInfo, error) {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
clientTableName = fmt.Sprintf("%s.%s", c.config.Schema, UsersTableName)
companyTableName = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
)
var getOwnerInfoById squirrel.SelectBuilder
// TODO: reingeneer the DB
switch ownerType {
case "agent":
getOwnerInfoById = psql.Select(
"uid",
"name",
).
From(clientTableName).
Where(squirrel.Eq{"uid": ownerId})
case "company":
getOwnerInfoById = psql.Select(
"id",
"name",
).
From(companyTableName).
Where(squirrel.Eq{"id": ownerId})
default:
return nil, fmt.Errorf("%w: invalid owner type", dberrors.ErrBadRequest)
}
query, args, err := getOwnerInfoById.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building get owner info by id query: %v", dberrors.ErrInternal, err)
}
row := driver.QueryRowContext(ctx, query, args...)
var ownerInfo dbtypes.TransactionOwnerInfo
if err := row.Scan(
&ownerInfo.Id,
&ownerInfo.Name,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, dberrors.ErrNotFound
}
return nil, fmt.Errorf("%w: error scanning row for get owner info by id query: %v", dberrors.ErrInternal, err)
}
return &ownerInfo, nil
}
func (c *client) getBankAccountInfoById(
ctx context.Context,
driver Driver,
bankAccountId string,
) (*dbtypes.BankAccountInfo, error) {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
bankAccountsTableName = fmt.Sprintf("%s.%s", c.config.Schema, BankAccountsTableName)
)
getBankAccountInfoById := psql.Select(
"id",
"account_number",
"bank_name",
"bik",
"correspondent_account",
"owner_type",
).
From(bankAccountsTableName).
Where(squirrel.Eq{"id": bankAccountId})
query, args, err := getBankAccountInfoById.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building get bank account info by id query: %v", dberrors.ErrInternal, err)
}
row := driver.QueryRowContext(ctx, query, args...)
var accountInfo dbtypes.BankAccountInfo
if err := row.Scan(
&accountInfo.Id,
&accountInfo.AccountNumber,
&accountInfo.BankName,
&accountInfo.Bik,
&accountInfo.CorrespondentAccount,
&accountInfo.OwnerType,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, dberrors.ErrNotFound
}
return nil, fmt.Errorf("%w: error scanning row for get bank account info by id query: %v", dberrors.ErrInternal, err)
}
return &accountInfo, nil
}
func (c *client) CreateTransaction(
ctx context.Context,
request *dbtypes.TransactionCreateRequest,
) (*dbtypes.TransactionCreateResponse, 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() }()
result, err := c.createTransactionWithDriver(ctx, tx, request)
if err != nil {
return nil, fmt.Errorf("error creating transaction: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("%w: error committing transaction: %w", dberrors.ErrInternal, err)
}
return result, nil
}
func (c *client) createTransactionWithDriver(
ctx context.Context,
driver Driver,
request *dbtypes.TransactionCreateRequest,
) (*dbtypes.TransactionCreateResponse, error) {
if _, err := c.getRawBalanceForUpdate(ctx, driver, request.OwnerId); err != nil {
return nil, fmt.Errorf("error getting raw balance for update: %w", err)
}
result, err := c.createTransaction(ctx, driver, request)
if err != nil {
if errors.Is(err, dberrors.ErrConflict) {
return result, nil
}
return nil, fmt.Errorf("error creating transaction: %w", err)
}
if err := c.updateBalance(ctx, driver, request.Amount, request.OwnerId); err != nil {
return nil, fmt.Errorf("error updating balance: %w", err)
}
return result, nil
}
func (c *client) getRawBalanceForUpdate(
ctx context.Context,
driver Driver,
ownerId string,
) (int64, error) {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
balancesTable = fmt.Sprintf("%s.%s", c.config.Schema, BalancesTableName)
)
getBalance := psql.Select(
"raw_balance",
).From(balancesTable).
Where(squirrel.Eq{"owner_id": ownerId}).
Suffix("FOR UPDATE")
query, args, err := getBalance.ToSql()
if err != nil {
return 0, fmt.Errorf("%w: error building 'get balance' query: %v", dberrors.ErrInternal, err)
}
row := driver.QueryRowContext(ctx, query, args...)
var rawBalance int64
if err := row.Scan(&rawBalance); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, dberrors.ErrNotFound
}
return 0, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
}
return rawBalance, nil
}
func (c *client) updateBalance(
ctx context.Context,
driver Driver,
amountDelta int64,
ownerId string,
) error {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
balancesTable = fmt.Sprintf("%s.%s", c.config.Schema, BalancesTableName)
)
updateBalance := psql.Update(balancesTable).
SetMap(map[string]any{
"raw_balance": squirrel.Expr("raw_balance + ?", amountDelta),
"updated_at": squirrel.Expr("NOW()"),
}).
Where(squirrel.Eq{"owner_id": ownerId})
query, args, err := updateBalance.ToSql()
if err != nil {
return fmt.Errorf("%w: error building 'update balance' query: %v", dberrors.ErrInternal, err)
}
res, err := driver.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("%w: error executing 'update balance' query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("%w: error getting rows affected for 'update balance' query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return dberrors.ErrInternal
}
return nil
}
func (c *client) createTransaction(
ctx context.Context,
driver Driver,
request *dbtypes.TransactionCreateRequest,
) (*dbtypes.TransactionCreateResponse, error) {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
transactionsTable = fmt.Sprintf("%s.%s", c.config.Schema, TransactionsTableName)
)
var payload []byte
if request.Payload == nil {
payload = []byte("{}")
} else {
payloadBytes, err := json.Marshal(request.Payload)
if err != nil {
return nil, fmt.Errorf("%w: error marshaling transaction payload: %v", dberrors.ErrInternal, err)
}
payload = payloadBytes
}
createTransaction := psql.Insert(transactionsTable).
Columns(
"id", "owner_id", "bank_account_id", "amount", "currency", "status", "created_at", "payload",
).
Values(
request.RequestId, request.OwnerId, request.BankAccountId, request.Amount, request.Currency,
dbtypes.TransactionStatusNew, squirrel.Expr("CURRENT_TIMESTAMP"), payload,
).
Suffix("ON CONFLICT (id, owner_id) DO NOTHING")
query, args, err := createTransaction.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building 'create transaction' query: %v", dberrors.ErrInternal, err)
}
res, err := driver.ExecContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("%w: error executing 'create transaction' query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return nil, fmt.Errorf("%w: error getting rows affected for 'create transaction' query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return &dbtypes.TransactionCreateResponse{
Id: request.RequestId,
}, dberrors.ErrConflict
}
return &dbtypes.TransactionCreateResponse{
Id: request.RequestId,
}, nil
}

View File

@@ -0,0 +1,203 @@
package pgdb
import (
"context"
"database/sql"
"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 (c *client) GetClientValidation(
ctx context.Context,
request *dbtypes.ClientValidationGetRequest,
) (*dbtypes.ClientValidationGetResponse, error) {
if request == nil {
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
}
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
userValidationTable = fmt.Sprintf("%s.%s", c.config.Schema, UserValidationTableName)
)
getUserValidation := psql.Select(
"status", "description",
).
From(userValidationTable).
Where(squirrel.Eq{"uid": request.UserId})
query, args, err := getUserValidation.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building 'get user validation' query: %v", dberrors.ErrInternal, err)
}
row := c.db.QueryRowContext(ctx, query, args...)
var result dbtypes.ClientValidation
if err := row.Scan(&result.Status, &result.Description); err != nil {
if err == sql.ErrNoRows {
return nil, dberrors.ErrNotFound
}
return nil, fmt.Errorf("%w: error scanning row for 'get user validation' query: %v", dberrors.ErrInternal, err)
}
return &dbtypes.ClientValidationGetResponse{
ClientValidation: &result,
}, nil
}
func (c *client) CreateUser(
ctx context.Context,
request *dbtypes.UserSaveRequest,
) (*dbtypes.UserSaveResponse, 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() }()
result, err := c.createUser(ctx, tx, request)
if err != nil {
return nil, fmt.Errorf("error creating user: %w", err)
}
if err := c.createUserValidationTicket(ctx, tx, request); err != nil {
return nil, fmt.Errorf("error creating user validation ticket: %w", err)
}
resp, err := c.createCompany(ctx, tx, &dbtypes.CompanyCreateRequest{
OwnerId: request.Id,
Staff: []string{
request.Id,
},
})
if err != nil {
return nil, fmt.Errorf("error creating company: %w", err)
}
var ownerId string
switch request.Type {
case dbtypes.UserTypeAgent:
ownerId = request.Id
case dbtypes.UserTypeDistributor:
ownerId = resp.Id
default:
return nil, fmt.Errorf("%w: unknown user type: %v", dberrors.ErrBadRequest, request.Type)
}
balanceId := fmt.Sprintf("%sBAL", strings.ReplaceAll(uuid.NewString(), "-", ""))
if err := c.createBalance(ctx, tx, &dbtypes.BalanceCreateRequest{
Id: balanceId,
OwnerId: ownerId,
RawBalance: 0,
CleanBalance: 0,
}); err != nil {
return nil, fmt.Errorf("error creating balance: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("%w: error committing transaction: %w", dberrors.ErrInternal, err)
}
return result, nil
}
func (c *client) createUser(
ctx context.Context,
driver Driver,
request *dbtypes.UserSaveRequest,
) (*dbtypes.UserSaveResponse, error) {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
userTable = fmt.Sprintf("%s.%s", c.config.Schema, UsersTableName)
)
saveUser := psql.Insert(userTable).
Columns(
"uid", "name", "phone", "email", "client_type_id",
).
Values(
request.Id, request.FullName, request.Phone, request.Email, request.Type,
)
query, args, err := saveUser.ToSql()
if err != nil {
return nil, fmt.Errorf("%w: error building 'save user' query: %v", dberrors.ErrInternal, err)
}
res, err := driver.ExecContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("%w: error executing 'save user' query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return nil, fmt.Errorf("%w: error getting rows affected for 'save user' query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return nil, dberrors.ErrInternal
}
return &dbtypes.UserSaveResponse{}, nil
}
func (c *client) createUserValidationTicket(
ctx context.Context,
driver Driver,
request *dbtypes.UserSaveRequest,
) error {
var (
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
userValidationTable = fmt.Sprintf("%s.%s", c.config.Schema, UserValidationTableName)
)
// TODO: use normal uuid after DB reengineering
ticketId := fmt.Sprintf("%sVAL", strings.ReplaceAll(uuid.NewString(), "-", ""))
createUserValidation := psql.Insert(userValidationTable).
Columns(
"id", "uid", "status", "description", "last_update",
).
Values(
ticketId, request.Id, dbtypes.ClientValStatusNew, request.FullName, squirrel.Expr("CURRENT_TIMESTAMP"),
)
query, args, err := createUserValidation.ToSql()
if err != nil {
return fmt.Errorf("%w: error building 'create user validation' query: %v", dberrors.ErrInternal, err)
}
res, err := driver.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("%w: error executing 'create user validation' query: %v", dberrors.ErrInternal, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("%w: error getting rows affected for 'create user validation' query: %v", dberrors.ErrInternal, err)
}
if rowsAffected == 0 {
return dberrors.ErrInternal
}
return nil
}

View File

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