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

314 lines
8.5 KiB
Go

package pgdb
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
"github.com/Masterminds/squirrel"
"github.com/google/uuid"
)
// 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
}