2024-05-24 17:44:24 +00:00
|
|
|
package organizations
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"log/slog"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
|
|
|
|
"github.com/emochka2007/block-accounting/internal/pkg/hdwallet"
|
|
|
|
"github.com/emochka2007/block-accounting/internal/pkg/logger"
|
|
|
|
"github.com/emochka2007/block-accounting/internal/pkg/models"
|
|
|
|
"github.com/emochka2007/block-accounting/internal/usecase/repository/cache"
|
|
|
|
"github.com/emochka2007/block-accounting/internal/usecase/repository/organizations"
|
2024-05-24 22:45:56 +00:00
|
|
|
"github.com/ethereum/go-ethereum/common"
|
2024-05-24 17:44:24 +00:00
|
|
|
"github.com/google/uuid"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
ErrorUnauthorizedAccess = errors.New("unauthorized access")
|
|
|
|
)
|
|
|
|
|
|
|
|
type CreateParams struct {
|
|
|
|
Name string
|
|
|
|
Address string
|
|
|
|
WalletMnemonic string
|
|
|
|
}
|
|
|
|
|
|
|
|
type ListParams struct {
|
|
|
|
Ids uuid.UUIDs
|
|
|
|
UserId uuid.UUID
|
|
|
|
|
|
|
|
Cursor string
|
|
|
|
Limit uint8 // Max limit is 50 (may change)
|
|
|
|
}
|
|
|
|
|
|
|
|
type ParticipantParams struct {
|
|
|
|
ID uuid.UUID
|
|
|
|
OrganizationID uuid.UUID
|
|
|
|
|
|
|
|
UsersOnly bool
|
|
|
|
ActiveOnly bool
|
|
|
|
EmployeesOnly bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type ParticipantsParams struct {
|
|
|
|
IDs uuid.UUIDs
|
|
|
|
OrganizationID uuid.UUID
|
2024-05-26 10:42:06 +00:00
|
|
|
PKs [][]byte
|
2024-05-24 17:44:24 +00:00
|
|
|
|
|
|
|
UsersOnly bool
|
|
|
|
ActiveOnly bool
|
|
|
|
EmployeesOnly bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type OrganizationsInteractor interface {
|
2024-05-24 22:45:56 +00:00
|
|
|
Create(ctx context.Context, params CreateParams) (*models.Organization, error)
|
|
|
|
List(ctx context.Context, params ListParams) (*ListResponse, error)
|
|
|
|
|
|
|
|
Participant(ctx context.Context, params ParticipantParams) (models.OrganizationParticipant, error)
|
|
|
|
Participants(ctx context.Context, params ParticipantsParams) ([]models.OrganizationParticipant, error)
|
|
|
|
AddParticipant(ctx context.Context, params AddParticipantParams) (models.OrganizationParticipant, error)
|
2024-05-24 17:44:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type organizationsInteractor struct {
|
|
|
|
log *slog.Logger
|
|
|
|
orgRepository organizations.Repository
|
|
|
|
cache cache.Cache
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewOrganizationsInteractor(
|
|
|
|
log *slog.Logger,
|
|
|
|
orgRepository organizations.Repository,
|
|
|
|
cache cache.Cache,
|
|
|
|
) OrganizationsInteractor {
|
|
|
|
return &organizationsInteractor{
|
|
|
|
log: log,
|
|
|
|
orgRepository: orgRepository,
|
|
|
|
cache: cache,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type organizationsListCursor struct {
|
|
|
|
Id uuid.UUID `json:"id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func newOrganizationsListCursor(id ...uuid.UUID) *organizationsListCursor {
|
|
|
|
if len(id) > 0 {
|
|
|
|
return &organizationsListCursor{id[0]}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new(organizationsListCursor)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *organizationsListCursor) 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 *organizationsListCursor) 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 ListResponse struct {
|
|
|
|
Organizations models.Organizations
|
|
|
|
NextCursor string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i ListResponse) MarshalBinary() ([]byte, error) {
|
|
|
|
return json.Marshal(i)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *organizationsInteractor) List(
|
|
|
|
ctx context.Context,
|
|
|
|
params ListParams,
|
|
|
|
) (*ListResponse, error) {
|
|
|
|
user, err := ctxmeta.User(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error fetch user from context. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if params.UserId != uuid.Nil {
|
|
|
|
if params.UserId != user.Id() {
|
|
|
|
return nil, fmt.Errorf("error unauthorized organizations list access. %w", ErrorUnauthorizedAccess)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
params.UserId = user.Id()
|
|
|
|
}
|
|
|
|
|
|
|
|
out := new(ListResponse)
|
|
|
|
|
|
|
|
// BUG: empty org set fetched from cache
|
|
|
|
// if err := i.cache.Get(ctx, params, out); err != nil && errors.Is(err, redis.Nil) {
|
|
|
|
// i.log.Error("no cache hit!", logger.Err(err))
|
|
|
|
// } else {
|
|
|
|
// i.log.Debug("cache hit!", slog.AnyValue(out))
|
|
|
|
// return out, nil
|
|
|
|
// }
|
|
|
|
|
|
|
|
if params.Limit <= 0 || params.Limit > 50 {
|
|
|
|
params.Limit = 50
|
|
|
|
}
|
|
|
|
|
|
|
|
cursor := newOrganizationsListCursor()
|
|
|
|
|
|
|
|
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?
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
i.log.Debug(
|
|
|
|
"organizations_list",
|
|
|
|
slog.String("cursor", params.Cursor),
|
|
|
|
slog.Int("limit", int(params.Limit)),
|
|
|
|
slog.Any("cursor-id", cursor.Id),
|
|
|
|
slog.Any("ids", params.Ids),
|
|
|
|
slog.Any("user_id", params.UserId),
|
|
|
|
)
|
|
|
|
|
|
|
|
orgs, err := i.orgRepository.Get(ctx, organizations.GetParams{
|
|
|
|
UserId: params.UserId,
|
|
|
|
Ids: params.Ids,
|
|
|
|
Limit: int64(params.Limit),
|
|
|
|
CursorId: cursor.Id,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error fetch organizations. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var nextCursor string
|
|
|
|
|
|
|
|
if len(orgs) >= 50 || len(orgs) >= int(params.Limit) {
|
|
|
|
cursor.Id = orgs[len(orgs)-1].ID
|
|
|
|
if nextCursor, err = cursor.encode(); err != nil {
|
|
|
|
return nil, fmt.Errorf("error encode next page token. %w", err) // maybe just log error?
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
out = &ListResponse{
|
|
|
|
Organizations: orgs,
|
|
|
|
NextCursor: nextCursor,
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = i.cache.Cache(ctx, params, *out, time.Hour*1); err != nil {
|
|
|
|
i.log.Error("error add cache record", logger.Err(err))
|
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *organizationsInteractor) Create(
|
|
|
|
ctx context.Context,
|
|
|
|
params CreateParams,
|
|
|
|
) (*models.Organization, error) {
|
|
|
|
var walletSeed []byte
|
|
|
|
|
|
|
|
user, err := ctxmeta.User(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error fetch user from context. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if params.WalletMnemonic == "" {
|
|
|
|
walletSeed = user.Seed()
|
|
|
|
} else {
|
|
|
|
seed, err := hdwallet.NewSeedFromMnemonic(params.WalletMnemonic)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error convert organization wallet mnemonic into a seed. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
walletSeed = seed
|
|
|
|
}
|
|
|
|
|
|
|
|
org := models.Organization{
|
|
|
|
ID: uuid.Must(uuid.NewV7()),
|
|
|
|
Name: params.Name,
|
|
|
|
Address: params.Address,
|
|
|
|
WalletSeed: walletSeed,
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
UpdatedAt: time.Now(),
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := i.orgRepository.CreateAndAdd(ctx, org, user); err != nil {
|
|
|
|
return nil, fmt.Errorf("error create new organization. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &org, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *organizationsInteractor) Participant(
|
|
|
|
ctx context.Context,
|
|
|
|
params ParticipantParams,
|
|
|
|
) (models.OrganizationParticipant, error) {
|
|
|
|
participants, err := i.Participants(ctx, ParticipantsParams{
|
|
|
|
IDs: uuid.UUIDs{params.ID},
|
|
|
|
OrganizationID: params.OrganizationID,
|
|
|
|
ActiveOnly: params.ActiveOnly,
|
|
|
|
UsersOnly: params.UsersOnly,
|
|
|
|
EmployeesOnly: params.EmployeesOnly,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error fetch organization participant. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(participants) == 0 {
|
|
|
|
return nil, fmt.Errorf("error organization participant empty. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return participants[0], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *organizationsInteractor) Participants(
|
|
|
|
ctx context.Context,
|
|
|
|
params ParticipantsParams,
|
|
|
|
) ([]models.OrganizationParticipant, error) {
|
|
|
|
user, err := ctxmeta.User(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error fetch user from context. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = i.orgRepository.Participants(ctx, organizations.ParticipantsParams{
|
|
|
|
Ids: uuid.UUIDs{user.Id()},
|
|
|
|
OrganizationId: params.OrganizationID,
|
|
|
|
ActiveOnly: params.ActiveOnly,
|
|
|
|
UsersOnly: true,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Join(
|
|
|
|
fmt.Errorf("error fetch organization user. %w", err),
|
|
|
|
ErrorUnauthorizedAccess,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
participants, err := i.orgRepository.Participants(ctx, organizations.ParticipantsParams{
|
|
|
|
Ids: params.IDs,
|
|
|
|
OrganizationId: params.OrganizationID,
|
2024-05-26 10:42:06 +00:00
|
|
|
PKs: params.PKs,
|
2024-05-24 17:44:24 +00:00
|
|
|
UsersOnly: params.UsersOnly,
|
|
|
|
EmployeesOnly: params.EmployeesOnly,
|
|
|
|
ActiveOnly: params.ActiveOnly,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error fetch organization participants. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return participants, nil
|
|
|
|
}
|
2024-05-24 22:45:56 +00:00
|
|
|
|
|
|
|
type AddParticipantParams struct {
|
|
|
|
OrganizationID uuid.UUID
|
|
|
|
EmployeeUserID uuid.UUID
|
|
|
|
Name string
|
|
|
|
Position string
|
|
|
|
WalletAddress string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *organizationsInteractor) AddParticipant(
|
|
|
|
ctx context.Context,
|
|
|
|
params AddParticipantParams,
|
|
|
|
) (models.OrganizationParticipant, error) {
|
|
|
|
user, err := ctxmeta.User(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error fetch user from context. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
actor, err := i.Participant(ctx, ParticipantParams{
|
|
|
|
ID: user.Id(),
|
|
|
|
OrganizationID: params.OrganizationID,
|
|
|
|
ActiveOnly: true,
|
|
|
|
UsersOnly: true,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error fetch actor. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !actor.IsOwner() {
|
|
|
|
return nil, fmt.Errorf("error actor not an owner")
|
|
|
|
}
|
|
|
|
|
|
|
|
if !common.IsHexAddress(params.WalletAddress) {
|
|
|
|
return nil, fmt.Errorf("error invalid address")
|
|
|
|
}
|
|
|
|
|
|
|
|
participantID := uuid.Must(uuid.NewV7())
|
|
|
|
|
|
|
|
empl := models.Employee{
|
|
|
|
ID: participantID,
|
|
|
|
EmployeeName: params.Name,
|
|
|
|
UserID: params.EmployeeUserID,
|
|
|
|
OrganizationId: params.OrganizationID,
|
|
|
|
WalletAddress: common.FromHex(params.WalletAddress),
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
UpdatedAt: time.Now(),
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = i.orgRepository.AddEmployee(ctx, empl); err != nil {
|
|
|
|
return nil, fmt.Errorf("error add new employee. %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &empl, nil
|
|
|
|
}
|