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 }