block-accounting/backend/internal/usecase/repository/transactions/repository.go

448 lines
9.4 KiB
Go
Raw Normal View History

2024-05-24 17:44:24 +00:00
package transactions
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/emochka2007/block-accounting/internal/pkg/models"
sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils"
"github.com/google/uuid"
)
type GetTransactionsParams struct {
Ids uuid.UUIDs
OrganizationId uuid.UUID
CreatedById uuid.UUID
To []byte
Limit int64
CursorId uuid.UUID
WithCancelled bool
WithConfirmed bool
WithCommited bool
WithExpired bool
WithPending bool
WithConfirmations bool
}
type ConfirmTransactionParams struct {
TxId uuid.UUID
UserId uuid.UUID
OrganizationId uuid.UUID
}
type CancelTransactionParams struct {
TxId uuid.UUID
UserId uuid.UUID
OrganizationId uuid.UUID
}
type Repository interface {
GetTransactions(ctx context.Context, params GetTransactionsParams) ([]*models.Transaction, error)
CreateTransaction(ctx context.Context, tx models.Transaction) error
UpdateTransaction(ctx context.Context, tx models.Transaction) error
DeleteTransaction(ctx context.Context, tx models.Transaction) error
ConfirmTransaction(ctx context.Context, params ConfirmTransactionParams) error
CancelTransaction(ctx context.Context, params CancelTransactionParams) error
2024-05-26 00:30:35 +00:00
AddMultisig(ctx context.Context, multisig models.Multisig) error
2024-05-24 17:44:24 +00:00
}
type repositorySQL struct {
db *sql.DB
}
func NewRepository(db *sql.DB) Repository {
return &repositorySQL{
db: db,
}
}
func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX {
if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok {
return tx
}
return s.db
}
func (r *repositorySQL) GetTransactions(
ctx context.Context,
params GetTransactionsParams,
) ([]*models.Transaction, error) {
var txs []*models.Transaction = make([]*models.Transaction, 0, len(params.Ids))
err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
query := buildGetTransactionsQuery(params)
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch transactions data from database. %w", err)
}
defer func() {
if cErr := rows.Close(); cErr != nil {
err = errors.Join(fmt.Errorf("error close database rows. %w", cErr), err)
}
}()
for rows.Next() {
var (
id uuid.UUID
description string
organizationId uuid.UUID
amount float64
toAddr []byte
maxFeeAllowed float64
deadline sql.NullTime
createdAt time.Time
updatedAt time.Time
confirmedAt sql.NullTime
cancelledAt sql.NullTime
commitedAt sql.NullTime
createdById uuid.UUID
createdBySeed []byte
createdByCreatedAt time.Time
createdByActivatedAt sql.NullTime
createdByIsAdmin bool
)
if err = rows.Scan(
&id,
&description,
&organizationId,
&createdById,
&amount,
&toAddr,
&maxFeeAllowed,
&deadline,
&createdAt,
&updatedAt,
&confirmedAt,
&cancelledAt,
&commitedAt,
&createdBySeed,
&createdByCreatedAt,
&createdByActivatedAt,
&createdByIsAdmin,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
tx := &models.Transaction{
Id: id,
Description: description,
OrganizationId: organizationId,
Amount: amount,
ToAddr: toAddr,
MaxFeeAllowed: maxFeeAllowed,
CreatedBy: &models.OrganizationUser{
User: models.User{
ID: createdById,
Bip39Seed: createdBySeed,
},
},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
if deadline.Valid {
tx.Deadline = deadline.Time
}
if confirmedAt.Valid {
tx.ConfirmedAt = confirmedAt.Time
}
if commitedAt.Valid {
tx.CommitedAt = commitedAt.Time
}
if cancelledAt.Valid {
tx.CancelledAt = cancelledAt.Time
}
if createdByActivatedAt.Valid {
tx.CreatedBy.Activated = true
}
txs = append(txs, tx)
}
return nil
})
if err != nil {
return nil, err
}
return txs, nil
}
func (r *repositorySQL) CreateTransaction(ctx context.Context, tx models.Transaction) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
columns := []string{
"id",
"description",
"organization_id",
"created_by",
"amount",
"to_addr",
"max_fee_allowed",
"created_at",
"updated_at",
}
values := []any{
tx.Id,
tx.Description,
tx.OrganizationId,
tx.CreatedBy.ID,
tx.Amount,
tx.ToAddr,
tx.MaxFeeAllowed,
tx.CreatedAt,
tx.CreatedAt,
}
if !tx.Deadline.IsZero() {
columns = append(columns, "deadline")
values = append(values, tx.Deadline)
}
query := sq.Insert("transactions").
Columns(columns...).
Values(values...).
PlaceholderFormat(sq.Dollar)
// todo add optional insertions
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert new transaction. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) UpdateTransaction(ctx context.Context, tx models.Transaction) error {
return nil
}
func (r *repositorySQL) DeleteTransaction(ctx context.Context, tx models.Transaction) error {
return nil
}
func (r *repositorySQL) ConfirmTransaction(ctx context.Context, params ConfirmTransactionParams) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Update("transactions").
SetMap(sq.Eq{
"confirmed_at": time.Now(),
"cancelled_at": nil,
}).
Where(sq.Eq{
"id": params.TxId,
"organization_id": params.OrganizationId,
}).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error update confirmed at. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) CancelTransaction(ctx context.Context, params CancelTransactionParams) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Update("transactions").
SetMap(sq.Eq{
"cancelled_at": time.Now(),
"confirmed_at": nil,
}).
Where(sq.Eq{
"id": params.TxId,
"organization_id": params.OrganizationId,
}).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error update confirmed at. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func buildGetTransactionsQuery(params GetTransactionsParams) sq.SelectBuilder {
query := sq.Select(
`t.id,
t.description,
t.organization_id,
t.created_by,
t.amount,
t.to_addr,
t.max_fee_allowed,
t.deadline,
t.created_at,
t.updated_at,
t.confirmed_at,
t.cancelled_at,
t.commited_at,
u.seed,
u.created_at,
u.activated_at,
ou.is_admin`,
).From("transactions as t").
InnerJoin("users as u on u.id = t.created_by").
InnerJoin(
`organizations_users as ou on
u.id = ou.user_id and ou.organization_id = t.organization_id`,
).
Where(sq.Eq{
"t.organization_id": params.OrganizationId,
}).PlaceholderFormat(sq.Dollar)
if len(params.Ids) > 0 {
query = query.Where(sq.Eq{
"t.id": params.Ids,
})
}
if params.CreatedById != uuid.Nil {
query = query.Where(sq.Eq{
"t.created_by": params.CreatedById,
})
}
if params.OrganizationId != uuid.Nil {
query = query.Where(sq.Eq{
"t.organization_id": params.OrganizationId,
})
}
if params.To != nil {
query = query.Where(sq.Eq{
"t.to_addr": params.To,
})
}
if params.WithExpired {
query = query.Where(sq.LtOrEq{
"t.deadline": time.Now(),
})
} else {
query = query.Where(sq.Or{
sq.GtOrEq{
"t.deadline": time.Now(),
},
sq.Eq{
"t.deadline": nil,
},
})
}
if params.WithCancelled {
query = query.Where(sq.NotEq{
"t.cancelled_at": nil,
})
}
if params.WithConfirmed {
query = query.Where(sq.NotEq{
"t.confirmed_at": nil,
})
}
if params.WithCommited {
query = query.Where(sq.NotEq{
"t.commited_at": nil,
})
}
if params.Limit <= 0 || params.Limit > 50 {
params.Limit = 50
}
if params.WithPending {
query = query.Where(sq.Eq{
"t.cancelled_at": nil,
"t.commited_at": nil,
"t.confirmed_at": nil,
})
}
if params.CursorId != uuid.Nil {
query = query.Where(sq.Gt{
"t.id": params.CursorId,
})
}
query = query.Limit(uint64(params.Limit))
return query
}
2024-05-26 00:30:35 +00:00
func (r *repositorySQL) AddMultisig(
ctx context.Context,
multisig models.Multisig,
) error {
return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Insert("multisigs").Columns(
"id",
"organization_id",
"title",
"created_at",
"updated_at",
).Values(
multisig.ID,
multisig.OrganizationID,
multisig.Title,
multisig.CreatedAt,
multisig.UpdatedAt,
).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert multisig data. %w", err)
}
for _, owner := range multisig.Owners {
addOwnerQuery := sq.Insert("multisig_owners").Columns(
"multisig_id",
"owner_id",
"created_at",
"updated_at",
).Values(
multisig.ID,
owner.Id(),
multisig.CreatedAt,
multisig.UpdatedAt,
).PlaceholderFormat(sq.Dollar)
2024-05-26 00:30:35 +00:00
if _, err := addOwnerQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert multisig owner data. %w", err)
}
}
return nil
})
}