block-accounting/backend/internal/usecase/interactors/transactions/interactor.go
2024-06-04 00:54:17 +03:00

277 lines
7.0 KiB
Go

package transactions
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log/slog"
"time"
"github.com/emochka2007/block-accounting/internal/infrastructure/repository/transactions"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/chain"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
"github.com/google/uuid"
)
type ListParams struct {
IDs uuid.UUIDs
OrganizationID uuid.UUID
Pending bool
ReadyToConfirm bool
Limit int64
Cursor string
}
type CreateParams struct {
Tx models.Transaction
OrganizationId uuid.UUID
}
type ConfirmParams struct {
TxID uuid.UUID
OrganizationID uuid.UUID
}
type CancelParams struct {
TxID uuid.UUID
OrganizationID uuid.UUID
Cause string
}
type TransactionsInteractor interface {
List(ctx context.Context, params ListParams) (*ListResult, error)
Create(ctx context.Context, params CreateParams) (*models.Transaction, error)
Confirm(ctx context.Context, params ConfirmParams) (*models.Transaction, error)
Cancel(ctx context.Context, params CancelParams) (*models.Transaction, error)
}
type transactionsInteractor struct {
log *slog.Logger
txRepo transactions.Repository
orgInteractor organizations.OrganizationsInteractor
chainInteractor chain.ChainInteractor
}
func NewTransactionsInteractor(
log *slog.Logger,
txRepo transactions.Repository,
orgInteractor organizations.OrganizationsInteractor,
chainInteractor chain.ChainInteractor,
) TransactionsInteractor {
return &transactionsInteractor{
log: log,
txRepo: txRepo,
orgInteractor: orgInteractor,
chainInteractor: chainInteractor,
}
}
type txsListCursor struct {
Id uuid.UUID `json:"id"`
}
func newTxsListCursor(id ...uuid.UUID) *txsListCursor {
if len(id) > 0 {
return &txsListCursor{id[0]}
}
return new(txsListCursor)
}
func (c *txsListCursor) encode() (string, error) {
data, err := json.Marshal(c)
if err != nil {
return "", fmt.Errorf("couldn't marshal reaction id. %w", err)
}
return base64.StdEncoding.EncodeToString(data), nil
}
func (c *txsListCursor) decode(s string) error {
if c == nil {
return nil
}
token, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return fmt.Errorf("error decode token. %w", err)
}
return json.Unmarshal(token, c)
}
type ListResult struct {
Txs []*models.Transaction
NextCursor string
}
func (i *transactionsInteractor) List(ctx context.Context, params ListParams) (*ListResult, error) {
if params.Limit == 0 {
params.Limit = 50
}
cursor := newTxsListCursor()
if params.Cursor != "" {
if err := cursor.decode(params.Cursor); err != nil {
return nil, fmt.Errorf("error decode cursor value. %w", err) // maybe just log error?
}
}
txs, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{
OrganizationId: params.OrganizationID,
Limit: params.Limit,
CursorId: cursor.Id,
Pending: params.Pending,
ReadyToConfirm: params.ReadyToConfirm,
})
if err != nil {
return nil, fmt.Errorf("error fetch transaction from repository. %w", err)
}
var nextCursor string
if len(txs) >= 50 || len(txs) >= int(params.Limit) {
cursor.Id = txs[len(txs)-1].Id
if nextCursor, err = cursor.encode(); err != nil {
return nil, fmt.Errorf("error encode next page token. %w", err) // maybe just log error?
}
}
return &ListResult{
Txs: txs,
NextCursor: nextCursor,
}, nil
}
func (i *transactionsInteractor) Create(
ctx context.Context,
params CreateParams,
) (*models.Transaction, error) {
user, err := ctxmeta.User(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
tx := params.Tx
if tx.Id == uuid.Nil {
tx.Id = uuid.Must(uuid.NewV7())
}
participant, err := i.orgInteractor.Participant(ctx, organizations.ParticipantParams{
ID: user.Id(),
OrganizationID: params.OrganizationId,
ActiveOnly: true,
UsersOnly: true,
})
if err != nil {
return nil, fmt.Errorf("error fetch actor prticipant. %w", err)
}
tx.CreatedBy = participant.GetUser()
tx.CreatedAt = time.Now()
tx.UpdatedAt = tx.CreatedAt
if err = i.txRepo.CreateTransaction(ctx, tx); err != nil {
return nil, fmt.Errorf("error create new tx. %w", err)
}
return &tx, nil
}
func (i *transactionsInteractor) Confirm(ctx context.Context, params ConfirmParams) (*models.Transaction, error) {
user, err := ctxmeta.User(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
participant, err := i.orgInteractor.Participant(ctx, organizations.ParticipantParams{
ID: user.Id(),
OrganizationID: params.OrganizationID,
ActiveOnly: true,
UsersOnly: true,
})
if err != nil {
return nil, fmt.Errorf("error fetch actor prticipant. %w", err)
}
if !participant.IsAdmin() {
return nil, fmt.Errorf("error not enouth rights. %w", organizations.ErrorUnauthorizedAccess)
}
if err := i.txRepo.ConfirmTransaction(ctx, transactions.ConfirmTransactionParams{
TxId: params.TxID,
OrganizationId: params.OrganizationID,
UserId: participant.Id(),
}); err != nil {
return nil, fmt.Errorf("error confirm transaction. %w", err)
}
// TODO confirm tx via chain-api
tx, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{
Ids: uuid.UUIDs{params.TxID},
OrganizationId: params.OrganizationID,
Limit: 1,
})
if err != nil {
return nil, fmt.Errorf("error fetch transaction. %w", err)
}
if len(tx) == 0 {
return nil, fmt.Errorf("error tx not found")
}
return tx[0], nil
}
// TODO Execute()
func (i *transactionsInteractor) Cancel(ctx context.Context, params CancelParams) (*models.Transaction, error) {
user, err := ctxmeta.User(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch user from context. %w", err)
}
participant, err := i.orgInteractor.Participant(ctx, organizations.ParticipantParams{
ID: user.Id(),
OrganizationID: params.OrganizationID,
ActiveOnly: true,
UsersOnly: true,
})
if err != nil {
return nil, fmt.Errorf("error fetch actor prticipant. %w", err)
}
if !participant.IsAdmin() {
return nil, fmt.Errorf("error not enouth rights. %w", organizations.ErrorUnauthorizedAccess)
}
if err := i.txRepo.CancelTransaction(ctx, transactions.CancelTransactionParams{
TxId: params.TxID,
OrganizationId: params.OrganizationID,
UserId: participant.Id(),
}); err != nil {
return nil, fmt.Errorf("error confirm transaction. %w", err)
}
tx, err := i.txRepo.GetTransactions(ctx, transactions.GetTransactionsParams{
Ids: uuid.UUIDs{params.TxID},
OrganizationId: params.OrganizationID,
Limit: 1,
})
if err != nil {
return nil, fmt.Errorf("error fetch transaction. %w", err)
}
if len(tx) == 0 {
return nil, fmt.Errorf("error tx not found")
}
return tx[0], nil
}