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 }