This commit is contained in:
r8zavetr8v 2024-05-19 13:29:23 +03:00
parent 866a862087
commit f28fe3d06f
15 changed files with 354 additions and 73 deletions
backend
README.md
cmd
internal
factory
interface/rest
pkg/models
usecase
interactors/transactions
repository
organizations
transactions
users
migrations

View File

@ -224,4 +224,49 @@ Response:
"total_items": 2
}
}
```
```
## POST **/{organization_id}/transactions**
### Request body:
description (string, optional)
amount (float)
to
max_fee_allowed
deadline
### Example
Request:
``` bash
curl --location 'http://localhost:8081/organizations/018f8ccd-2431-7d21-a0c2-a2735c852764/transactions' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer TOKEN' \
--data '{
"description":"New test tx!",
"amount": 100,
"to":"0x323b5d4c32345ced77393b3530b1eed0f346429d",
"max_fee_allowed":5,
"deadline": 123456767
}'
```
Response:
``` json
{
"_type": "transaction",
"_links": {
"self": {
"href": "/organizations/{organization_id}/transactions"
}
},
"id": "018f8ce2-dada-75fb-9745-8560e5736bec",
"description": "New test tx!",
"organization_id": "018f8ccd-2431-7d21-a0c2-a2735c852764",
"created_by": "018f8ccc-e4fc-7a46-9628-15f9c3301f5b",
"amount": 100,
"to": "MjtdTDI0XO13OTs1MLHu0PNGQp0=",
"max_fee_allowed": 5,
"deadline": 123456767,
"created_at": 1716055628507,
"updated_at": 1716055628507
}
```

View File

