diff --git a/backend/README.md b/backend/README.md index 4c6a394..6ba00c5 100644 --- a/backend/README.md +++ b/backend/README.md @@ -226,6 +226,60 @@ Response: } ``` +## GET **/{organization_id}/participants** +### Request body: +```json +{ + "limit":1 +} +``` + +### Example +Request: +```bash +curl --request GET \ + --url http://localhost:8081/organizations/018faff4-481f-73ec-a4b8-27ef07b4029b/participants \ + --header 'Authorization: Bearer TOKEN' \ + --header 'content-type: application/json' \ + --data '{ + "limit":1 +}' +``` +Response: +```json +{ + "_type": "participants", + "_links": { + "self": { + "href": "/organizations/018faff4-481f-73ec-a4b8-27ef07b4029b/participants" + } + }, + "participants": [ + { + "_type": "participant", + "_links": { + "self": { + "href": "/organizations/018faff4-481f-73ec-a4b8-27ef07b4029b/participants018faff4-25fb-7973-860a-59eb69b766a4" + } + }, + "id": "018faff4-25fb-7973-860a-59eb69b766a4", + "name": "Bladee The Grand Drainer", + "credentials": { + "email": "bladeee@gmail.com", + "phone": "+79999999999", + "telegram": "@thebladee" + }, + "created_at": 1716654773151, + "updated_at": 1716654773151, + "is_user": true, + "is_admin": true, + "is_owner": true, + "is_active": true + } + ] +} +``` + ## GET **/{organization_id}/transactions** ### Request body: diff --git a/backend/internal/factory/interface.go b/backend/internal/factory/interface.go index 8dd5efb..6f08493 100644 --- a/backend/internal/factory/interface.go +++ b/backend/internal/factory/interface.go @@ -11,6 +11,7 @@ import ( "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" "github.com/emochka2007/block-accounting/internal/pkg/config" "github.com/emochka2007/block-accounting/internal/pkg/logger" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/chain" "github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt" "github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations" "github.com/emochka2007/block-accounting/internal/usecase/interactors/transactions" @@ -90,11 +91,13 @@ func provideOrganizationsController( func provideTxController( log *slog.Logger, txInteractor transactions.TransactionsInteractor, + chainInteractor chain.ChainInteractor, ) controllers.TransactionsController { return controllers.NewTransactionsController( log.WithGroup("transactions-controller"), txInteractor, presenters.NewTransactionsPresenter(), + chainInteractor, ) } diff --git a/backend/internal/factory/wire_gen.go b/backend/internal/factory/wire_gen.go index 468846b..82078b8 100644 --- a/backend/internal/factory/wire_gen.go +++ b/backend/internal/factory/wire_gen.go @@ -35,7 +35,7 @@ func ProvideService(c config.Config) (service.Service, func(), error) { organizationsPresenter := provideOrganizationsPresenter() organizationsController := provideOrganizationsController(logger, organizationsInteractor, organizationsPresenter) transactionsInteractor := provideTxInteractor(logger, transactionsRepository, organizationsInteractor) - transactionsController := provideTxController(logger, transactionsInteractor) + transactionsController := provideTxController(logger, transactionsInteractor, chainInteractor) participantsController := provideParticipantsController(logger, organizationsInteractor, usersInteractor) rootController := provideControllers(logger, authController, organizationsController, transactionsController, participantsController) server := provideRestServer(logger, rootController, c, jwtInteractor) diff --git a/backend/internal/interface/rest/controllers/transactions.go b/backend/internal/interface/rest/controllers/chain.go similarity index 66% rename from backend/internal/interface/rest/controllers/transactions.go rename to backend/internal/interface/rest/controllers/chain.go index 30de116..dc390a2 100644 --- a/backend/internal/interface/rest/controllers/transactions.go +++ b/backend/internal/interface/rest/controllers/chain.go @@ -14,31 +14,45 @@ import ( "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" "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/transactions" "github.com/ethereum/go-ethereum/common" ) +// TODO по хорошему это уебищу надо разносить, но ни времени ни сил пока нет +// в рамках рефакторинка не забыть +// TransactionsController | ChainController type TransactionsController interface { New(w http.ResponseWriter, r *http.Request) ([]byte, error) List(w http.ResponseWriter, r *http.Request) ([]byte, error) UpdateStatus(w http.ResponseWriter, r *http.Request) ([]byte, error) + + NewPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error) + ConfirmPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error) + ListPayrolls(w http.ResponseWriter, r *http.Request) ([]byte, error) + + NewMultisig(w http.ResponseWriter, r *http.Request) ([]byte, error) + ListMultisigs(w http.ResponseWriter, r *http.Request) ([]byte, error) } type transactionsController struct { - log *slog.Logger - txInteractor transactions.TransactionsInteractor - txPresenter presenters.TransactionsPresenter + log *slog.Logger + txInteractor transactions.TransactionsInteractor + txPresenter presenters.TransactionsPresenter + chainInteractor chain.ChainInteractor } func NewTransactionsController( log *slog.Logger, txInteractor transactions.TransactionsInteractor, txPresenter presenters.TransactionsPresenter, + chainInteractor chain.ChainInteractor, ) TransactionsController { return &transactionsController{ - log: log, - txInteractor: txInteractor, - txPresenter: txPresenter, + log: log, + txInteractor: txInteractor, + txPresenter: txPresenter, + chainInteractor: chainInteractor, } } @@ -187,7 +201,59 @@ func (c *transactionsController) UpdateStatus(w http.ResponseWriter, r *http.Req return c.txPresenter.ResponseNewTransaction(ctx, tx) } -// todo creates a new payout -func (c *transactionsController) NewPayout(w http.ResponseWriter, r *http.Request) ([]byte, error) { +func (c *transactionsController) NewMultisig(w http.ResponseWriter, r *http.Request) ([]byte, error) { + req, err := presenters.CreateRequest[domain.NewMultisigRequest](r) + if err != nil { + return nil, fmt.Errorf("error build new transaction request. %w", err) + } + + organizationID, err := ctxmeta.OrganizationId(r.Context()) + if err != nil { + return nil, fmt.Errorf("error fetch organization ID from context. %w", err) + } + + c.log.Debug( + "new multisig request", + slog.String("org id", organizationID.String()), + slog.Any("req", req), + ) + + ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + + ownersPKs := make([]string, len(req.Owners)) + + for i, pk := range req.Owners { + ownersPKs[i] = pk.PublicKey + } + + if req.Confirmations <= 0 { + req.Confirmations = 1 + } + + if err := c.chainInteractor.NewMultisig(ctx, chain.NewMultisigParams{ + OwnersPKs: ownersPKs, + Confirmations: req.Confirmations, + }); err != nil { + return nil, fmt.Errorf("error deploy multisig. %w", err) + } + + return presenters.ResponseOK() +} + +func (s *transactionsController) ListMultisigs(w http.ResponseWriter, r *http.Request) ([]byte, error) { + return nil, nil +} + +// todo creates a new payout +func (c *transactionsController) NewPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error) { + return nil, nil +} + +func (c *transactionsController) ConfirmPayroll(w http.ResponseWriter, r *http.Request) ([]byte, error) { + return nil, nil +} + +func (c *transactionsController) ListPayrolls(w http.ResponseWriter, r *http.Request) ([]byte, error) { return nil, nil } diff --git a/backend/internal/interface/rest/domain/dto.go b/backend/internal/interface/rest/domain/dto.go index c85b9b9..ee98247 100644 --- a/backend/internal/interface/rest/domain/dto.go +++ b/backend/internal/interface/rest/domain/dto.go @@ -112,6 +112,7 @@ type NewMultisigRequest struct { Owners []struct { PublicKey string `json:"public_key"` } `json:"owners"` + Confirmations int `json:confirmations` } func BuildRequest[T any](data []byte) (*T, error) { diff --git a/backend/internal/interface/rest/presenters/participants.go b/backend/internal/interface/rest/presenters/participants.go index 30056ca..d6550dd 100644 --- a/backend/internal/interface/rest/presenters/participants.go +++ b/backend/internal/interface/rest/presenters/participants.go @@ -56,6 +56,7 @@ func (p *participantsPresenter) responseParticipant( domainParticipant.IsUser = true domainParticipant.IsAdmin = user.IsAdmin() domainParticipant.IsOwner = user.IsOwner() + domainParticipant.IsActive = user.Activated } diff --git a/backend/internal/interface/rest/presenters/request.go b/backend/internal/interface/rest/presenters/request.go index a3a748b..097d65e 100644 --- a/backend/internal/interface/rest/presenters/request.go +++ b/backend/internal/interface/rest/presenters/request.go @@ -23,3 +23,11 @@ func CreateRequest[T any](r *http.Request) (*T, error) { return &request, nil } + +type ok struct { + Ok bool +} + +func ResponseOK() ([]byte, error) { + return json.Marshal(&ok{Ok: true}) +} diff --git a/backend/internal/interface/rest/server.go b/backend/internal/interface/rest/server.go index 159f409..d38eb15 100644 --- a/backend/internal/interface/rest/server.go +++ b/backend/internal/interface/rest/server.go @@ -114,18 +114,21 @@ func (s *Server) buildRouter() { }) r.Route("/payrolls", func(r chi.Router) { - r.Get("/", nil) // list payrolls - r.Post("/", nil) // deploy contract + r.Get("/", s.handle(s.controllers.Transactions.ListPayrolls, "list_payrolls")) + r.Post("/", s.handle(s.controllers.Transactions.NewPayroll, "new_payroll")) + r.Put("/", nil) // todo }) r.Route("/multisig", func(r chi.Router) { - r.Post("/", nil) // new multisig (deploy) - r.Get("/", nil) // list + r.Post("/", s.handle(s.controllers.Transactions.NewMultisig, "new_multisig")) + r.Get("/", s.handle(s.controllers.Transactions.ListMultisigs, "list_multisig")) + r.Put("/", nil) // todo }) r.Route("/license", func(r chi.Router) { r.Get("/", nil) // list license r.Post("/", nil) // deploy contract + r.Put("/", nil) // todo }) // join via invite link @@ -133,20 +136,20 @@ func (s *Server) buildRouter() { r.Route("/participants", func(r chi.Router) { r.Get("/", s.handle(s.controllers.Participants.List, "participants_list")) - r.Post("/", nil) + r.Post("/", s.handle(s.controllers.Participants.New, "new_participant")) // generate new invite link r.Post("/invite", s.handle(s.controllers.Auth.Invite, "invite")) r.Route("/{participant_id}", func(r chi.Router) { - r.Put("/", nil) // update user / employee - r.Delete("/", nil) // remove user / employee + r.Get("/", nil) + // r.Put("/", nil) // update user / employee + // r.Delete("/", nil) // remove user / employee - r.Route("/payroll", func(r chi.Router) { - r.Post("/", nil) // set salary - r.Put("/", nil) // edit - r.Get("/", nil) - }) + // r.Route("/payroll", func(r chi.Router) { + // r.Put("/", nil) // edit + // r.Get("/", nil) + // }) }) }) }) diff --git a/backend/internal/usecase/repository/organizations/repository.go b/backend/internal/usecase/repository/organizations/repository.go index 460a3e8..639f51b 100644 --- a/backend/internal/usecase/repository/organizations/repository.go +++ b/backend/internal/usecase/repository/organizations/repository.go @@ -358,12 +358,93 @@ func (r *repositorySQL) Participants( participants := make([]models.OrganizationParticipant, 0, len(params.Ids)) if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { - orgUsersModels, err := r.fetchOrganizationUsers(ctx, params) - if err != nil { - return fmt.Errorf("error fetch organization users raw models. %w", err) + orgUsersModels := make([]fetchOrganizationUsersModel, 0, len(params.Ids)) + + ouQuery := sq.Select( + "ou.organization_id", + "ou.user_id", + "ou.employee_id", + "ou.position", + "ou.added_at", + "ou.updated_at", + "ou.deleted_at", + "ou.is_admin", + "ou.is_owner", + ).Where(sq.Eq{ + "ou.organization_id": params.OrganizationId, + }).From("organizations_users as ou"). + PlaceholderFormat(sq.Dollar) + + if len(params.Ids) > 0 { + ouQuery = ouQuery.Where(sq.Eq{ + "ou.user_id": params.Ids, + }) } - eg, egCtx := errgroup.WithContext(ctx) + rows, err := ouQuery.RunWith(r.Conn(ctx)).QueryContext(ctx) + if err != nil { + return fmt.Errorf("error fetch organization participants. %w", err) + } + + defer func() { + if closeErr := rows.Close(); closeErr != nil { + err = errors.Join(fmt.Errorf("error close rows. %w", closeErr), err) + } + }() + + for rows.Next() { + var ( + organizationID uuid.UUID + userID uuid.UUID + employeeID uuid.UUID + position sql.NullString + addedAt time.Time + updatedAt time.Time + deletedAt sql.NullTime + isAdmin bool + isOwner bool + ) + + if err = rows.Scan( + &organizationID, + &userID, + &employeeID, + &position, + &addedAt, + &updatedAt, + &deletedAt, + &isAdmin, + &isOwner, + ); err != nil { + return fmt.Errorf("error scan row. %w", err) + } + + if params.EmployeesOnly && employeeID == uuid.Nil { + continue + } + + if params.UsersOnly && userID == uuid.Nil { + continue + } + + if params.ActiveOnly && deletedAt.Valid { + continue + } + + orgUsersModels = append(orgUsersModels, fetchOrganizationUsersModel{ + organizationID: organizationID, + userID: userID, + employeeID: employeeID, + position: position.String, + addedAt: addedAt, + updatedAt: updatedAt, + deletedAt: deletedAt.Time, + isAdmin: isAdmin, + isOwner: isOwner, + }) + } + + eg, _ := errgroup.WithContext(ctx) var employees []*models.Employee = make([]*models.Employee, 0, len(orgUsersModels)) if !params.UsersOnly { @@ -376,12 +457,66 @@ func (r *repositorySQL) Participants( } } - employees, err = r.fetchEmployees(egCtx, fetchEmployeesParams{ - IDs: ids, - OrganizationId: params.OrganizationId, - }) + query := sq.Select( + "e.id", + "e.user_id", + "e.organization_id", + "e.wallet_address", + "e.created_at", + "e.updated_at", + ).Where(sq.Eq{ + "e.organization_id": params.OrganizationId, + }).From("employees as e"). + PlaceholderFormat(sq.Dollar) + + if len(ids) > 0 { + query = query.Where(sq.Eq{ + "e.id": ids, + }) + } + + fmt.Println(query.ToSql()) + + rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx) if err != nil { - return fmt.Errorf("error fetch employees. %w", err) + return fmt.Errorf("error fetch employees from database. %w", err) + } + + defer func() { + if closeErr := rows.Close(); closeErr != nil { + err = errors.Join(fmt.Errorf("error close rows. %w", closeErr), err) + } + }() + + for rows.Next() { + var ( + id uuid.UUID + userID uuid.UUID + orgID uuid.UUID + walletAddr []byte + createdAt time.Time + updatedAt time.Time + ) + + if err = rows.Scan( + &id, + &userID, + &orgID, + &walletAddr, + &createdAt, + &updatedAt, + ); err != nil { + return fmt.Errorf("error scan row. %w", err) + } + + employees = append(employees, &models.Employee{ + ID: id, + UserID: userID, + OrganizationId: orgID, + WalletAddress: walletAddr, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) } return nil @@ -399,7 +534,10 @@ func (r *repositorySQL) Participants( } } - usrs, err = r.usersRepository.Get(egCtx, users.GetParams{ + usersCtx, cancel := context.WithTimeout(context.TODO(), 3*time.Second) + defer cancel() + + usrs, err = r.usersRepository.Get(usersCtx, users.GetParams{ Ids: ids, }) if err != nil { @@ -437,7 +575,11 @@ func (r *repositorySQL) Participants( User: *u, OrgPosition: ou.position, Admin: ou.isAdmin, + Owner: ou.isOwner, Employee: employee, + CreatedAt: ou.addedAt, + UpdatedAt: ou.updatedAt, + DeletedAt: ou.deletedAt, }) break @@ -497,8 +639,6 @@ 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) diff --git a/backend/migrations/blockd.sql b/backend/migrations/blockd.sql index 91d5b6b..2fed27e 100644 --- a/backend/migrations/blockd.sql +++ b/backend/migrations/blockd.sql @@ -8,6 +8,7 @@ create table if not exists users ( mnemonic varchar(500) not null, seed bytea not null unique, created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp, activated_at timestamp default null ); diff --git a/backend/test.txt b/backend/test.txt new file mode 100644 index 0000000..b04b500 --- /dev/null +++ b/backend/test.txt @@ -0,0 +1,554 @@ + + +Австралийский доллар +Australian Dollar +1 +R01010 +36 +AUD + + +Австрийский шиллинг +Austrian Shilling +1000 +R01015 +40 +ATS + + +Азербайджанский манат +Azerbaijan Manat +1 +R01020 +944 +AZN + + +Фунт стерлингов Соединенного королевства +British Pound Sterling +1 +R01035 +826 +GBP + + +Ангольская новая кванза +Angolan new Kwanza +100000 +R01040 +24 +AON + + +Армянский драм +Armenia Dram +100 +R01060 +51 +AMD + + +Белорусский рубль +Belarussian Ruble +1 +R01090 +933 +BYN + + +Бельгийский франк +Belgium Franc +1000 +R01095 +56 +BEF + + +Болгарский лев +Bulgarian lev +1 +R01100 +975 +BGN + + +Бразильский реал +Brazil Real +1 +R01115 +986 +BRL + + +Венгерский форинт +Hungarian Forint +100 +R01135 +348 +HUF + + +Вьетнамский донг +Vietnam Dong +10000 +R01150 +704 +VND + + +Гонконгский доллар +Hong Kong Dollar +1 +R01200 +344 +HKD + + +Греческая драхма +Greek Drachma +10000 +R01205 +300 +GRD + + +Грузинский лари +Georgia Lari +1 +R01210 +981 +GEL + + +Датская крона +Danish Krone +1 +R01215 +208 +DKK + + +Дирхам ОАЭ +UAE Dirham +1 +R01230 +784 +AED + + +Доллар США +US Dollar +1 +R01235 +840 +USD + + +Евро +Euro +1 +R01239 +978 +EUR + + +Египетский фунт +Egyptian Pound +10 +R01240 +818 +EGP + + +Индийская рупия +Indian Rupee +10 +R01270 +356 +INR + + +Индонезийская рупия +Indonesian Rupiah +10000 +R01280 +360 +IDR + + +Ирландский фунт +Irish Pound +100 +R01305 +372 +IEP + + +Исландская крона +Iceland Krona +10000 +R01310 +352 +ISK + + +Испанская песета +Spanish Peseta +10000 +R01315 +724 +ESP + + +Итальянская лира +Italian Lira +100000 +R01325 +380 +ITL + + +Казахстанский тенге +Kazakhstan Tenge +100 +R01335 +398 +KZT + + +Канадский доллар +Canadian Dollar +1 +R01350 +124 +CAD + + +Катарский риал +Qatari Riyal +1 +R01355 +634 +QAR + + +Киргизский сом +Kyrgyzstan Som +10 +R01370 +417 +KGS + + +Китайский юань +China Yuan +1 +R01375 +156 +CNY + + +Кувейтский динар +Kuwaiti Dinar +10 +R01390 +414 +KWD + + +Латвийский лат +Latvian Lat +1 +R01405 +428 +LVL + + +Ливанский фунт +Lebanese Pound +100000 +R01420 +422 +LBP + + +Литовский лит +Lithuanian Lita +1 +R01435 +440 +LTL + + +Литовский талон +Lithuanian talon +1 +R01435 + + + + +Молдавский лей +Moldova Lei +10 +R01500 +498 +MDL + + +Немецкая марка +Deutsche Mark +1 +R01510 +276 +DEM + + +Немецкая марка +Deutsche Mark +100 +R01510 +280 +DEM + + +Нидерландский гульден +Netherlands Gulden +100 +R01523 +528 +NLG + + +Новозеландский доллар +New Zealand Dollar +1 +R01530 +554 +NZD + + +Норвежская крона +Norwegian Krone +10 +R01535 +578 +NOK + + +Польский злотый +Polish Zloty +1 +R01565 +985 +PLN + + +Португальский эскудо +Portuguese Escudo +10000 +R01570 +620 +PTE + + +Румынский лей +Romanian Leu +10000 +R01585 +642 +ROL + + +Румынский лей +Romanian Leu +1 +R01585 +946 +RON + + +СДР (специальные права заимствования) +SDR +1 +R01589 +960 +XDR + + +Сингапурский доллар +Singapore Dollar +1 +R01625 +702 +SGD + + +Суринамский доллар +Surinam Dollar +1 +R01665 +968 +SRD + + +Таджикский сомони +Tajikistan Ruble +10 +R01670 +972 +TJS + + +Таиландский бат +Thai Baht +10 +R01675 +764 +THB + + +Турецкая лира +Turkish Lira +10 +R01700 +949 +TRY + + +Туркменский манат +Turkmenistan Manat +10000 +R01710 +795 +TMM + + +Новый туркменский манат +New Turkmenistan Manat +1 +R01710 +934 +TMT + + +Узбекский сум +Uzbekistan Sum +10000 +R01717 +860 +UZS + + +Украинская гривна +Ukrainian Hryvnia +10 +R01720 +980 +UAH + + +Украинский карбованец +Ukrainian Hryvnia +1 +R01720 + + + + +Финляндская марка +Finnish Marka +100 +R01740 +246 +FIM + + +Французский франк +French Franc +1000 +R01750 +250 +FRF + + +Чешская крона +Czech Koruna +10 +R01760 +203 +CZK + + +Шведская крона +Swedish Krona +10 +R01770 +752 +SEK + + +Швейцарский франк +Swiss Franc +1 +R01775 +756 +CHF + + +ЭКЮ +ECU +1 +R01790 +954 +XEU + + +Эстонская крона +Estonian Kroon +10 +R01795 +233 +EEK + + +Югославский новый динар +Yugoslavian Dinar +1 +R01804 +890 +YUN + + +Сербский динар +Serbian Dinar +100 +R01804 +941 +RSD + + +Южноафриканский рэнд +S.African Rand +10 +R01810 +710 +ZAR + + +Вон Республики Корея +South Korean Won +1000 +R01815 +410 +KRW + + +Японская иена +Japanese Yen +100 +R01820 +392 +JPY + +