diff --git a/components/ledger/internal/adapters/postgres/asset/asset.mock.go b/components/ledger/internal/adapters/postgres/asset/asset.mock.go index 78975a39..dbeb2bc8 100644 --- a/components/ledger/internal/adapters/postgres/asset/asset.mock.go +++ b/components/ledger/internal/adapters/postgres/asset/asset.mock.go @@ -11,18 +11,18 @@ package asset import ( context "context" - gomock "go.uber.org/mock/gomock" reflect "reflect" mmodel "github.com/LerianStudio/midaz/pkg/mmodel" - uuid "github.com/google/uuid" + gomock "go.uber.org/mock/gomock" ) // MockRepository is a mock of Repository interface. type MockRepository struct { ctrl *gomock.Controller recorder *MockRepositoryMockRecorder + isgomock struct{} } // MockRepositoryMockRecorder is the mock recorder for MockRepository. @@ -43,105 +43,105 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { } // Create mocks base method. -func (m *MockRepository) Create(arg0 context.Context, arg1 *mmodel.Asset) (*mmodel.Asset, error) { +func (m *MockRepository) Create(ctx context.Context, asset *mmodel.Asset) (*mmodel.Asset, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret := m.ctrl.Call(m, "Create", ctx, asset) ret0, _ := ret[0].(*mmodel.Asset) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. -func (mr *MockRepositoryMockRecorder) Create(arg0, arg1 any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Create(ctx, asset any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), ctx, asset) } // Delete mocks base method. -func (m *MockRepository) Delete(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) error { +func (m *MockRepository) Delete(ctx context.Context, organizationID, ledgerID, id uuid.UUID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "Delete", ctx, organizationID, ledgerID, id) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. -func (mr *MockRepositoryMockRecorder) Delete(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Delete(ctx, organizationID, ledgerID, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), ctx, organizationID, ledgerID, id) } // Find mocks base method. -func (m *MockRepository) Find(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) (*mmodel.Asset, error) { +func (m *MockRepository) Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*mmodel.Asset, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Find", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "Find", ctx, organizationID, ledgerID, id) ret0, _ := ret[0].(*mmodel.Asset) ret1, _ := ret[1].(error) return ret0, ret1 } // Find indicates an expected call of Find. -func (mr *MockRepositoryMockRecorder) Find(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Find(ctx, organizationID, ledgerID, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockRepository)(nil).Find), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockRepository)(nil).Find), ctx, organizationID, ledgerID, id) } -// FindAll mocks base method. -func (m *MockRepository) FindAll(arg0 context.Context, arg1, arg2 uuid.UUID, arg3, arg4 int) ([]*mmodel.Asset, error) { +// FindAllWithDeleted mocks base method. +func (m *MockRepository) FindAllWithDeleted(ctx context.Context, organizationID, ledgerID uuid.UUID, limit, page int) ([]*mmodel.Asset, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindAll", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "FindAllWithDeleted", ctx, organizationID, ledgerID, limit, page) ret0, _ := ret[0].([]*mmodel.Asset) ret1, _ := ret[1].(error) return ret0, ret1 } -// FindAll indicates an expected call of FindAll. -func (mr *MockRepositoryMockRecorder) FindAll(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { +// FindAllWithDeleted indicates an expected call of FindAllWithDeleted. +func (mr *MockRepositoryMockRecorder) FindAllWithDeleted(ctx, organizationID, ledgerID, limit, page any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAll", reflect.TypeOf((*MockRepository)(nil).FindAll), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAllWithDeleted", reflect.TypeOf((*MockRepository)(nil).FindAllWithDeleted), ctx, organizationID, ledgerID, limit, page) } // FindByNameOrCode mocks base method. -func (m *MockRepository) FindByNameOrCode(arg0 context.Context, arg1, arg2 uuid.UUID, arg3, arg4 string) (bool, error) { +func (m *MockRepository) FindByNameOrCode(ctx context.Context, organizationID, ledgerID uuid.UUID, name, code string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindByNameOrCode", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "FindByNameOrCode", ctx, organizationID, ledgerID, name, code) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // FindByNameOrCode indicates an expected call of FindByNameOrCode. -func (mr *MockRepositoryMockRecorder) FindByNameOrCode(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) FindByNameOrCode(ctx, organizationID, ledgerID, name, code any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByNameOrCode", reflect.TypeOf((*MockRepository)(nil).FindByNameOrCode), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByNameOrCode", reflect.TypeOf((*MockRepository)(nil).FindByNameOrCode), ctx, organizationID, ledgerID, name, code) } // ListByIDs mocks base method. -func (m *MockRepository) ListByIDs(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 []uuid.UUID) ([]*mmodel.Asset, error) { +func (m *MockRepository) ListByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*mmodel.Asset, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListByIDs", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "ListByIDs", ctx, organizationID, ledgerID, ids) ret0, _ := ret[0].([]*mmodel.Asset) ret1, _ := ret[1].(error) return ret0, ret1 } // ListByIDs indicates an expected call of ListByIDs. -func (mr *MockRepositoryMockRecorder) ListByIDs(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) ListByIDs(ctx, organizationID, ledgerID, ids any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByIDs", reflect.TypeOf((*MockRepository)(nil).ListByIDs), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByIDs", reflect.TypeOf((*MockRepository)(nil).ListByIDs), ctx, organizationID, ledgerID, ids) } // Update mocks base method. -func (m *MockRepository) Update(arg0 context.Context, arg1, arg2, arg3 uuid.UUID, arg4 *mmodel.Asset) (*mmodel.Asset, error) { +func (m *MockRepository) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, asset *mmodel.Asset) (*mmodel.Asset, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "Update", ctx, organizationID, ledgerID, id, asset) ret0, _ := ret[0].(*mmodel.Asset) ret1, _ := ret[1].(error) return ret0, ret1 } // Update indicates an expected call of Update. -func (mr *MockRepositoryMockRecorder) Update(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Update(ctx, organizationID, ledgerID, id, asset any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), ctx, organizationID, ledgerID, id, asset) } diff --git a/components/ledger/internal/adapters/postgres/asset/asset.postgresql.go b/components/ledger/internal/adapters/postgres/asset/asset.postgresql.go index 4ca7eac7..d89cf8ac 100644 --- a/components/ledger/internal/adapters/postgres/asset/asset.postgresql.go +++ b/components/ledger/internal/adapters/postgres/asset/asset.postgresql.go @@ -27,7 +27,7 @@ import ( //go:generate mockgen --destination=asset.mock.go --package=asset . Repository type Repository interface { Create(ctx context.Context, asset *mmodel.Asset) (*mmodel.Asset, error) - FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID, limit, page int) ([]*mmodel.Asset, error) + FindAllWithDeleted(ctx context.Context, organizationID, ledgerID uuid.UUID, limit, page int) ([]*mmodel.Asset, error) ListByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*mmodel.Asset, error) Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*mmodel.Asset, error) FindByNameOrCode(ctx context.Context, organizationID, ledgerID uuid.UUID, name, code string) (bool, error) @@ -164,8 +164,8 @@ func (r *AssetPostgreSQLRepository) FindByNameOrCode(ctx context.Context, organi return false, nil } -// FindAll retrieves Asset entities from the database. -func (r *AssetPostgreSQLRepository) FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID, limit, page int) ([]*mmodel.Asset, error) { +// FindAllWithDeleted retrieves Asset entities from the database with soft-deleted records. +func (r *AssetPostgreSQLRepository) FindAllWithDeleted(ctx context.Context, organizationID, ledgerID uuid.UUID, limit, page int) ([]*mmodel.Asset, error) { tracer := pkg.NewTracerFromContext(ctx) ctx, span := tracer.Start(ctx, "postgres.find_all_assets") @@ -184,7 +184,6 @@ func (r *AssetPostgreSQLRepository) FindAll(ctx context.Context, organizationID, From(r.tableName). Where(squirrel.Expr("organization_id = ?", organizationID)). Where(squirrel.Expr("ledger_id = ?", ledgerID)). - Where(squirrel.Eq{"deleted_at": nil}). OrderBy("created_at DESC"). Limit(pkg.SafeIntToUint64(limit)). Offset(pkg.SafeIntToUint64((page - 1) * limit)). diff --git a/components/ledger/internal/services/command/create-asset.go b/components/ledger/internal/services/command/create-asset.go index 798f038b..ae416d31 100644 --- a/components/ledger/internal/services/command/create-asset.go +++ b/components/ledger/internal/services/command/create-asset.go @@ -2,6 +2,7 @@ package command import ( "context" + "github.com/LerianStudio/midaz/pkg/constant" "reflect" "time" @@ -94,7 +95,7 @@ func (uc *UseCase) CreateAsset(ctx context.Context, organizationID, ledgerID uui inst.Metadata = metadata - aAlias := "@external/" + cii.Code + aAlias := constant.DefaultExternalAccountAliasPrefix + cii.Code aStatusDescription := "Account external created by asset: " + cii.Code account, err := uc.AccountRepo.ListAccountsByAlias(ctx, organizationID, ledgerID, []string{aAlias}) diff --git a/components/ledger/internal/services/command/delete-asset.go b/components/ledger/internal/services/command/delete-asset.go index 9463f8e7..04992844 100644 --- a/components/ledger/internal/services/command/delete-asset.go +++ b/components/ledger/internal/services/command/delete-asset.go @@ -24,6 +24,41 @@ func (uc *UseCase) DeleteAssetByID(ctx context.Context, organizationID, ledgerID logger.Infof("Remove asset for id: %s", id) + asset, err := uc.AssetRepo.Find(ctx, organizationID, ledgerID, id) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get asset on repo by id", err) + + logger.Errorf("Error getting asset on repo by id: %v", err) + + if errors.Is(err, services.ErrDatabaseItemNotFound) { + return pkg.ValidateBusinessError(constant.ErrAssetIDNotFound, reflect.TypeOf(mmodel.Asset{}).Name(), id) + } + + return err + } + + aAlias := constant.DefaultExternalAccountAliasPrefix + asset.Code + + acc, err := uc.AccountRepo.ListAccountsByAlias(ctx, organizationID, ledgerID, []string{aAlias}) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to retrieve asset external account", err) + + logger.Errorf("Error retrieving asset external account: %v", err) + + return err + } + + if len(acc) > 0 { + err := uc.AccountRepo.Delete(ctx, organizationID, ledgerID, nil, uuid.MustParse(acc[0].ID)) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to delete asset external account", err) + + logger.Errorf("Error deleting asset external account: %v", err) + + return err + } + } + if err := uc.AssetRepo.Delete(ctx, organizationID, ledgerID, id); err != nil { mopentelemetry.HandleSpanError(&span, "Failed to delete asset on repo by id", err) diff --git a/components/ledger/internal/services/query/get-all-asset.go b/components/ledger/internal/services/query/get-all-asset.go index 862a2278..67854f04 100644 --- a/components/ledger/internal/services/query/get-all-asset.go +++ b/components/ledger/internal/services/query/get-all-asset.go @@ -25,7 +25,7 @@ func (uc *UseCase) GetAllAssets(ctx context.Context, organizationID, ledgerID uu logger.Infof("Retrieving assets") - assets, err := uc.AssetRepo.FindAll(ctx, organizationID, ledgerID, filter.Limit, filter.Page) + assets, err := uc.AssetRepo.FindAllWithDeleted(ctx, organizationID, ledgerID, filter.Limit, filter.Page) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to get assets on repo", err) diff --git a/components/ledger/internal/services/query/get-all-asset_test.go b/components/ledger/internal/services/query/get-all-asset_test.go index 4eb58ff7..053eaa07 100644 --- a/components/ledger/internal/services/query/get-all-asset_test.go +++ b/components/ledger/internal/services/query/get-all-asset_test.go @@ -33,10 +33,10 @@ func TestGetAllAssets(t *testing.T) { assets := []*mmodel.Asset{{}} mockAssetRepo. EXPECT(). - FindAll(gomock.Any(), organizationID, ledgerID, page, limit). + FindAllWithDeleted(gomock.Any(), organizationID, ledgerID, page, limit). Return(assets, nil). Times(1) - res, err := uc.AssetRepo.FindAll(context.TODO(), organizationID, ledgerID, page, limit) + res, err := uc.AssetRepo.FindAllWithDeleted(context.TODO(), organizationID, ledgerID, page, limit) assert.NoError(t, err) assert.Len(t, res, 1) @@ -46,10 +46,10 @@ func TestGetAllAssets(t *testing.T) { errMsg := "errDatabaseItemNotFound" mockAssetRepo. EXPECT(). - FindAll(gomock.Any(), organizationID, ledgerID, page, limit). + FindAllWithDeleted(gomock.Any(), organizationID, ledgerID, page, limit). Return(nil, errors.New(errMsg)). Times(1) - res, err := uc.AssetRepo.FindAll(context.TODO(), organizationID, ledgerID, page, limit) + res, err := uc.AssetRepo.FindAllWithDeleted(context.TODO(), organizationID, ledgerID, page, limit) assert.EqualError(t, err, errMsg) assert.Nil(t, res) diff --git a/pkg/constant/account.go b/pkg/constant/account.go new file mode 100644 index 00000000..ba9c1d02 --- /dev/null +++ b/pkg/constant/account.go @@ -0,0 +1,5 @@ +package constant + +const ( + DefaultExternalAccountAliasPrefix = "@external/" +) diff --git a/pkg/mmodel/account.go b/pkg/mmodel/account.go index 3e97a7a6..50eac513 100644 --- a/pkg/mmodel/account.go +++ b/pkg/mmodel/account.go @@ -13,7 +13,7 @@ import ( type CreateAccountInput struct { AssetCode string `json:"assetCode" validate:"required,max=100" example:"BRL"` Name string `json:"name" validate:"max=256" example:"My Account"` - Alias *string `json:"alias" validate:"max=100,excludes=@external/" example:"@person1"` + Alias *string `json:"alias" validate:"max=100,prohibitedexternalaccountprefix" example:"@person1"` Type string `json:"type" validate:"required" example:"creditCard"` ParentAccountID *string `json:"parentAccountId" validate:"omitempty,uuid" example:"00000000-0000-0000-0000-000000000000"` ProductID *string `json:"productId" validate:"omitempty,uuid" example:"00000000-0000-0000-0000-000000000000"` @@ -34,7 +34,7 @@ type UpdateAccountInput struct { Status Status `json:"status"` AllowSending *bool `json:"allowSending" example:"true"` AllowReceiving *bool `json:"allowReceiving" example:"true"` - Alias *string `json:"alias" validate:"max=100,excludes=@external/" example:"@person1"` + Alias *string `json:"alias" validate:"max=100,prohibitedexternalaccountprefix" example:"@person1"` ProductID *string `json:"productId" validate:"uuid" example:"00000000-0000-0000-0000-000000000000"` Metadata map[string]any `json:"metadata" validate:"dive,keys,keymax=100,endkeys,nonested,valuemax=2000"` } // @name UpdateAccountInput diff --git a/pkg/net/http/withBody.go b/pkg/net/http/withBody.go index 0d02d115..b29b06ec 100644 --- a/pkg/net/http/withBody.go +++ b/pkg/net/http/withBody.go @@ -214,6 +214,7 @@ func newValidator() (*validator.Validate, ut.Translator) { _ = v.RegisterValidation("nonested", validateMetadataNestedValues) _ = v.RegisterValidation("valuemax", validateMetadataValueMaxLength) _ = v.RegisterValidation("singletransactiontype", validateSingleTransactionType) + _ = v.RegisterValidation("prohibitedexternalaccountprefix", validateProhibitedExternalAccountPrefix) _ = v.RegisterTranslation("required", trans, func(ut ut.Translator) error { return ut.Add("required", "{0} is a required field", true) @@ -271,6 +272,15 @@ func newValidator() (*validator.Validate, ut.Translator) { return t }) + _ = v.RegisterTranslation("prohibitedexternalaccountprefix", trans, func(ut ut.Translator) error { + prefix := cn.DefaultExternalAccountAliasPrefix + return ut.Add("prohibitedexternalaccountprefix", "{0} cannot contain the text '"+prefix+"'", true) + }, func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T("prohibitedexternalaccountprefix", formatErrorFieldName(fe.Namespace())) + + return t + }) + return v, trans } @@ -349,6 +359,13 @@ func validateSingleTransactionType(fl validator.FieldLevel) bool { return true } +// validateProhibitedExternalAccountPrefix +func validateProhibitedExternalAccountPrefix(fl validator.FieldLevel) bool { + f := fl.Field().Interface().(string) + + return !strings.Contains(f, cn.DefaultExternalAccountAliasPrefix) +} + // formatErrorFieldName formats metadata field error names for error messages func formatErrorFieldName(text string) string { re, _ := regexp.Compile(`\.(.+)$`)