@ -16,8 +16,8 @@ import (
func main() {
app := &cli.App{
Name: "biocom-ioannes",
Version: "0.0.1a",
Name: "blockd",
Version: "0.1.0",
Commands: commands.Commands(),
Flags: []cli.Flag{
// common

View File

@ -93,6 +93,7 @@ func provideTxController(
return controllers.NewTransactionsController(
log.WithGroup("transactions-controller"),
txInteractor,
presenters.NewTransactionsPresenter(),
)
}

View File

@ -1,9 +1,15 @@
package controllers
import (
"context"
"fmt"
"log/slog"
"net/http"
"time"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/interface/rest/presenters"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/transactions"
)
@ -15,20 +21,55 @@ type TransactionsController interface {
type transactionsController struct {
log *slog.Logger
txInteractor transactions.TransactionsInteractor
txPresenter presenters.TransactionsPresenter
}
func NewTransactionsController(
log *slog.Logger,
txInteractor transactions.TransactionsInteractor,
txPresenter presenters.TransactionsPresenter,
) TransactionsController {
return &transactionsController{
log: log,
txInteractor: txInteractor,
txPresenter: txPresenter,
}
}
func (c *transactionsController) New(w http.ResponseWriter, r *http.Request) ([]byte, error) {
panic("implement me!")
req, err := presenters.CreateRequest[domain.NewTransactionRequest](r)
if err != nil {
return nil, fmt.Errorf("error build new transaction request. %w", err)
}
ctx, cancel := context.WithTimeout(r.Context(), 30000*time.Second)
defer cancel()
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return nil, fmt.Errorf("error fetch organization ID from context. %w", err)
}
requestTx, err := c.txPresenter.RequestTransaction(ctx, req)
if err != nil {
return nil, fmt.Errorf("error build transaction model from request. %w", err)
}
c.log.Debug(
"new reuqest",
slog.Any("req", req),
slog.String("org id", organizationID.String()),
)
tx, err := c.txInteractor.Create(ctx, transactions.CreateParams{
OrganizationId: organizationID,
Tx: requestTx,
})
if err != nil {
return nil, fmt.Errorf("error create new transaction. %w", err)
}
return c.txPresenter.ResponseNewTransaction(ctx, tx)
}
func (c *transactionsController) List(w http.ResponseWriter, r *http.Request) ([]byte, error) {

View File

@ -64,6 +64,31 @@ type ListOrganizationsRequest struct {
OffsetDate int64 `json:"offset_date,omitempty"` // List organizations, updated since the date
}
// Transactions
type NewTransactionRequest struct {
Description string `json:"description,omitempty"`
Amount float64 `json:"amount,omitempty"`
ToAddr string `json:"to,omitempty"`
MaxFeeAllowed float64 `json:"max_fee_allowed,omitempty"`
Deadline int64 `json:"deadline,omitempty"`
}
type ListTransactionsRequest struct {
IDs []string `json:"ids,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
To string `json:"to,omitempty"`
WithCancelled bool `json:"cancelled,omitempty"`
WithConfirmed bool `json:"confirmed,omitempty"`
WithCommited bool `json:"commited,omitempty"`
WithExpired bool `json:"expired,omitempty"`
Cursor string `json:"cursor,omitempty"`
Limit uint8 `json:"limit,omitempty"` // Default: 50, Max: 50
OffsetDate int64 `json:"offset_date,omitempty"` // List organizations, updated since the date
}
func BuildRequest[T any](data []byte) (*T, error) {
var req T

View File

@ -0,0 +1,17 @@
package domain
type Transaction struct {
Id string `json:"id"`
Description string `json:"description"`
OrganizationId string `json:"organization_id"`
CreatedBy string `json:"created_by"`
Amount float64 `json:"amount"`
ToAddr []byte `json:"to"`
MaxFeeAllowed float64 `json:"max_fee_allowed"`
Deadline int64 `json:"deadline,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
ConfirmedAt int64 `json:"confirmed_at,omitempty"`
CancelledAt int64 `json:"cancelled_at,omitempty"`
CommitedAt int64 `json:"commited_at,omitempty"`
}

View File

@ -8,6 +8,10 @@ import (
"github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt"
)
var (
ErrorBadPathParams = errors.New("bad path params")
)
type apiError struct {
Code int `json:"code"`
Message string `json:"message"`
@ -22,6 +26,9 @@ func buildApiError(code int, message string) apiError {
func mapError(err error) apiError {
switch {
// server error
case errors.Is(err, ErrorBadPathParams):
return buildApiError(http.StatusBadRequest, "Invalid Path Params")
// auth controller errors
case errors.Is(err, controllers.ErrorAuthInvalidMnemonic):
return buildApiError(http.StatusBadRequest, "Invalid Mnemonic")

View File

@ -0,0 +1,111 @@
package presenters
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain/hal"
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/ethereum/go-ethereum/common"
)
var (
ErrorInvalidHexAddress = errors.New("error invalid hex address")
)
type TransactionsPresenter interface {
RequestTransaction(ctx context.Context, r *domain.NewTransactionRequest) (models.Transaction, error)
ResponseTransaction(ctx context.Context, tx *models.Transaction) (*hal.Resource, error)
ResponseNewTransaction(ctx context.Context, tx *models.Transaction) ([]byte, error)
}
type transactionsPresenter struct {
}
func NewTransactionsPresenter() TransactionsPresenter {
return &transactionsPresenter{}
}
// RequestTransaction returns a Transaction model WITHOUT CreatedBy user set. CreatedAt set as time.Now()
func (p *transactionsPresenter) RequestTransaction(
ctx context.Context, r *domain.NewTransactionRequest,
) (models.Transaction, error) {
if !common.IsHexAddress(r.ToAddr) {
return models.Transaction{}, ErrorInvalidHexAddress
}
toAddress := common.HexToAddress(r.ToAddr)
organizationID, err := ctxmeta.OrganizationId(ctx)
if err != nil {
return models.Transaction{}, fmt.Errorf("error fetch organization id from context. %w", err)
}
return models.Transaction{
OrganizationId: organizationID,
Description: r.Description,
Amount: r.Amount,
ToAddr: toAddress.Bytes(),
MaxFeeAllowed: r.MaxFeeAllowed,
Deadline: time.UnixMilli(r.Deadline),
CreatedAt: time.Now(),
}, nil
}
func (c *transactionsPresenter) ResponseTransaction(
ctx context.Context,
tx *models.Transaction,
) (*hal.Resource, error) {
r := &domain.Transaction{
Id: tx.Id.String(),
Description: tx.Description,
OrganizationId: tx.OrganizationId.String(),
CreatedBy: tx.CreatedBy.Id().String(),
Amount: tx.Amount,
ToAddr: tx.ToAddr,
MaxFeeAllowed: tx.MaxFeeAllowed,
Deadline: tx.Deadline.UnixMilli(),
CreatedAt: tx.CreatedAt.UnixMilli(),
UpdatedAt: tx.UpdatedAt.UnixMilli(),
}
if !tx.ConfirmedAt.IsZero() {
r.ConfirmedAt = tx.ConfirmedAt.UnixMilli()
}
if !tx.CancelledAt.IsZero() {
r.CancelledAt = tx.CancelledAt.UnixMilli()
}
if !tx.CommitedAt.IsZero() {
r.CommitedAt = tx.CommitedAt.UnixMilli()
}
return hal.NewResource(
r,
"/organizations/{organization_id}/transactions",
hal.WithType("transaction"),
), nil
}
func (c *transactionsPresenter) ResponseNewTransaction(
ctx context.Context,
tx *models.Transaction,
) ([]byte, error) {
dtoTx, err := c.ResponseTransaction(ctx, tx)
if err != nil {
return nil, fmt.Errorf("error map tx to dto. %w", err)
}
out, err := json.Marshal(dtoTx)
if err != nil {
return nil, fmt.Errorf("error marshal tx to hal resource. %w", err)
}
return out, nil
}

View File

@ -19,6 +19,7 @@ import (
"github.com/go-chi/chi/v5"
mw "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
)
@ -237,9 +238,27 @@ func (s *Server) withAuthorization(next http.Handler) http.Handler {
return
}
next.ServeHTTP(w, r.WithContext(
ctxmeta.UserContext(r.Context(), user),
))
ctx := ctxmeta.UserContext(r.Context(), user)
if organizationID := chi.URLParam(r, "organization_id"); organizationID != "" {
organizationUUID, err := uuid.Parse(organizationID)
if err != nil {
s.log.Warn(
"invalid path org id",
slog.String("remote_addr", r.RemoteAddr),
slog.String("endpoint", r.RequestURI),
slog.String("org path id", organizationID),
logger.Err(err),
)
s.responseError(w, ErrorBadPathParams)
return
}
ctx = ctxmeta.OrganizationIdContext(ctx, organizationUUID)
}
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)

View File

@ -12,11 +12,11 @@ type Transaction struct {
Description string
OrganizationId uuid.UUID
CreatedBy *OrganizationUser
Amount int64
Amount float64
ToAddr []byte
MaxFeeAllowed int64
MaxFeeAllowed float64
Deadline time.Time
CreatedAt time.Time

View File

@ -113,10 +113,15 @@ func (i *transactionsInteractor) Create(
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(),
ActiveOnly: true,
UsersOnly: true,
ID: user.Id(),
OrganizationID: params.OrganizationId,
ActiveOnly: true,
UsersOnly: true,
})
if err != nil {
return nil, fmt.Errorf("error fetch actor prticipant. %w", err)
@ -124,6 +129,7 @@ func (i *transactionsInteractor) Create(
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)
@ -135,6 +141,7 @@ func (i *transactionsInteractor) Create(
func (i *transactionsInteractor) Confirm(ctx context.Context, params ConfirmParams) (*models.Transaction, error) {
panic("implement me!")
}
func (i *transactionsInteractor) Cancel(ctx context.Context, params CancelParams) (*models.Transaction, error) {
panic("implement me!")
}

View File

@ -361,45 +361,49 @@ func (r *repositorySQL) Participants(
eg, egCtx := errgroup.WithContext(ctx)
var employees []*models.Employee = make([]*models.Employee, 0, len(orgUsersModels))
eg.Go(func() error {
ids := make(uuid.UUIDs, 0, len(orgUsersModels))
if !params.UsersOnly {
eg.Go(func() error {
ids := make(uuid.UUIDs, 0, len(orgUsersModels))
for _, m := range orgUsersModels {
if m.employeeID != uuid.Nil {
ids = append(ids, m.employeeID)
for _, m := range orgUsersModels {
if m.employeeID != uuid.Nil {
ids = append(ids, m.employeeID)
}
}
}
employees, err = r.fetchEmployees(egCtx, fetchEmployeesParams{
IDs: ids,
OrganizationId: params.OrganizationId,
employees, err = r.fetchEmployees(egCtx, fetchEmployeesParams{
IDs: ids,
OrganizationId: params.OrganizationId,
})
if err != nil {
return fmt.Errorf("error fetch employees. %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("error fetch employees. %w", err)
}
return nil
})
}
var usrs []*models.User
eg.Go(func() error {
ids := make(uuid.UUIDs, 0, len(orgUsersModels))
if !params.EmployeesOnly {
eg.Go(func() error {
ids := make(uuid.UUIDs, 0, len(orgUsersModels))
for _, m := range orgUsersModels {
if m.userID != uuid.Nil {
ids = append(ids, m.employeeID)
for _, m := range orgUsersModels {
if m.userID != uuid.Nil {
ids = append(ids, m.userID)
}
}
}
usrs, err = r.usersRepository.Get(egCtx, users.GetParams{
Ids: ids,
usrs, err = r.usersRepository.Get(egCtx, users.GetParams{
Ids: ids,
})
if err != nil {
return fmt.Errorf("error fetch users by ids. %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("error fetch users by ids. %w", err)
}
return nil
})
}
if err = eg.Wait(); err != nil {
return fmt.Errorf("error organizations users entitiels. %w", err)
@ -422,20 +426,18 @@ func (r *repositorySQL) Participants(
participants = append(participants, employee)
}
var orgUser *models.OrganizationUser
for _, u := range usrs {
if u.Id() == ou.userID {
orgUser = &models.OrganizationUser{
participants = append(participants, &models.OrganizationUser{
User: *u,
OrgPosition: ou.position,
Admin: ou.isAdmin,
Employee: employee,
}
})
break
}
}
participants = append(participants, orgUser)
}
return nil
@ -479,7 +481,8 @@ func (r *repositorySQL) fetchOrganizationUsers(
"ou.is_admin",
).Where(sq.Eq{
"ou.organization_id": params.OrganizationId,
}).PlaceholderFormat(sq.Dollar)
}).From("organizations_users as ou").
PlaceholderFormat(sq.Dollar)
if len(params.Ids) > 0 {
ouQuery = ouQuery.Where(sq.Eq{
@ -487,6 +490,8 @@ func (r *repositorySQL) fetchOrganizationUsers(
})
}
fmt.Println(ouQuery.ToSql())
rows, err := ouQuery.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch organization participants. %w", err)
@ -503,7 +508,7 @@ func (r *repositorySQL) fetchOrganizationUsers(
organizationID uuid.UUID
userID uuid.UUID
employeeID uuid.UUID
position string
position sql.NullString
addedAt time.Time
updatedAt time.Time
deletedAt sql.NullTime
@ -539,7 +544,7 @@ func (r *repositorySQL) fetchOrganizationUsers(
organizationID: organizationID,
userID: userID,
employeeID: employeeID,
position: position,
position: position.String,
addedAt: addedAt,
updatedAt: updatedAt,
deletedAt: deletedAt.Time,
@ -576,7 +581,8 @@ func (r *repositorySQL) fetchEmployees(
"e.updated_at",
).Where(sq.Eq{
"e.organization_id": params.OrganizationId,
}).PlaceholderFormat(sq.Dollar)
}).From("employees as e").
PlaceholderFormat(sq.Dollar)
if len(params.IDs) > 0 {
query = query.Where(sq.Eq{
@ -584,6 +590,8 @@ func (r *repositorySQL) fetchEmployees(
})
}
fmt.Println(query.ToSql())
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch employees from database. %w", err)

View File

@ -96,9 +96,9 @@ func (r *repositorySQL) GetTransactions(
id uuid.UUID
description string
organizationId uuid.UUID
amount int64
amount float64
toAddr []byte
maxFeeAllowed int64
maxFeeAllowed float64
deadline sql.NullTime
createdAt time.Time
updatedAt time.Time
@ -188,21 +188,16 @@ func (r *repositorySQL) GetTransactions(
func (r *repositorySQL) CreateTransaction(ctx context.Context, tx models.Transaction) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Insert("transactions").Columns(
"t.id",
"t.description",
"t.organization_id",
"t.created_by",
"t.amount",
"t.to_addr",
"t.max_fee_allowed",
// todo insert later
// "t.deadline",
// "t.created_at",
// "t.updated_at",
// "t.confirmed_at",
// "t.cancelled_at",
// "t.commited_at",
"id",
"description",
"organization_id",
"created_by",
"amount",
"to_addr",
"max_fee_allowed",
"deadline",
"created_at",
"updated_at",
).Values(
tx.Id,
tx.Description,
@ -211,7 +206,10 @@ func (r *repositorySQL) CreateTransaction(ctx context.Context, tx models.Transac
tx.Amount,
tx.ToAddr,
tx.MaxFeeAllowed,
)
tx.Deadline,
tx.CreatedAt,
tx.CreatedAt,
).PlaceholderFormat(sq.Dollar)
// todo add optional insertions

View File

@ -72,6 +72,8 @@ func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Us
query = query.Where("u.seed = ?", params.Seed)
}
fmt.Println(query.ToSql())
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch data from database. %w", err)

View File

@ -31,7 +31,7 @@ create table if not exists access_tokens (
refresh_token varchar(350) not null,
refresh_token_expired_at timestamp,
created_at timestamp default current_timestamp,
remote_addr string
remote_addr varchar(100)
);
create index if not exists index_access_tokens_token_refresh_token
@ -94,12 +94,12 @@ create table if not exists transactions (
description text default 'New Transaction',
organization_id uuid not null,
created_by uuid not null,
amount bigint default 0,
amount decimal default 0,
to_addr bytea not null,
tx_index bytea default null,
max_fee_allowed bigint default 0,
max_fee_allowed decimal default 0,
deadline timestamp default null,
created_at timestamp default current_timestamp,
@ -144,7 +144,7 @@ create table contracts (
created_by uuid not null references users(id),
organization_id uuid not null references organizations(id),
status tinyint default 0,
status smallint default 0,
tx_index bytea default null,
created_at timestamp default current_timestamp,