From 1704905bad9419e953921fb47066ae285725edba Mon Sep 17 00:00:00 2001 From: Brecci Date: Thu, 5 Dec 2024 18:53:53 -0300 Subject: [PATCH 1/4] refactor(asset-rate): update put endpoint with new model and rules :hammer: --- components/auth/setup/00_init.sql | 3 +- components/auth/setup/init_data.json | 30 +++- .../internal/adapters/http/in/assetrate.go | 10 +- .../internal/adapters/http/in/routes.go | 2 +- .../adapters/postgres/assetrate/assetrate.go | 102 ++++++----- .../postgres/assetrate/assetrate.mock.go | 49 ++++- .../assetrate/assetrate.postgresql.go | 170 ++++++++++++++++-- .../services/command/create-assetrate.go | 112 ++++++++++-- .../services/command/create-assetrate_test.go | 116 +++++++++++- .../000002_create_asset_rate_table.up.sql | 21 ++- pkg/mpointers/pointers.go | 5 + postman/MIDAZ.postman_collection.json | 90 ++++++++-- 12 files changed, 589 insertions(+), 121 deletions(-) diff --git a/components/auth/setup/00_init.sql b/components/auth/setup/00_init.sql index 048f8ca6..7df6aeeb 100644 --- a/components/auth/setup/00_init.sql +++ b/components/auth/setup/00_init.sql @@ -55,9 +55,8 @@ INSERT INTO "casbin_lerian_enforcer_rule" ("ptype", "v0", "v1", "v2", "v3", "v4" ('p', 'developer_role', 'operation', 'post', '', '', ''), ('p', 'developer_role', 'operation', 'get', '', '', ''), ('p', 'developer_role', 'operation', 'patch', '', '', ''), -('p', 'developer_role', 'asset-rate', 'post', '', '', ''), +('p', 'developer_role', 'asset-rate', 'put', '', '', ''), ('p', 'developer_role', 'asset-rate', 'get', '', '', ''), -('p', 'developer_role', 'asset-rate', 'patch', '', '', ''), ('p', 'user_role', 'organization', 'get', '', '', ''), ('p', 'user_role', 'ledger', 'get', '', '', ''), ('p', 'user_role', 'asset', 'get', '', '', ''), diff --git a/components/auth/setup/init_data.json b/components/auth/setup/init_data.json index 22d7deec..13070b8e 100644 --- a/components/auth/setup/init_data.json +++ b/components/auth/setup/init_data.json @@ -792,8 +792,7 @@ "portfolio", "product", "transaction", - "operation", - "asset-rate" + "operation" ], "actions": [ "get", @@ -807,6 +806,33 @@ "approveTime": "2024-01-01T22:51:35+08:00", "state": "Approved" }, + { + "owner": "lerian", + "name": "developer-api-permission", + "displayName": "Developer API Permission", + "isEnabled": true, + "model": "api-model", + "roles": [ + "lerian/developer_role" + ], + "users": [ + "lerian/user_lisa" + ], + "resourceType": "Custom", + "resources": [ + "asset-rate" + ], + "actions": [ + "get", + "put" + ], + "domains": [], + "effect": "Allow", + "submitter": "admin", + "approver": "admin", + "approveTime": "2024-01-01T22:51:35+08:00", + "state": "Approved" + }, { "owner": "lerian", "name": "grpc-api-permission", diff --git a/components/transaction/internal/adapters/http/in/assetrate.go b/components/transaction/internal/adapters/http/in/assetrate.go index 66f5e8d3..0ce469ad 100644 --- a/components/transaction/internal/adapters/http/in/assetrate.go +++ b/components/transaction/internal/adapters/http/in/assetrate.go @@ -18,10 +18,10 @@ type AssetRateHandler struct { Query *query.UseCase } -// CreateAssetRate creates a new asset rate. +// CreateOrUpdateAssetRate creates or updates an asset rate. // -// @Summary Create an AssetRate -// @Description Create an AssetRate with the input payload +// @Summary Create or Update an AssetRate +// @Description Create or Update an AssetRate with the input details // @Tags Asset Rates // @Accept json // @Produce json @@ -32,7 +32,7 @@ type AssetRateHandler struct { // @Param asset-rate body assetrate.CreateAssetRateInput true "AssetRate Input" // @Success 200 {object} assetrate.AssetRate // @Router /v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates [post] -func (handler *AssetRateHandler) CreateAssetRate(p any, c *fiber.Ctx) error { +func (handler *AssetRateHandler) CreateOrUpdateAssetRate(p any, c *fiber.Ctx) error { ctx := c.UserContext() logger := pkg.NewLoggerFromContext(ctx) @@ -57,7 +57,7 @@ func (handler *AssetRateHandler) CreateAssetRate(p any, c *fiber.Ctx) error { return http.WithError(c, err) } - assetRate, err := handler.Command.CreateAssetRate(ctx, organizationID, ledgerID, payload) + assetRate, err := handler.Command.CreateOrUpdateAssetRate(ctx, organizationID, ledgerID, payload) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to create AssetRate on command", err) diff --git a/components/transaction/internal/adapters/http/in/routes.go b/components/transaction/internal/adapters/http/in/routes.go index 22ec7d25..acf123b3 100644 --- a/components/transaction/internal/adapters/http/in/routes.go +++ b/components/transaction/internal/adapters/http/in/routes.go @@ -50,7 +50,7 @@ func NewRouter(lg mlog.Logger, tl *mopentelemetry.Telemetry, cc *mcasdoor.Casdoo f.Patch("/v1/organizations/:organization_id/ledgers/:ledger_id/transactions/:transaction_id/operations/:operation_id", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("operation"), http.ParseUUIDPathParameters, http.WithBody(new(operation.UpdateOperationInput), oh.UpdateOperation)) // Asset-rate - f.Post("/v1/organizations/:organization_id/ledgers/:ledger_id/asset-rates", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("asset-rate"), http.ParseUUIDPathParameters, http.WithBody(new(assetrate.CreateAssetRateInput), ah.CreateAssetRate)) + f.Put("/v1/organizations/:organization_id/ledgers/:ledger_id/asset-rates", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("asset-rate"), http.ParseUUIDPathParameters, http.WithBody(new(assetrate.CreateAssetRateInput), ah.CreateOrUpdateAssetRate)) f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/asset-rates/:asset_rate_id", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("asset-rate"), http.ParseUUIDPathParameters, ah.GetAssetRate) // Health diff --git a/components/transaction/internal/adapters/postgres/assetrate/assetrate.go b/components/transaction/internal/adapters/postgres/assetrate/assetrate.go index 7f31b20e..d7dea37b 100644 --- a/components/transaction/internal/adapters/postgres/assetrate/assetrate.go +++ b/components/transaction/internal/adapters/postgres/assetrate/assetrate.go @@ -8,16 +8,19 @@ import ( // AssetRatePostgreSQLModel represents the entity AssetRatePostgreSQLModel into SQL context in Database type AssetRatePostgreSQLModel struct { - ID string - BaseAssetCode string - CounterAssetCode string - Amount float64 - Scale float64 - Source string - OrganizationID string - LedgerID string - CreatedAt time.Time - Metadata map[string]any + ID string + OrganizationID string + LedgerID string + ExternalID string + From string + To string + Rate float64 + RateScale float64 + Source *string + TTL int + CreatedAt time.Time + UpdatedAt time.Time + Metadata map[string]any } // CreateAssetRateInput is a struct design to encapsulate payload data. @@ -25,12 +28,14 @@ type AssetRatePostgreSQLModel struct { // swagger:model CreateAssetRateInput // @Description CreateAssetRateInput is the input payload to create an asset rate. type CreateAssetRateInput struct { - BaseAssetCode string `json:"baseAssetCode" example:"BRL"` - CounterAssetCode string `json:"counterAssetCode" example:"USD"` - Amount float64 `json:"amount" example:"5000"` - Scale float64 `json:"scale" example:"2"` - Source string `json:"source" example:"@person1"` - Metadata map[string]any `json:"metadata,omitempty"` + From string `json:"from" validate:"required" example:"USD"` + To string `json:"to" validate:"required" example:"BRL"` + Rate int `json:"rate" validate:"required" example:"100"` + Scale int `json:"scale,omitempty" validate:"gte=0" example:"2"` + Source *string `json:"source,omitempty" example:"External System"` + TTL *int `json:"ttl,omitempty" example:"3600"` + ExternalID *string `json:"externalId,omitempty" example:"00000000-0000-0000-0000-000000000000"` + Metadata map[string]any `json:"metadata,omitempty"` } // @name CreateAssetRateInput // AssetRate is a struct designed to encapsulate response payload data. @@ -38,30 +43,36 @@ type CreateAssetRateInput struct { // swagger:model AssetRate // @Description AssetRate is a struct designed to store asset rate data. type AssetRate struct { - ID string `json:"id" example:"00000000-0000-0000-0000-000000000000"` - BaseAssetCode string `json:"baseAssetCode" example:"BRL"` - CounterAssetCode string `json:"counterAssetCode" example:"USD"` - Amount float64 `json:"amount" example:"5000"` - Scale float64 `json:"scale" example:"2"` - Source string `json:"source" example:"@person1"` - OrganizationID string `json:"organizationId" example:"00000000-0000-0000-0000-000000000000"` - LedgerID string `json:"ledgerId" example:"00000000-0000-0000-0000-000000000000"` - CreatedAt time.Time `json:"createdAt" example:"2021-01-01T00:00:00Z"` - Metadata map[string]any `json:"metadata"` + ID string `json:"id" example:"00000000-0000-0000-0000-000000000000"` + OrganizationID string `json:"organizationId" example:"00000000-0000-0000-0000-000000000000"` + LedgerID string `json:"ledgerId" example:"00000000-0000-0000-0000-000000000000"` + ExternalID string `json:"externalId" example:"00000000-0000-0000-0000-000000000000"` + From string `json:"from" example:"USD"` + To string `json:"to" example:"BRL"` + Rate float64 `json:"rate" example:"100"` + Scale *float64 `json:"scale" example:"2"` + Source *string `json:"source" example:"External System"` + TTL int `json:"ttl" example:"3600"` + CreatedAt time.Time `json:"createdAt" example:"2021-01-01T00:00:00Z"` + UpdatedAt time.Time `json:"updatedAt" example:"2021-01-01T00:00:00Z"` + Metadata map[string]any `json:"metadata"` } // @name AssetRate // ToEntity converts an TransactionPostgreSQLModel to entity Transaction func (a *AssetRatePostgreSQLModel) ToEntity() *AssetRate { assetRate := &AssetRate{ - ID: a.ID, - BaseAssetCode: a.BaseAssetCode, - CounterAssetCode: a.CounterAssetCode, - Amount: a.Amount, - Scale: a.Scale, - Source: a.Source, - OrganizationID: a.OrganizationID, - LedgerID: a.LedgerID, - CreatedAt: a.CreatedAt, + ID: a.ID, + OrganizationID: a.OrganizationID, + LedgerID: a.LedgerID, + ExternalID: a.ExternalID, + From: a.From, + To: a.To, + Rate: a.Rate, + Scale: &a.RateScale, + Source: a.Source, + TTL: a.TTL, + CreatedAt: a.CreatedAt, + UpdatedAt: a.UpdatedAt, } return assetRate @@ -70,14 +81,17 @@ func (a *AssetRatePostgreSQLModel) ToEntity() *AssetRate { // FromEntity converts an entity AssetRate to AssetRatePostgreSQLModel func (a *AssetRatePostgreSQLModel) FromEntity(assetRate *AssetRate) { *a = AssetRatePostgreSQLModel{ - ID: pkg.GenerateUUIDv7().String(), - BaseAssetCode: assetRate.BaseAssetCode, - CounterAssetCode: assetRate.CounterAssetCode, - Amount: assetRate.Amount, - Scale: assetRate.Scale, - Source: assetRate.Source, - OrganizationID: assetRate.OrganizationID, - LedgerID: assetRate.LedgerID, - CreatedAt: assetRate.CreatedAt, + ID: pkg.GenerateUUIDv7().String(), + OrganizationID: assetRate.OrganizationID, + LedgerID: assetRate.LedgerID, + ExternalID: assetRate.ExternalID, + From: assetRate.From, + To: assetRate.To, + Rate: assetRate.Rate, + RateScale: *assetRate.Scale, + Source: assetRate.Source, + TTL: assetRate.TTL, + CreatedAt: assetRate.CreatedAt, + UpdatedAt: assetRate.UpdatedAt, } } diff --git a/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go b/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go index 92e97538..9924b703 100644 --- a/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go +++ b/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go @@ -11,16 +11,17 @@ package assetrate import ( context "context" - gomock "go.uber.org/mock/gomock" reflect "reflect" 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. @@ -41,31 +42,61 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { } // Create mocks base method. -func (m *MockRepository) Create(arg0 context.Context, arg1 *AssetRate) (*AssetRate, error) { +func (m *MockRepository) Create(ctx context.Context, assetRate *AssetRate) (*AssetRate, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret := m.ctrl.Call(m, "Create", ctx, assetRate) ret0, _ := ret[0].(*AssetRate) 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, assetRate 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, assetRate) } // Find mocks base method. -func (m *MockRepository) Find(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) (*AssetRate, error) { +func (m *MockRepository) Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*AssetRate, 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].(*AssetRate) 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), ctx, organizationID, ledgerID, id) +} + +// FindByCurrencyPair mocks base method. +func (m *MockRepository) FindByCurrencyPair(ctx context.Context, organizationID, ledgerID uuid.UUID, from, to string) (*AssetRate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByCurrencyPair", ctx, organizationID, ledgerID, from, to) + ret0, _ := ret[0].(*AssetRate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByCurrencyPair indicates an expected call of FindByCurrencyPair. +func (mr *MockRepositoryMockRecorder) FindByCurrencyPair(ctx, organizationID, ledgerID, from, to any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByCurrencyPair", reflect.TypeOf((*MockRepository)(nil).FindByCurrencyPair), ctx, organizationID, ledgerID, from, to) +} + +// Update mocks base method. +func (m *MockRepository) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, assetRate *AssetRate) (*AssetRate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, organizationID, ledgerID, id, assetRate) + ret0, _ := ret[0].(*AssetRate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockRepositoryMockRecorder) Update(ctx, organizationID, ledgerID, id, assetRate 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, "Update", reflect.TypeOf((*MockRepository)(nil).Update), ctx, organizationID, ledgerID, id, assetRate) } diff --git a/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go b/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go index 478e2bb0..1de82ac5 100644 --- a/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go +++ b/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go @@ -5,6 +5,9 @@ import ( "database/sql" "errors" "reflect" + "strconv" + "strings" + "time" "github.com/LerianStudio/midaz/pkg" "github.com/LerianStudio/midaz/pkg/constant" @@ -20,6 +23,8 @@ import ( type Repository interface { Create(ctx context.Context, assetRate *AssetRate) (*AssetRate, error) Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*AssetRate, error) + FindByCurrencyPair(ctx context.Context, organizationID, ledgerID uuid.UUID, from, to string) (*AssetRate, error) + Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, assetRate *AssetRate) (*AssetRate, error) } // AssetRatePostgreSQLRepository is a Postgresql-specific implementation of the AssetRateRepository. @@ -69,16 +74,19 @@ func (r *AssetRatePostgreSQLRepository) Create(ctx context.Context, assetRate *A return nil, err } - result, err := db.ExecContext(ctx, `INSERT INTO asset_rate VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`, - record.ID, - record.BaseAssetCode, - record.CounterAssetCode, - record.Amount, - record.Scale, - record.Source, - record.OrganizationID, - record.LedgerID, - record.CreatedAt, + result, err := db.ExecContext(ctx, `INSERT INTO asset_rate VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`, + &record.ID, + &record.OrganizationID, + &record.LedgerID, + &record.ExternalID, + &record.From, + &record.To, + &record.Rate, + &record.RateScale, + &record.Source, + &record.TTL, + &record.CreatedAt, + &record.UpdatedAt, ) if err != nil { mopentelemetry.HandleSpanError(&spanExec, "Failed to execute insert query", err) @@ -124,20 +132,23 @@ func (r *AssetRatePostgreSQLRepository) Find(ctx context.Context, organizationID ctx, spanQuery := tracer.Start(ctx, "postgres.find.query") - row := db.QueryRowContext(ctx, `SELECT * FROM asset_rate WHERE id = $1 AND organization_id = $2 AND ledger_id = $3`, assetRateID, organizationID, ledgerID) + row := db.QueryRowContext(ctx, `SELECT * FROM asset_rate WHERE organization_id = $1 AND ledger_id = $2 AND id = $3 ORDER BY created_at DESC`, organizationID, ledgerID, assetRateID) spanQuery.End() if err := row.Scan( &record.ID, - &record.BaseAssetCode, - &record.CounterAssetCode, - &record.Amount, - &record.Scale, - &record.Source, &record.OrganizationID, &record.LedgerID, + &record.ExternalID, + &record.From, + &record.To, + &record.Rate, + &record.RateScale, + &record.Source, + &record.TTL, &record.CreatedAt, + &record.UpdatedAt, ); err != nil { mopentelemetry.HandleSpanError(&span, "Failed to scan asset rate record", err) @@ -150,3 +161,130 @@ func (r *AssetRatePostgreSQLRepository) Find(ctx context.Context, organizationID return record.ToEntity(), nil } + +// FindByCurrencyPair an AssetRate entity by its currency pair in Postgresql and returns it. +func (r *AssetRatePostgreSQLRepository) FindByCurrencyPair(ctx context.Context, organizationID, ledgerID uuid.UUID, from, to string) (*AssetRate, error) { + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "postgres.find_asset_rate") + defer span.End() + + db, err := r.connection.GetDB() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get database connection", err) + + return nil, err + } + + record := &AssetRatePostgreSQLModel{} + + ctx, spanQuery := tracer.Start(ctx, "postgres.find.query") + + row := db.QueryRowContext(ctx, `SELECT * FROM asset_rate WHERE organization_id = $1 AND ledger_id = $2 AND "from" = $3 AND "to" = $4 ORDER BY created_at DESC`, organizationID, ledgerID, from, to) + + spanQuery.End() + + if err := row.Scan( + &record.ID, + &record.OrganizationID, + &record.LedgerID, + &record.ExternalID, + &record.From, + &record.To, + &record.Rate, + &record.RateScale, + &record.Source, + &record.TTL, + &record.CreatedAt, + &record.UpdatedAt, + ); err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to scan asset rate record", err) + + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + + return nil, err + } + + return record.ToEntity(), nil +} + +// Update an AssetRate entity into Postgresql and returns the AssetRate updated. +func (r *AssetRatePostgreSQLRepository) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, assetRate *AssetRate) (*AssetRate, error) { + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "postgres.update_asset_rate") + defer span.End() + + db, err := r.connection.GetDB() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get database connection", err) + + return nil, err + } + + record := &AssetRatePostgreSQLModel{} + record.FromEntity(assetRate) + + var updates []string + + var args []any + + if !pkg.IsNilOrEmpty(assetRate.Source) { + updates = append(updates, "source = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Source) + } + + record.UpdatedAt = time.Now() + + updates = append(updates, + "updated_at = $"+strconv.Itoa(len(args)+1), + "rate = $"+strconv.Itoa(len(args)+2), + "rate_scale = $"+strconv.Itoa(len(args)+3), + "ttl = $"+strconv.Itoa(len(args)+4), + "external_id = $"+strconv.Itoa(len(args)+5), + ) + + args = append(args, record.UpdatedAt, record.Rate, record.RateScale, record.TTL, record.ExternalID, organizationID, ledgerID, id) + + query := `UPDATE asset_rate SET ` + strings.Join(updates, ", ") + + ` WHERE organization_id = $` + strconv.Itoa(len(args)-2) + + ` AND ledger_id = $` + strconv.Itoa(len(args)-1) + + ` AND id = $` + strconv.Itoa(len(args)) + + ctx, spanExec := tracer.Start(ctx, "postgres.update.exec") + + err = mopentelemetry.SetSpanAttributesFromStruct(&spanExec, "asset_rate_repository_input", record) + if err != nil { + mopentelemetry.HandleSpanError(&spanExec, "Failed to convert asset rate record from entity to JSON string", err) + + return nil, err + } + + result, err := db.ExecContext(ctx, query, args...) + if err != nil { + mopentelemetry.HandleSpanError(&spanExec, "Failed to execute query", err) + + return nil, err + } + + spanExec.End() + + rowsAffected, err := result.RowsAffected() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get rows affected", err) + + return nil, err + } + + if rowsAffected == 0 { + err := pkg.ValidateBusinessError(constant.ErrEntityNotFound, reflect.TypeOf(AssetRate{}).Name()) + + mopentelemetry.HandleSpanError(&span, "Failed to update asset rate. Rows affected is 0", err) + + return nil, err + } + + return record.ToEntity(), nil +} diff --git a/components/transaction/internal/services/command/create-assetrate.go b/components/transaction/internal/services/command/create-assetrate.go index ee2e17c6..f78d1794 100644 --- a/components/transaction/internal/services/command/create-assetrate.go +++ b/components/transaction/internal/services/command/create-assetrate.go @@ -13,40 +13,118 @@ import ( "github.com/google/uuid" ) -// CreateAssetRate creates a new asset rate and persists data in the repository. -func (uc *UseCase) CreateAssetRate(ctx context.Context, organizationID, ledgerID uuid.UUID, cari *assetrate.CreateAssetRateInput) (*assetrate.AssetRate, error) { +// CreateOrUpdateAssetRate creates or updates an asset rate. +func (uc *UseCase) CreateOrUpdateAssetRate(ctx context.Context, organizationID, ledgerID uuid.UUID, cari *assetrate.CreateAssetRateInput) (*assetrate.AssetRate, error) { logger := pkg.NewLoggerFromContext(ctx) tracer := pkg.NewTracerFromContext(ctx) - ctx, span := tracer.Start(ctx, "command.create_asset_rate") + ctx, span := tracer.Start(ctx, "command.create_or_update_asset_rate") defer span.End() - logger.Infof("Trying to create asset rate: %v", cari) + logger.Infof("Initializing the create or update asset rate operation: %v", cari) - if err := pkg.ValidateCode(cari.BaseAssetCode); err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to validate base asset code", err) + if err := pkg.ValidateCode(cari.From); err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to validate 'from' asset code", err) return nil, pkg.ValidateBusinessError(err, reflect.TypeOf(assetrate.AssetRate{}).Name()) } - if err := pkg.ValidateCode(cari.CounterAssetCode); err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to validate counter asset code", err) + if err := pkg.ValidateCode(cari.To); err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to validate 'to' asset code", err) return nil, pkg.ValidateBusinessError(err, reflect.TypeOf(assetrate.AssetRate{}).Name()) } + externalID := cari.ExternalID + emptyExternalID := pkg.IsNilOrEmpty(externalID) + + rate := float64(cari.Rate) + scale := float64(cari.Scale) + + logger.Infof("Trying to find existing asset rate by currency pair: %v", cari) + + arFound, err := uc.AssetRateRepo.FindByCurrencyPair(ctx, organizationID, ledgerID, cari.From, cari.To) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to find asset rate by currency pair", err) + + logger.Errorf("Error creating asset rate: %v", err) + + return nil, err + } + + if arFound != nil { + logger.Infof("Trying to update asset rate: %v", cari) + + arFound.Rate = rate + arFound.Scale = &scale + arFound.Source = cari.Source + arFound.TTL = *cari.TTL + arFound.UpdatedAt = time.Now() + + if !emptyExternalID { + arFound.ExternalID = *externalID + } + + arFound, err = uc.AssetRateRepo.Update(ctx, organizationID, ledgerID, uuid.MustParse(arFound.ID), arFound) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to update asset rate", err) + + logger.Errorf("Error updating asset rate: %v", err) + + return nil, err + } + + if cari.Metadata != nil { + if err := pkg.CheckMetadataKeyAndValueLength(100, cari.Metadata); err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to validate metadata", err) + + return nil, pkg.ValidateBusinessError(err, reflect.TypeOf(assetrate.AssetRate{}).Name()) + } + + meta := mongodb.Metadata{ + EntityID: arFound.ID, + EntityName: reflect.TypeOf(assetrate.AssetRate{}).Name(), + Data: cari.Metadata, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := uc.MetadataRepo.Create(ctx, reflect.TypeOf(assetrate.AssetRate{}).Name(), &meta); err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to create asset rate metadata", err) + + logger.Errorf("Error into creating asset rate metadata: %v", err) + + return nil, err + } + + arFound.Metadata = cari.Metadata + } + + return arFound, nil + } + + if emptyExternalID { + idStr := pkg.GenerateUUIDv7().String() + externalID = &idStr + } + assetRateDB := &assetrate.AssetRate{ - ID: pkg.GenerateUUIDv7().String(), - BaseAssetCode: cari.BaseAssetCode, - CounterAssetCode: cari.CounterAssetCode, - Amount: cari.Amount, - Scale: cari.Scale, - Source: cari.Source, - OrganizationID: organizationID.String(), - LedgerID: ledgerID.String(), - CreatedAt: time.Now(), + ID: pkg.GenerateUUIDv7().String(), + OrganizationID: organizationID.String(), + LedgerID: ledgerID.String(), + ExternalID: *externalID, + From: cari.From, + To: cari.To, + Rate: rate, + Scale: &scale, + Source: cari.Source, + TTL: *cari.TTL, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } + logger.Infof("Trying to create asset rate: %v", cari) + assetRate, err := uc.AssetRateRepo.Create(ctx, assetRateDB) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to create asset rate on repository", err) diff --git a/components/transaction/internal/services/command/create-assetrate_test.go b/components/transaction/internal/services/command/create-assetrate_test.go index 25e966fd..d11ea7ff 100644 --- a/components/transaction/internal/services/command/create-assetrate_test.go +++ b/components/transaction/internal/services/command/create-assetrate_test.go @@ -3,6 +3,7 @@ package command import ( "context" "errors" + "github.com/LerianStudio/midaz/pkg/mpointers" "go.uber.org/mock/gomock" "testing" @@ -12,26 +13,129 @@ import ( "github.com/stretchr/testify/assert" ) -// TestCreateAssetRateSuccess is responsible to test CreateAssetRate with success +// TestUpdateAssetRateSuccess is responsible to test TestUpdateAssetRateSuccess with success +func TestUpdateAssetRateSuccess(t *testing.T) { + id := pkg.GenerateUUIDv7() + orgID := pkg.GenerateUUIDv7() + ledgerID := pkg.GenerateUUIDv7() + exID := pkg.GenerateUUIDv7() + + assetRate := &assetrate.AssetRate{ + ID: id.String(), + OrganizationID: orgID.String(), + LedgerID: ledgerID.String(), + ExternalID: exID.String(), + From: "USD", + To: "BRL", + Rate: 100, + Scale: mpointers.Float64(2), + Source: mpointers.String("External System"), + TTL: 3600, + } + + uc := UseCase{ + AssetRateRepo: assetrate.NewMockRepository(gomock.NewController(t)), + } + + uc.AssetRateRepo.(*assetrate.MockRepository). + EXPECT(). + FindByCurrencyPair(gomock.Any(), orgID, ledgerID, assetRate.From, assetRate.To). + Return(assetRate, nil). + Times(1) + res, err := uc.AssetRateRepo.FindByCurrencyPair(context.TODO(), orgID, ledgerID, assetRate.From, assetRate.To) + if err != nil { + t.Errorf("Error finding asset rate by currency pair: %v", err) + } + + assert.Equal(t, assetRate.OrganizationID, res.OrganizationID) + assert.Equal(t, assetRate.LedgerID, res.LedgerID) + assert.Equal(t, assetRate.ExternalID, res.ExternalID) + assert.Equal(t, assetRate.From, res.From) + assert.Equal(t, assetRate.To, res.To) + assert.Equal(t, assetRate.Rate, res.Rate) + assert.Equal(t, assetRate.Scale, res.Scale) + assert.Equal(t, assetRate.Source, res.Source) + assert.Equal(t, assetRate.TTL, res.TTL) + assert.Nil(t, err) + + uc.AssetRateRepo.(*assetrate.MockRepository). + EXPECT(). + Update(gomock.Any(), orgID, ledgerID, id, assetRate). + Return(assetRate, nil). + Times(1) + res, err = uc.AssetRateRepo.Update(context.TODO(), orgID, ledgerID, id, assetRate) + if err != nil { + t.Errorf("Error creating asset rate: %v", err) + } + + assert.Equal(t, assetRate.OrganizationID, res.OrganizationID) + assert.Equal(t, assetRate.LedgerID, res.LedgerID) + assert.Equal(t, assetRate.ExternalID, res.ExternalID) + assert.Equal(t, assetRate.From, res.From) + assert.Equal(t, assetRate.To, res.To) + assert.Equal(t, assetRate.Rate, res.Rate) + assert.Equal(t, assetRate.Scale, res.Scale) + assert.Equal(t, assetRate.Source, res.Source) + assert.Equal(t, assetRate.TTL, res.TTL) + assert.Nil(t, err) +} + +// TestCreateAssetRateSuccess is responsible to test TestCreateAssetRateSuccess with success func TestCreateAssetRateSuccess(t *testing.T) { + id := pkg.GenerateUUIDv7() + orgID := pkg.GenerateUUIDv7() + ledgerID := pkg.GenerateUUIDv7() + exID := pkg.GenerateUUIDv7() + assetRate := &assetrate.AssetRate{ - ID: pkg.GenerateUUIDv7().String(), - OrganizationID: pkg.GenerateUUIDv7().String(), - LedgerID: pkg.GenerateUUIDv7().String(), + ID: id.String(), + OrganizationID: orgID.String(), + LedgerID: ledgerID.String(), + ExternalID: exID.String(), + From: "USD", + To: "BRL", + Rate: 100, + Scale: mpointers.Float64(2), + Source: mpointers.String("External System"), + TTL: 3600, } uc := UseCase{ AssetRateRepo: assetrate.NewMockRepository(gomock.NewController(t)), } + uc.AssetRateRepo.(*assetrate.MockRepository). + EXPECT(). + FindByCurrencyPair(gomock.Any(), orgID, ledgerID, assetRate.From, assetRate.To). + Return(nil, nil). + Times(1) + res, err := uc.AssetRateRepo.FindByCurrencyPair(context.TODO(), orgID, ledgerID, assetRate.From, assetRate.To) + if err != nil { + t.Errorf("Error finding asset rate by currency pair: %v", err) + } + + assert.Nil(t, res) + assert.Nil(t, err) + uc.AssetRateRepo.(*assetrate.MockRepository). EXPECT(). Create(gomock.Any(), assetRate). Return(assetRate, nil). Times(1) - res, err := uc.AssetRateRepo.Create(context.TODO(), assetRate) + res, err = uc.AssetRateRepo.Create(context.TODO(), assetRate) + if err != nil { + t.Errorf("Error creating asset rate: %v", err) + } - assert.Equal(t, assetRate, res) + assert.Equal(t, assetRate.OrganizationID, res.OrganizationID) + assert.Equal(t, assetRate.LedgerID, res.LedgerID) + assert.Equal(t, assetRate.ExternalID, res.ExternalID) + assert.Equal(t, assetRate.From, res.From) + assert.Equal(t, assetRate.To, res.To) + assert.Equal(t, assetRate.Rate, res.Rate) + assert.Equal(t, assetRate.Scale, res.Scale) + assert.Equal(t, assetRate.Source, res.Source) + assert.Equal(t, assetRate.TTL, res.TTL) assert.Nil(t, err) } diff --git a/components/transaction/migrations/000002_create_asset_rate_table.up.sql b/components/transaction/migrations/000002_create_asset_rate_table.up.sql index c01e2f7a..f26b95d0 100644 --- a/components/transaction/migrations/000002_create_asset_rate_table.up.sql +++ b/components/transaction/migrations/000002_create_asset_rate_table.up.sql @@ -1,11 +1,14 @@ CREATE TABLE IF NOT EXISTS asset_rate ( - id UUID PRIMARY KEY NOT NULL, - base_asset_code TEXT NOT NULL, - counter_asset_code TEXT NOT NULL, - amount NUMERIC NOT NULL, - scale NUMERIC NOT NULL, - source TEXT NOT NULL, - organization_id UUID NOT NULL, - ledger_id UUID NOT NULL, - created_at TIMESTAMP WITH TIME ZONE + id UUID PRIMARY KEY NOT NULL, + organization_id UUID NOT NULL, + ledger_id UUID NOT NULL, + external_id UUID NOT NULL, + "from" TEXT NOT NULL, + "to" TEXT NOT NULL, + rate BIGINT NOT NULL, + rate_scale NUMERIC NOT NULL, + source TEXT, + ttl BIGINT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE ) \ No newline at end of file diff --git a/pkg/mpointers/pointers.go b/pkg/mpointers/pointers.go index 0be07a79..7b7c1ba2 100644 --- a/pkg/mpointers/pointers.go +++ b/pkg/mpointers/pointers.go @@ -22,6 +22,11 @@ func Int64(t int64) *int64 { return &t } +// Float64 just return given t as a pointer +func Float64(t float64) *float64 { + return &t +} + // Int just return given t as a pointer func Int(t int) *int { return &t diff --git a/postman/MIDAZ.postman_collection.json b/postman/MIDAZ.postman_collection.json index b2b67218..d84e27c3 100644 --- a/postman/MIDAZ.postman_collection.json +++ b/postman/MIDAZ.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "ad9e2c69-4c89-4027-a957-da5cfb58b0a4", + "_postman_id": "b8e21fb3-53bc-4301-b920-90987c5f8ead", "name": "MIDAZ", "description": "## **How generate token to use on MIDAZ**\n\n\n\n\n\n\n\n\n\n", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", @@ -2795,12 +2795,7 @@ "listen": "test", "script": { "exec": [ - "const jsonData = JSON.parse(responseBody);\r", - "if (jsonData.hasOwnProperty('id')) {\r", - " console.log(\"asset_rate_id before: \" + pm.collectionVariables.get(\"asset_rate_id\"));\r", - " pm.collectionVariables.set(\"asset_rate_id\", jsonData.id);\r", - " console.log(\"asset_rate_id after: \" + pm.collectionVariables.get(\"asset_rate_id\"));\r", - "}" + "" ], "type": "text/javascript", "packages": {} @@ -2808,7 +2803,7 @@ } ], "request": { - "method": "POST", + "method": "PUT", "header": [ { "key": "Midaz-ID", @@ -2819,7 +2814,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"baseAssetCode\": \"BRL\",\n \"counterAssetCode\": \"BTC\",\n \"amount\": 35000000,\n \"scale\": 2,\n \"source\": \"Chainlink database\",\n \"metadata\": {\n \"updateCycle\": 300,\n \"urgent\": true\n }\n}", + "raw": "{\n \"from\": \"USD\",\n \"to\": \"BRL\",\n \"rate\": 1000,\n \"scale\": 2,\n \"source\": \"External System 2\",\n \"ttl\": 3600,\n \"externalId\": \"04089c2e-75d3-4672-add3-daa73388af84\",\n \"metadata\": {\n \"string\": \"string\",\n \"int\": 1\n }\n}", "options": { "raw": { "language": "json" @@ -2842,7 +2837,82 @@ }, "description": "This is a POST request, submitting data to an API via the request body. This request submits JSON data, and the data is reflected in the response.\n\nA successful POST request typically returns a `200 OK` or `201 Created` response code." }, - "response": [] + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "PUT", + "header": [ + { + "key": "Midaz-ID", + "value": "{{$randomUUID}}", + "type": "text", + "disabled": true + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"from\": \"USD\",\n \"to\": \"BRL\",\n \"rate\": 1000,\n \"scale\": 2,\n \"source\": \"External System 2\",\n \"ttl\": 3600,\n \"externalId\": \"04089c2e-75d3-4672-add3-daa73388af84\",\n \"metadata\": {\n \"string\": \"string\",\n \"int\": 1\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url_transaction}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/asset-rates", + "host": [ + "{{url_transaction}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "asset-rates" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Date", + "value": "Thu, 05 Dec 2024 19:42:41 GMT", + "description": "", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + }, + { + "key": "Content-Length", + "value": "425", + "description": "", + "type": "text" + }, + { + "key": "X-Correlation-Id", + "value": "3e97d0ff-6d14-42f4-8b60-b2248441d8a3", + "description": "", + "type": "text" + }, + { + "key": "Midaz-Id", + "value": "b125a033-f4d8-4309-bfdf-1300b051915b", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"id\": \"01939858-92fc-78b0-8ad5-acd0e81f3216\",\n \"organizationId\": \"01939842-ce59-720a-beac-ba57a4db8622\",\n \"ledgerId\": \"01939842-dc08-78ec-b03e-8b6200f11314\",\n \"externalId\": \"04089c2e-75d3-4672-add3-daa73388af84\",\n \"from\": \"USD\",\n \"to\": \"BRL\",\n \"rate\": 1000,\n \"scale\": 2,\n \"source\": \"External System 2\",\n \"ttl\": 3600,\n \"createdAt\": \"2024-12-05T16:16:14.617577-03:00\",\n \"updatedAt\": \"2024-12-05T16:42:41.916575251-03:00\",\n \"metadata\": {\n \"int\": 1,\n \"string\": \"string\"\n }\n}" + } + ] }, { "name": "Asset rates", From 45c326fe1269fd59942f48276df35349c42fd299 Mon Sep 17 00:00:00 2001 From: Brecci Date: Thu, 5 Dec 2024 19:45:11 -0300 Subject: [PATCH 2/4] refactor(asset-rate): update get by id endpoint to get by external id to comply with new model and rules :hammer: --- components/auth/setup/init_data.json | 31 +---- components/transaction/api/docs.go | 108 +++++++++++------- components/transaction/api/swagger.json | 108 +++++++++++------- components/transaction/api/swagger.yaml | 93 +++++++++------ .../internal/adapters/http/in/assetrate.go | 25 ++-- .../internal/adapters/http/in/routes.go | 2 +- .../postgres/assetrate/assetrate.mock.go | 24 ++-- .../assetrate/assetrate.postgresql.go | 12 +- ...etrate.go => get-external-id-assetrate.go} | 14 +-- ...t.go => get-external-id-assetrate_test.go} | 27 +++-- 10 files changed, 249 insertions(+), 195 deletions(-) rename components/transaction/internal/services/query/{get-id-assetrate.go => get-external-id-assetrate.go} (62%) rename components/transaction/internal/services/query/{get-id-assetrate_test.go => get-external-id-assetrate_test.go} (60%) diff --git a/components/auth/setup/init_data.json b/components/auth/setup/init_data.json index 13070b8e..6748a3cf 100644 --- a/components/auth/setup/init_data.json +++ b/components/auth/setup/init_data.json @@ -792,38 +792,13 @@ "portfolio", "product", "transaction", - "operation" - ], - "actions": [ - "get", - "post", - "patch" - ], - "domains": [], - "effect": "Allow", - "submitter": "admin", - "approver": "admin", - "approveTime": "2024-01-01T22:51:35+08:00", - "state": "Approved" - }, - { - "owner": "lerian", - "name": "developer-api-permission", - "displayName": "Developer API Permission", - "isEnabled": true, - "model": "api-model", - "roles": [ - "lerian/developer_role" - ], - "users": [ - "lerian/user_lisa" - ], - "resourceType": "Custom", - "resources": [ + "operation", "asset-rate" ], "actions": [ "get", + "post", + "patch", "put" ], "domains": [], diff --git a/components/transaction/api/docs.go b/components/transaction/api/docs.go index a58ead20..17d6aab3 100644 --- a/components/transaction/api/docs.go +++ b/components/transaction/api/docs.go @@ -159,7 +159,7 @@ const docTemplate = `{ }, "/v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates": { "post": { - "description": "Create an AssetRate with the input payload", + "description": "Create or Update an AssetRate with the input details", "consumes": [ "application/json" ], @@ -169,7 +169,7 @@ const docTemplate = `{ "tags": [ "Asset Rates" ], - "summary": "Create an AssetRate", + "summary": "Create or Update an AssetRate", "parameters": [ { "type": "string", @@ -218,16 +218,16 @@ const docTemplate = `{ } } }, - "/v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{asset_rate_id}": { + "/v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{external_id}": { "get": { - "description": "Get an AssetRate with the input ID", + "description": "Get an AssetRate by External ID with the input details", "produces": [ "application/json" ], "tags": [ "Asset Rates" ], - "summary": "Get an AssetRate by ID", + "summary": "Get an AssetRate by External ID", "parameters": [ { "type": "string", @@ -258,19 +258,10 @@ const docTemplate = `{ }, { "type": "string", - "description": "AssetRate ID", - "name": "asset_rate_id", + "description": "External ID", + "name": "external_id", "in": "path", "required": true - }, - { - "description": "AssetRate Input", - "name": "asset-rate", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/CreateAssetRateInput" - } } ], "responses": { @@ -803,14 +794,23 @@ const docTemplate = `{ "Amount": { "description": "Amount is the struct designed to represent the amount of an operation.", "type": "object", + "required": [ + "asset", + "value" + ], "properties": { - "amount": { - "type": "number", - "example": 1500 + "asset": { + "type": "string", + "example": "BRL" }, "scale": { - "type": "number", + "type": "integer", + "minimum": 0, "example": 2 + }, + "value": { + "type": "integer", + "example": 1000 } } }, @@ -818,21 +818,17 @@ const docTemplate = `{ "description": "AssetRate is a struct designed to store asset rate data.", "type": "object", "properties": { - "amount": { - "type": "number", - "example": 5000 - }, - "baseAssetCode": { + "createdAt": { "type": "string", - "example": "BRL" + "example": "2021-01-01T00:00:00Z" }, - "counterAssetCode": { + "externalId": { "type": "string", - "example": "USD" + "example": "00000000-0000-0000-0000-000000000000" }, - "createdAt": { + "from": { "type": "string", - "example": "2021-01-01T00:00:00Z" + "example": "USD" }, "id": { "type": "string", @@ -850,13 +846,29 @@ const docTemplate = `{ "type": "string", "example": "00000000-0000-0000-0000-000000000000" }, + "rate": { + "type": "number", + "example": 100 + }, "scale": { "type": "number", "example": 2 }, "source": { "type": "string", - "example": "@person1" + "example": "External System" + }, + "to": { + "type": "string", + "example": "BRL" + }, + "ttl": { + "type": "integer", + "example": 3600 + }, + "updatedAt": { + "type": "string", + "example": "2021-01-01T00:00:00Z" } } }, @@ -881,16 +893,17 @@ const docTemplate = `{ "CreateAssetRateInput": { "description": "CreateAssetRateInput is the input payload to create an asset rate.", "type": "object", + "required": [ + "from", + "rate", + "to" + ], "properties": { - "amount": { - "type": "number", - "example": 5000 - }, - "baseAssetCode": { + "externalId": { "type": "string", - "example": "BRL" + "example": "00000000-0000-0000-0000-000000000000" }, - "counterAssetCode": { + "from": { "type": "string", "example": "USD" }, @@ -898,13 +911,26 @@ const docTemplate = `{ "type": "object", "additionalProperties": {} }, + "rate": { + "type": "integer", + "example": 100 + }, "scale": { - "type": "number", + "type": "integer", + "minimum": 0, "example": 2 }, "source": { "type": "string", - "example": "@person1" + "example": "External System" + }, + "to": { + "type": "string", + "example": "BRL" + }, + "ttl": { + "type": "integer", + "example": 3600 } } }, @@ -1155,7 +1181,7 @@ const docTemplate = `{ } }, "Status": { - "description": "Status is the struct designed to represent the status of an operation.", + "description": "Status is the struct designed to represent the status of a transaction.", "type": "object", "properties": { "code": { diff --git a/components/transaction/api/swagger.json b/components/transaction/api/swagger.json index 016f876e..b63baa55 100644 --- a/components/transaction/api/swagger.json +++ b/components/transaction/api/swagger.json @@ -153,7 +153,7 @@ }, "/v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates": { "post": { - "description": "Create an AssetRate with the input payload", + "description": "Create or Update an AssetRate with the input details", "consumes": [ "application/json" ], @@ -163,7 +163,7 @@ "tags": [ "Asset Rates" ], - "summary": "Create an AssetRate", + "summary": "Create or Update an AssetRate", "parameters": [ { "type": "string", @@ -212,16 +212,16 @@ } } }, - "/v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{asset_rate_id}": { + "/v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{external_id}": { "get": { - "description": "Get an AssetRate with the input ID", + "description": "Get an AssetRate by External ID with the input details", "produces": [ "application/json" ], "tags": [ "Asset Rates" ], - "summary": "Get an AssetRate by ID", + "summary": "Get an AssetRate by External ID", "parameters": [ { "type": "string", @@ -252,19 +252,10 @@ }, { "type": "string", - "description": "AssetRate ID", - "name": "asset_rate_id", + "description": "External ID", + "name": "external_id", "in": "path", "required": true - }, - { - "description": "AssetRate Input", - "name": "asset-rate", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/CreateAssetRateInput" - } } ], "responses": { @@ -797,14 +788,23 @@ "Amount": { "description": "Amount is the struct designed to represent the amount of an operation.", "type": "object", + "required": [ + "asset", + "value" + ], "properties": { - "amount": { - "type": "number", - "example": 1500 + "asset": { + "type": "string", + "example": "BRL" }, "scale": { - "type": "number", + "type": "integer", + "minimum": 0, "example": 2 + }, + "value": { + "type": "integer", + "example": 1000 } } }, @@ -812,21 +812,17 @@ "description": "AssetRate is a struct designed to store asset rate data.", "type": "object", "properties": { - "amount": { - "type": "number", - "example": 5000 - }, - "baseAssetCode": { + "createdAt": { "type": "string", - "example": "BRL" + "example": "2021-01-01T00:00:00Z" }, - "counterAssetCode": { + "externalId": { "type": "string", - "example": "USD" + "example": "00000000-0000-0000-0000-000000000000" }, - "createdAt": { + "from": { "type": "string", - "example": "2021-01-01T00:00:00Z" + "example": "USD" }, "id": { "type": "string", @@ -844,13 +840,29 @@ "type": "string", "example": "00000000-0000-0000-0000-000000000000" }, + "rate": { + "type": "number", + "example": 100 + }, "scale": { "type": "number", "example": 2 }, "source": { "type": "string", - "example": "@person1" + "example": "External System" + }, + "to": { + "type": "string", + "example": "BRL" + }, + "ttl": { + "type": "integer", + "example": 3600 + }, + "updatedAt": { + "type": "string", + "example": "2021-01-01T00:00:00Z" } } }, @@ -875,16 +887,17 @@ "CreateAssetRateInput": { "description": "CreateAssetRateInput is the input payload to create an asset rate.", "type": "object", + "required": [ + "from", + "rate", + "to" + ], "properties": { - "amount": { - "type": "number", - "example": 5000 - }, - "baseAssetCode": { + "externalId": { "type": "string", - "example": "BRL" + "example": "00000000-0000-0000-0000-000000000000" }, - "counterAssetCode": { + "from": { "type": "string", "example": "USD" }, @@ -892,13 +905,26 @@ "type": "object", "additionalProperties": {} }, + "rate": { + "type": "integer", + "example": 100 + }, "scale": { - "type": "number", + "type": "integer", + "minimum": 0, "example": 2 }, "source": { "type": "string", - "example": "@person1" + "example": "External System" + }, + "to": { + "type": "string", + "example": "BRL" + }, + "ttl": { + "type": "integer", + "example": 3600 } } }, @@ -1149,7 +1175,7 @@ } }, "Status": { - "description": "Status is the struct designed to represent the status of an operation.", + "description": "Status is the struct designed to represent the status of a transaction.", "type": "object", "properties": { "code": { diff --git a/components/transaction/api/swagger.yaml b/components/transaction/api/swagger.yaml index 5149d9e9..ca7845e1 100644 --- a/components/transaction/api/swagger.yaml +++ b/components/transaction/api/swagger.yaml @@ -3,28 +3,32 @@ definitions: Amount: description: Amount is the struct designed to represent the amount of an operation. properties: - amount: - example: 1500 - type: number + asset: + example: BRL + type: string scale: example: 2 - type: number + minimum: 0 + type: integer + value: + example: 1000 + type: integer + required: + - asset + - value type: object AssetRate: description: AssetRate is a struct designed to store asset rate data. properties: - amount: - example: 5000 - type: number - baseAssetCode: - example: BRL - type: string - counterAssetCode: - example: USD - type: string createdAt: example: "2021-01-01T00:00:00Z" type: string + externalId: + example: 00000000-0000-0000-0000-000000000000 + type: string + from: + example: USD + type: string id: example: 00000000-0000-0000-0000-000000000000 type: string @@ -37,11 +41,23 @@ definitions: organizationId: example: 00000000-0000-0000-0000-000000000000 type: string + rate: + example: 100 + type: number scale: example: 2 type: number source: - example: '@person1' + example: External System + type: string + to: + example: BRL + type: string + ttl: + example: 3600 + type: integer + updatedAt: + example: "2021-01-01T00:00:00Z" type: string type: object Balance: @@ -60,24 +76,35 @@ definitions: CreateAssetRateInput: description: CreateAssetRateInput is the input payload to create an asset rate. properties: - amount: - example: 5000 - type: number - baseAssetCode: - example: BRL + externalId: + example: 00000000-0000-0000-0000-000000000000 type: string - counterAssetCode: + from: example: USD type: string metadata: additionalProperties: {} type: object + rate: + example: 100 + type: integer scale: example: 2 - type: number + minimum: 0 + type: integer source: - example: '@person1' + example: External System + type: string + to: + example: BRL type: string + ttl: + example: 3600 + type: integer + required: + - from + - rate + - to type: object CreateTransactionInput: description: CreateTransactionInput is the input payload to create a transaction. @@ -262,7 +289,7 @@ definitions: - from type: object Status: - description: Status is the struct designed to represent the status of an operation. + description: Status is the struct designed to represent the status of a transaction. properties: code: example: ACTIVE @@ -464,7 +491,7 @@ paths: post: consumes: - application/json - description: Create an AssetRate with the input payload + description: Create or Update an AssetRate with the input details parameters: - description: Authorization Bearer Token in: header @@ -498,12 +525,12 @@ paths: description: OK schema: $ref: '#/definitions/AssetRate' - summary: Create an AssetRate + summary: Create or Update an AssetRate tags: - Asset Rates - /v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{asset_rate_id}: + /v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{external_id}: get: - description: Get an AssetRate with the input ID + description: Get an AssetRate by External ID with the input details parameters: - description: Authorization Bearer Token in: header @@ -524,17 +551,11 @@ paths: name: ledger_id required: true type: string - - description: AssetRate ID + - description: External ID in: path - name: asset_rate_id + name: external_id required: true type: string - - description: AssetRate Input - in: body - name: asset-rate - required: true - schema: - $ref: '#/definitions/CreateAssetRateInput' produces: - application/json responses: @@ -542,7 +563,7 @@ paths: description: OK schema: $ref: '#/definitions/AssetRate' - summary: Get an AssetRate by ID + summary: Get an AssetRate by External ID tags: - Asset Rates /v1/organizations/{organization_id}/ledgers/{ledger_id}/portfolios/{portfolio_id}/operations: diff --git a/components/transaction/internal/adapters/http/in/assetrate.go b/components/transaction/internal/adapters/http/in/assetrate.go index 0ce469ad..29f5b9b0 100644 --- a/components/transaction/internal/adapters/http/in/assetrate.go +++ b/components/transaction/internal/adapters/http/in/assetrate.go @@ -71,39 +71,36 @@ func (handler *AssetRateHandler) CreateOrUpdateAssetRate(p any, c *fiber.Ctx) er return http.Created(c, assetRate) } -// GetAssetRate retrieves an asset rate. +// GetAssetRateByExternalID retrieves an asset rate. // -// @Summary Get an AssetRate by ID -// @Description Get an AssetRate with the input ID +// @Summary Get an AssetRate by External ID +// @Description Get an AssetRate by External ID with the input details // @Tags Asset Rates // @Produce json // @Param Authorization header string true "Authorization Bearer Token" // @Param Midaz-Id header string false "Request ID" // @Param organization_id path string true "Organization ID" // @Param ledger_id path string true "Ledger ID" -// @Param asset_rate_id path string true "AssetRate ID" -// @Param asset-rate body assetrate.CreateAssetRateInput true "AssetRate Input" +// @Param external_id path string true "External ID" // @Success 200 {object} assetrate.AssetRate -// @Router /v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{asset_rate_id} [get] -func (handler *AssetRateHandler) GetAssetRate(c *fiber.Ctx) error { +// @Router /v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{external_id} [get] +func (handler *AssetRateHandler) GetAssetRateByExternalID(c *fiber.Ctx) error { ctx := c.UserContext() logger := pkg.NewLoggerFromContext(ctx) tracer := pkg.NewTracerFromContext(ctx) - ctx, span := tracer.Start(ctx, "handler.get_asset_rate") + ctx, span := tracer.Start(ctx, "handler.get_asset_rate_by_external_id") defer span.End() organizationID := c.Locals("organization_id").(uuid.UUID) - logger.Infof("Initiating get of AssetRate with organization ID: %s", organizationID.String()) - ledgerID := c.Locals("ledger_id").(uuid.UUID) - logger.Infof("Initiating get of AssetRate with ledger ID: %s", ledgerID.String()) + externalID := c.Locals("external_id").(uuid.UUID) - assetRateID := c.Locals("asset_rate_id").(uuid.UUID) - logger.Infof("Initiating get of AssetRate with asset rate ID: %s", assetRateID.String()) + logger.Infof("Initiating get of AssetRate with organization ID '%s', ledger ID: '%s', and external ID: '%s'", + organizationID.String(), ledgerID.String(), externalID.String()) - assetRate, err := handler.Query.GetAssetRateByID(ctx, organizationID, ledgerID, assetRateID) + assetRate, err := handler.Query.GetAssetRateByExternalID(ctx, organizationID, ledgerID, externalID) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to get AssetRate on query", err) diff --git a/components/transaction/internal/adapters/http/in/routes.go b/components/transaction/internal/adapters/http/in/routes.go index acf123b3..47556124 100644 --- a/components/transaction/internal/adapters/http/in/routes.go +++ b/components/transaction/internal/adapters/http/in/routes.go @@ -51,7 +51,7 @@ func NewRouter(lg mlog.Logger, tl *mopentelemetry.Telemetry, cc *mcasdoor.Casdoo // Asset-rate f.Put("/v1/organizations/:organization_id/ledgers/:ledger_id/asset-rates", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("asset-rate"), http.ParseUUIDPathParameters, http.WithBody(new(assetrate.CreateAssetRateInput), ah.CreateOrUpdateAssetRate)) - f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/asset-rates/:asset_rate_id", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("asset-rate"), http.ParseUUIDPathParameters, ah.GetAssetRate) + f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/asset-rates/:external_id", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("asset-rate"), http.ParseUUIDPathParameters, ah.GetAssetRateByExternalID) // Health f.Get("/health", http.Ping) diff --git a/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go b/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go index 9924b703..45d93ace 100644 --- a/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go +++ b/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go @@ -56,34 +56,34 @@ func (mr *MockRepositoryMockRecorder) Create(ctx, assetRate any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), ctx, assetRate) } -// Find mocks base method. -func (m *MockRepository) Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*AssetRate, error) { +// FindByCurrencyPair mocks base method. +func (m *MockRepository) FindByCurrencyPair(ctx context.Context, organizationID, ledgerID uuid.UUID, from, to string) (*AssetRate, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Find", ctx, organizationID, ledgerID, id) + ret := m.ctrl.Call(m, "FindByCurrencyPair", ctx, organizationID, ledgerID, from, to) ret0, _ := ret[0].(*AssetRate) ret1, _ := ret[1].(error) return ret0, ret1 } -// Find indicates an expected call of Find. -func (mr *MockRepositoryMockRecorder) Find(ctx, organizationID, ledgerID, id any) *gomock.Call { +// FindByCurrencyPair indicates an expected call of FindByCurrencyPair. +func (mr *MockRepositoryMockRecorder) FindByCurrencyPair(ctx, organizationID, ledgerID, from, to any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockRepository)(nil).Find), ctx, organizationID, ledgerID, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByCurrencyPair", reflect.TypeOf((*MockRepository)(nil).FindByCurrencyPair), ctx, organizationID, ledgerID, from, to) } -// FindByCurrencyPair mocks base method. -func (m *MockRepository) FindByCurrencyPair(ctx context.Context, organizationID, ledgerID uuid.UUID, from, to string) (*AssetRate, error) { +// FindByExternalID mocks base method. +func (m *MockRepository) FindByExternalID(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*AssetRate, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindByCurrencyPair", ctx, organizationID, ledgerID, from, to) + ret := m.ctrl.Call(m, "FindByExternalID", ctx, organizationID, ledgerID, id) ret0, _ := ret[0].(*AssetRate) ret1, _ := ret[1].(error) return ret0, ret1 } -// FindByCurrencyPair indicates an expected call of FindByCurrencyPair. -func (mr *MockRepositoryMockRecorder) FindByCurrencyPair(ctx, organizationID, ledgerID, from, to any) *gomock.Call { +// FindByExternalID indicates an expected call of FindByExternalID. +func (mr *MockRepositoryMockRecorder) FindByExternalID(ctx, organizationID, ledgerID, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByCurrencyPair", reflect.TypeOf((*MockRepository)(nil).FindByCurrencyPair), ctx, organizationID, ledgerID, from, to) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByExternalID", reflect.TypeOf((*MockRepository)(nil).FindByExternalID), ctx, organizationID, ledgerID, id) } // Update mocks base method. diff --git a/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go b/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go index 1de82ac5..05bd340e 100644 --- a/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go +++ b/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go @@ -22,8 +22,8 @@ import ( //go:generate mockgen --destination=assetrate.mock.go --package=assetrate . Repository type Repository interface { Create(ctx context.Context, assetRate *AssetRate) (*AssetRate, error) - Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*AssetRate, error) FindByCurrencyPair(ctx context.Context, organizationID, ledgerID uuid.UUID, from, to string) (*AssetRate, error) + FindByExternalID(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*AssetRate, error) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, assetRate *AssetRate) (*AssetRate, error) } @@ -114,11 +114,11 @@ func (r *AssetRatePostgreSQLRepository) Create(ctx context.Context, assetRate *A return record.ToEntity(), nil } -// Find an AssetRate entity by its ID in Postgresql and returns it. -func (r *AssetRatePostgreSQLRepository) Find(ctx context.Context, organizationID, ledgerID, assetRateID uuid.UUID) (*AssetRate, error) { +// FindByExternalID an AssetRate entity by its external ID in Postgresql and returns it. +func (r *AssetRatePostgreSQLRepository) FindByExternalID(ctx context.Context, organizationID, ledgerID, externalID uuid.UUID) (*AssetRate, error) { tracer := pkg.NewTracerFromContext(ctx) - ctx, span := tracer.Start(ctx, "postgres.find_asset_rate") + ctx, span := tracer.Start(ctx, "postgres.find_asset_rate_by_external_id") defer span.End() db, err := r.connection.GetDB() @@ -132,7 +132,7 @@ func (r *AssetRatePostgreSQLRepository) Find(ctx context.Context, organizationID ctx, spanQuery := tracer.Start(ctx, "postgres.find.query") - row := db.QueryRowContext(ctx, `SELECT * FROM asset_rate WHERE organization_id = $1 AND ledger_id = $2 AND id = $3 ORDER BY created_at DESC`, organizationID, ledgerID, assetRateID) + row := db.QueryRowContext(ctx, `SELECT * FROM asset_rate WHERE organization_id = $1 AND ledger_id = $2 AND external_id = $3 ORDER BY created_at DESC`, organizationID, ledgerID, externalID) spanQuery.End() @@ -166,7 +166,7 @@ func (r *AssetRatePostgreSQLRepository) Find(ctx context.Context, organizationID func (r *AssetRatePostgreSQLRepository) FindByCurrencyPair(ctx context.Context, organizationID, ledgerID uuid.UUID, from, to string) (*AssetRate, error) { tracer := pkg.NewTracerFromContext(ctx) - ctx, span := tracer.Start(ctx, "postgres.find_asset_rate") + ctx, span := tracer.Start(ctx, "postgres.find_asset_rate_by_currency_pair") defer span.End() db, err := r.connection.GetDB() diff --git a/components/transaction/internal/services/query/get-id-assetrate.go b/components/transaction/internal/services/query/get-external-id-assetrate.go similarity index 62% rename from components/transaction/internal/services/query/get-id-assetrate.go rename to components/transaction/internal/services/query/get-external-id-assetrate.go index cf8615e7..ef4e3a05 100644 --- a/components/transaction/internal/services/query/get-id-assetrate.go +++ b/components/transaction/internal/services/query/get-external-id-assetrate.go @@ -11,19 +11,19 @@ import ( "github.com/google/uuid" ) -// GetAssetRateByID gets data in the repository. -func (uc *UseCase) GetAssetRateByID(ctx context.Context, organizationID, ledgerID, assetRateID uuid.UUID) (*assetrate.AssetRate, error) { +// GetAssetRateByExternalID gets data in the repository. +func (uc *UseCase) GetAssetRateByExternalID(ctx context.Context, organizationID, ledgerID, externalID uuid.UUID) (*assetrate.AssetRate, error) { logger := pkg.NewLoggerFromContext(ctx) tracer := pkg.NewTracerFromContext(ctx) - ctx, span := tracer.Start(ctx, "query.get_asset_rate_by_id") + ctx, span := tracer.Start(ctx, "query.get_asset_rate_by_external_id") defer span.End() - logger.Infof("Trying to get asset rate") + logger.Infof("Trying to get asset rate by external id: %s", externalID.String()) - assetRate, err := uc.AssetRateRepo.Find(ctx, organizationID, ledgerID, assetRateID) + assetRate, err := uc.AssetRateRepo.FindByExternalID(ctx, organizationID, ledgerID, externalID) if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to get asset rate on repository", err) + mopentelemetry.HandleSpanError(&span, "Failed to get asset rate by external id on repository", err) logger.Errorf("Error getting asset rate: %v", err) @@ -31,7 +31,7 @@ func (uc *UseCase) GetAssetRateByID(ctx context.Context, organizationID, ledgerI } if assetRate != nil { - metadata, err := uc.MetadataRepo.FindByEntity(ctx, reflect.TypeOf(assetrate.AssetRate{}).Name(), assetRateID.String()) + metadata, err := uc.MetadataRepo.FindByEntity(ctx, reflect.TypeOf(assetrate.AssetRate{}).Name(), assetRate.ID) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to get metadata on mongodb asset rate", err) diff --git a/components/transaction/internal/services/query/get-id-assetrate_test.go b/components/transaction/internal/services/query/get-external-id-assetrate_test.go similarity index 60% rename from components/transaction/internal/services/query/get-id-assetrate_test.go rename to components/transaction/internal/services/query/get-external-id-assetrate_test.go index 930649e4..e4a432f0 100644 --- a/components/transaction/internal/services/query/get-id-assetrate_test.go +++ b/components/transaction/internal/services/query/get-external-id-assetrate_test.go @@ -3,6 +3,7 @@ package query import ( "context" "errors" + "github.com/LerianStudio/midaz/pkg/mpointers" "go.uber.org/mock/gomock" "testing" @@ -13,14 +14,22 @@ import ( ) func TestGetAssetRateByID(t *testing.T) { - ID := pkg.GenerateUUIDv7() - organizationID := pkg.GenerateUUIDv7() + id := pkg.GenerateUUIDv7() + orgID := pkg.GenerateUUIDv7() ledgerID := pkg.GenerateUUIDv7() + exID := pkg.GenerateUUIDv7() assetRate := &assetrate.AssetRate{ - ID: ID.String(), - OrganizationID: organizationID.String(), + ID: id.String(), + OrganizationID: orgID.String(), LedgerID: ledgerID.String(), + ExternalID: exID.String(), + From: "USD", + To: "BRL", + Rate: 100, + Scale: mpointers.Float64(2), + Source: mpointers.String("External System"), + TTL: 3600, } uc := UseCase{ @@ -29,16 +38,16 @@ func TestGetAssetRateByID(t *testing.T) { uc.AssetRateRepo.(*assetrate.MockRepository). EXPECT(). - Find(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + FindByExternalID(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(assetRate, nil). Times(1) - res, err := uc.AssetRateRepo.Find(context.TODO(), organizationID, ledgerID, ID) + res, err := uc.AssetRateRepo.FindByExternalID(context.TODO(), orgID, ledgerID, exID) assert.Equal(t, assetRate, res) assert.Nil(t, err) } -// TestGetAssetRateByIDError is responsible to test GetAssetRateByID with error +// TestGetAssetRateByIDError is responsible to test GetAssetRateByExternalID with error func TestGetAssetRateByIDError(t *testing.T) { id := pkg.GenerateUUIDv7() organizationID := pkg.GenerateUUIDv7() @@ -51,10 +60,10 @@ func TestGetAssetRateByIDError(t *testing.T) { uc.AssetRateRepo.(*assetrate.MockRepository). EXPECT(). - Find(gomock.Any(), organizationID, ledgerID, id). + FindByExternalID(gomock.Any(), organizationID, ledgerID, id). Return(nil, errors.New(errMSG)). Times(1) - res, err := uc.AssetRateRepo.Find(context.TODO(), organizationID, ledgerID, id) + res, err := uc.AssetRateRepo.FindByExternalID(context.TODO(), organizationID, ledgerID, id) assert.NotEmpty(t, err) assert.Equal(t, err.Error(), errMSG) From 37b16d378b4bbb909bfe419a9f83ebc13724e93c Mon Sep 17 00:00:00 2001 From: Brecci Date: Thu, 5 Dec 2024 22:53:04 -0300 Subject: [PATCH 3/4] refactor(asset-rate): create endpoint to get all asset rates by asset codes to comply with new model and rules :hammer: --- components/auth/setup/00_init.sql | 8 + .../services/query/get-all-organizations.go | 10 + components/transaction/api/docs.go | 82 ++++++++ components/transaction/api/swagger.json | 82 ++++++++ components/transaction/api/swagger.yaml | 53 +++++ .../internal/adapters/http/in/assetrate.go | 69 ++++++- .../internal/adapters/http/in/routes.go | 1 + .../postgres/assetrate/assetrate.mock.go | 15 ++ .../assetrate/assetrate.postgresql.go | 81 ++++++++ .../query/get-all-assetrates-assetcode.go | 58 ++++++ .../get-all-assetrates-assetcode_test.go | 78 +++++++ pkg/constant/http.go | 15 ++ pkg/net/http/httputils.go | 26 ++- pkg/net/http/withBody.go | 13 +- postman/MIDAZ.postman_collection.json | 193 +++++++++++++++++- 15 files changed, 763 insertions(+), 21 deletions(-) create mode 100644 components/transaction/internal/services/query/get-all-assetrates-assetcode.go create mode 100644 components/transaction/internal/services/query/get-all-assetrates-assetcode_test.go create mode 100644 pkg/constant/http.go diff --git a/components/auth/setup/00_init.sql b/components/auth/setup/00_init.sql index 7df6aeeb..72f6b923 100644 --- a/components/auth/setup/00_init.sql +++ b/components/auth/setup/00_init.sql @@ -34,27 +34,35 @@ INSERT INTO "casbin_lerian_enforcer_rule" ("ptype", "v0", "v1", "v2", "v3", "v4" ('p', 'developer_role', 'organization', 'post', '', '', ''), ('p', 'developer_role', 'organization', 'get', '', '', ''), ('p', 'developer_role', 'organization', 'patch', '', '', ''), +('p', 'developer_role', 'organization', 'put', '', '', ''), ('p', 'developer_role', 'ledger', 'post', '', '', ''), ('p', 'developer_role', 'ledger', 'get', '', '', ''), ('p', 'developer_role', 'ledger', 'patch', '', '', ''), +('p', 'developer_role', 'ledger', 'put', '', '', ''), ('p', 'developer_role', 'asset', 'post', '', '', ''), ('p', 'developer_role', 'asset', 'get', '', '', ''), ('p', 'developer_role', 'asset', 'patch', '', '', ''), +('p', 'developer_role', 'asset', 'put', '', '', ''), ('p', 'developer_role', 'portfolio', 'post', '', '', ''), ('p', 'developer_role', 'portfolio', 'get', '', '', ''), ('p', 'developer_role', 'portfolio', 'patch', '', '', ''), +('p', 'developer_role', 'portfolio', 'put', '', '', ''), ('p', 'developer_role', 'product', 'post', '', '', ''), ('p', 'developer_role', 'product', 'get', '', '', ''), ('p', 'developer_role', 'product', 'patch', '', '', ''), +('p', 'developer_role', 'product', 'put', '', '', ''), ('p', 'developer_role', 'account', 'post', '', '', ''), ('p', 'developer_role', 'account', 'get', '', '', ''), ('p', 'developer_role', 'account', 'patch', '', '', ''), +('p', 'developer_role', 'account', 'put', '', '', ''), ('p', 'developer_role', 'transaction', 'post', '', '', ''), ('p', 'developer_role', 'transaction', 'get', '', '', ''), ('p', 'developer_role', 'transaction', 'patch', '', '', ''), +('p', 'developer_role', 'transaction', 'put', '', '', ''), ('p', 'developer_role', 'operation', 'post', '', '', ''), ('p', 'developer_role', 'operation', 'get', '', '', ''), ('p', 'developer_role', 'operation', 'patch', '', '', ''), +('p', 'developer_role', 'operation', 'put', '', '', ''), ('p', 'developer_role', 'asset-rate', 'put', '', '', ''), ('p', 'developer_role', 'asset-rate', 'get', '', '', ''), ('p', 'user_role', 'organization', 'get', '', '', ''), diff --git a/components/ledger/internal/services/query/get-all-organizations.go b/components/ledger/internal/services/query/get-all-organizations.go index 394231aa..768168dd 100644 --- a/components/ledger/internal/services/query/get-all-organizations.go +++ b/components/ledger/internal/services/query/get-all-organizations.go @@ -3,6 +3,7 @@ package query import ( "context" "errors" + "github.com/LerianStudio/midaz/pkg/mopentelemetry" "reflect" "github.com/LerianStudio/midaz/components/ledger/internal/services" @@ -15,10 +16,17 @@ import ( // GetAllOrganizations fetch all Organizations from the repository func (uc *UseCase) GetAllOrganizations(ctx context.Context, filter http.QueryHeader) ([]*mmodel.Organization, error) { logger := pkg.NewLoggerFromContext(ctx) + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "query.get_all_organizations") + defer span.End() + logger.Infof("Retrieving organizations") organizations, err := uc.OrganizationRepo.FindAll(ctx, filter.Limit, filter.Page) if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get organizations on repo", err) + logger.Errorf("Error getting organizations on repo: %v", err) if errors.Is(err, services.ErrDatabaseItemNotFound) { @@ -31,6 +39,8 @@ func (uc *UseCase) GetAllOrganizations(ctx context.Context, filter http.QueryHea if organizations != nil { metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(mmodel.Organization{}).Name(), filter) if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get metadata on repo", err) + return nil, pkg.ValidateBusinessError(constant.ErrNoOrganizationsFound, reflect.TypeOf(mmodel.Organization{}).Name()) } diff --git a/components/transaction/api/docs.go b/components/transaction/api/docs.go index 17d6aab3..8f44ade9 100644 --- a/components/transaction/api/docs.go +++ b/components/transaction/api/docs.go @@ -218,6 +218,88 @@ const docTemplate = `{ } } }, + "/v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/from/{asset_code}": { + "get": { + "description": "Get an AssetRate by the Asset Code with the input details", + "produces": [ + "application/json" + ], + "tags": [ + "Asset Rates" + ], + "summary": "Get an AssetRate by the Asset Code", + "parameters": [ + { + "type": "string", + "description": "Authorization Bearer Token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Request ID", + "name": "Midaz-Id", + "in": "header" + }, + { + "type": "string", + "description": "Organization ID", + "name": "organization_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Ledger ID", + "name": "ledger_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "From Asset Code", + "name": "asset_code", + "in": "path", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "example": "\"BRL,USD,SGD\"", + "description": "To Asset Codes", + "name": "to", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Pagination" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/AssetRate" + } + } + } + } + ] + } + } + } + } + }, "/v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{external_id}": { "get": { "description": "Get an AssetRate by External ID with the input details", diff --git a/components/transaction/api/swagger.json b/components/transaction/api/swagger.json index b63baa55..3bd717b2 100644 --- a/components/transaction/api/swagger.json +++ b/components/transaction/api/swagger.json @@ -212,6 +212,88 @@ } } }, + "/v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/from/{asset_code}": { + "get": { + "description": "Get an AssetRate by the Asset Code with the input details", + "produces": [ + "application/json" + ], + "tags": [ + "Asset Rates" + ], + "summary": "Get an AssetRate by the Asset Code", + "parameters": [ + { + "type": "string", + "description": "Authorization Bearer Token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Request ID", + "name": "Midaz-Id", + "in": "header" + }, + { + "type": "string", + "description": "Organization ID", + "name": "organization_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Ledger ID", + "name": "ledger_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "From Asset Code", + "name": "asset_code", + "in": "path", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "example": "\"BRL,USD,SGD\"", + "description": "To Asset Codes", + "name": "to", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/Pagination" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/AssetRate" + } + } + } + } + ] + } + } + } + } + }, "/v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{external_id}": { "get": { "description": "Get an AssetRate by External ID with the input details", diff --git a/components/transaction/api/swagger.yaml b/components/transaction/api/swagger.yaml index ca7845e1..6ee8119a 100644 --- a/components/transaction/api/swagger.yaml +++ b/components/transaction/api/swagger.yaml @@ -566,6 +566,59 @@ paths: summary: Get an AssetRate by External ID tags: - Asset Rates + /v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/from/{asset_code}: + get: + description: Get an AssetRate by the Asset Code with the input details + parameters: + - description: Authorization Bearer Token + in: header + name: Authorization + required: true + type: string + - description: Request ID + in: header + name: Midaz-Id + type: string + - description: Organization ID + in: path + name: organization_id + required: true + type: string + - description: Ledger ID + in: path + name: ledger_id + required: true + type: string + - description: From Asset Code + in: path + name: asset_code + required: true + type: string + - collectionFormat: csv + description: To Asset Codes + example: '"BRL,USD,SGD"' + in: query + items: + type: string + name: to + type: array + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/Pagination' + - properties: + items: + items: + $ref: '#/definitions/AssetRate' + type: array + type: object + summary: Get an AssetRate by the Asset Code + tags: + - Asset Rates /v1/organizations/{organization_id}/ledgers/{ledger_id}/portfolios/{portfolio_id}/operations: get: description: Get all Operations with the input ID diff --git a/components/transaction/internal/adapters/http/in/assetrate.go b/components/transaction/internal/adapters/http/in/assetrate.go index 29f5b9b0..b021a23c 100644 --- a/components/transaction/internal/adapters/http/in/assetrate.go +++ b/components/transaction/internal/adapters/http/in/assetrate.go @@ -6,7 +6,9 @@ import ( "github.com/LerianStudio/midaz/components/transaction/internal/services/query" "github.com/LerianStudio/midaz/pkg" "github.com/LerianStudio/midaz/pkg/mopentelemetry" + "github.com/LerianStudio/midaz/pkg/mpostgres" "github.com/LerianStudio/midaz/pkg/net/http" + "go.mongodb.org/mongo-driver/bson" "github.com/gofiber/fiber/v2" "github.com/google/uuid" @@ -77,11 +79,11 @@ func (handler *AssetRateHandler) CreateOrUpdateAssetRate(p any, c *fiber.Ctx) er // @Description Get an AssetRate by External ID with the input details // @Tags Asset Rates // @Produce json -// @Param Authorization header string true "Authorization Bearer Token" -// @Param Midaz-Id header string false "Request ID" -// @Param organization_id path string true "Organization ID" -// @Param ledger_id path string true "Ledger ID" -// @Param external_id path string true "External ID" +// @Param Authorization header string true "Authorization Bearer Token" +// @Param Midaz-Id header string false "Request ID" +// @Param organization_id path string true "Organization ID" +// @Param ledger_id path string true "Ledger ID" +// @Param external_id path string true "External ID" // @Success 200 {object} assetrate.AssetRate // @Router /v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{external_id} [get] func (handler *AssetRateHandler) GetAssetRateByExternalID(c *fiber.Ctx) error { @@ -113,3 +115,60 @@ func (handler *AssetRateHandler) GetAssetRateByExternalID(c *fiber.Ctx) error { return http.OK(c, assetRate) } + +// GetAllAssetRatesByAssetCode retrieves an asset rate. +// +// @Summary Get an AssetRate by the Asset Code +// @Description Get an AssetRate by the Asset Code with the input details +// @Tags Asset Rates +// @Produce json +// @Param Authorization header string true "Authorization Bearer Token" +// @Param Midaz-Id header string false "Request ID" +// @Param organization_id path string true "Organization ID" +// @Param ledger_id path string true "Ledger ID" +// @Param asset_code path string true "From Asset Code" +// +// @Param to query []string false "To Asset Codes" example("BRL,USD,SGD") +// +// @Success 200 {object} mpostgres.Pagination{items=[]assetrate.AssetRate} +// @Router /v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/from/{asset_code} [get] +func (handler *AssetRateHandler) GetAllAssetRatesByAssetCode(c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := pkg.NewLoggerFromContext(ctx) + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "handler.get_asset_rate_by_asset_code") + defer span.End() + + headerParams := http.ValidateParameters(c.Queries()) + + pagination := mpostgres.Pagination{ + Limit: headerParams.Limit, + Page: headerParams.Page, + } + + organizationID := c.Locals("organization_id").(uuid.UUID) + ledgerID := c.Locals("ledger_id").(uuid.UUID) + assetCode := c.Locals("asset_code").(string) + + logger.Infof("Initiating get of AssetRate with organization ID '%s', ledger ID: '%s', and asset_code: '%s'", + organizationID.String(), ledgerID.String(), assetCode) + + headerParams.Metadata = &bson.M{} + + assetRates, err := handler.Query.GetAllAssetRatesByAssetCode(ctx, organizationID, ledgerID, assetCode, *headerParams) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get AssetRate on query", err) + + logger.Infof("Error to get AssetRate: %s", err.Error()) + + return http.WithError(c, err) + } + + logger.Infof("Successfully get AssetRate") + + pagination.SetItems(assetRates) + + return http.OK(c, pagination) +} diff --git a/components/transaction/internal/adapters/http/in/routes.go b/components/transaction/internal/adapters/http/in/routes.go index 47556124..07eddd44 100644 --- a/components/transaction/internal/adapters/http/in/routes.go +++ b/components/transaction/internal/adapters/http/in/routes.go @@ -52,6 +52,7 @@ func NewRouter(lg mlog.Logger, tl *mopentelemetry.Telemetry, cc *mcasdoor.Casdoo // Asset-rate f.Put("/v1/organizations/:organization_id/ledgers/:ledger_id/asset-rates", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("asset-rate"), http.ParseUUIDPathParameters, http.WithBody(new(assetrate.CreateAssetRateInput), ah.CreateOrUpdateAssetRate)) f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/asset-rates/:external_id", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("asset-rate"), http.ParseUUIDPathParameters, ah.GetAssetRateByExternalID) + f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/asset-rates/from/:asset_code", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("asset-rate"), http.ParseUUIDPathParameters, ah.GetAllAssetRatesByAssetCode) // Health f.Get("/health", http.Ping) diff --git a/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go b/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go index 45d93ace..a5c89914 100644 --- a/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go +++ b/components/transaction/internal/adapters/postgres/assetrate/assetrate.mock.go @@ -56,6 +56,21 @@ func (mr *MockRepositoryMockRecorder) Create(ctx, assetRate any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), ctx, assetRate) } +// FindAllByAssetCodes mocks base method. +func (m *MockRepository) FindAllByAssetCodes(ctx context.Context, organizationID, ledgerID uuid.UUID, fromAssetCode string, toAssetCodes []string, limit, page int) ([]*AssetRate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAllByAssetCodes", ctx, organizationID, ledgerID, fromAssetCode, toAssetCodes, limit, page) + ret0, _ := ret[0].([]*AssetRate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAllByAssetCodes indicates an expected call of FindAllByAssetCodes. +func (mr *MockRepositoryMockRecorder) FindAllByAssetCodes(ctx, organizationID, ledgerID, fromAssetCode, toAssetCodes, limit, page any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAllByAssetCodes", reflect.TypeOf((*MockRepository)(nil).FindAllByAssetCodes), ctx, organizationID, ledgerID, fromAssetCode, toAssetCodes, limit, page) +} + // FindByCurrencyPair mocks base method. func (m *MockRepository) FindByCurrencyPair(ctx context.Context, organizationID, ledgerID uuid.UUID, from, to string) (*AssetRate, error) { m.ctrl.T.Helper() diff --git a/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go b/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go index 05bd340e..cdb8fbcf 100644 --- a/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go +++ b/components/transaction/internal/adapters/postgres/assetrate/assetrate.postgresql.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "github.com/Masterminds/squirrel" "reflect" "strconv" "strings" @@ -24,6 +25,7 @@ type Repository interface { Create(ctx context.Context, assetRate *AssetRate) (*AssetRate, error) FindByCurrencyPair(ctx context.Context, organizationID, ledgerID uuid.UUID, from, to string) (*AssetRate, error) FindByExternalID(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*AssetRate, error) + FindAllByAssetCodes(ctx context.Context, organizationID, ledgerID uuid.UUID, fromAssetCode string, toAssetCodes []string, limit, page int) ([]*AssetRate, error) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, assetRate *AssetRate) (*AssetRate, error) } @@ -210,6 +212,85 @@ func (r *AssetRatePostgreSQLRepository) FindByCurrencyPair(ctx context.Context, return record.ToEntity(), nil } +// FindAllByAssetCodes returns all asset rates by asset codes. +func (r *AssetRatePostgreSQLRepository) FindAllByAssetCodes(ctx context.Context, organizationID, ledgerID uuid.UUID, fromAssetCode string, toAssetCodes []string, limit, page int) ([]*AssetRate, error) { + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "postgres.find_all_asset_rates_by_asset_codes") + defer span.End() + + db, err := r.connection.GetDB() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get database connection", err) + + return nil, err + } + + var assetRates []*AssetRate + + findAll := squirrel.Select("*"). + From(r.tableName). + Where(squirrel.Expr("organization_id = ?", organizationID)). + Where(squirrel.Expr("ledger_id = ?", ledgerID)). + Where(squirrel.Expr(`"from" = ?`, fromAssetCode)). + Where(squirrel.Eq{`"to"`: toAssetCodes}). + OrderBy("created_at DESC"). + Limit(pkg.SafeIntToUint64(limit)). + Offset(pkg.SafeIntToUint64((page - 1) * limit)). + PlaceholderFormat(squirrel.Dollar) + + query, args, err := findAll.ToSql() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to build query", err) + + return nil, err + } + + ctx, spanQuery := tracer.Start(ctx, "postgres.find_all.query") + + rows, err := db.QueryContext(ctx, query, args...) + if err != nil { + mopentelemetry.HandleSpanError(&spanQuery, "Failed to execute query", err) + + return nil, pkg.ValidateBusinessError(constant.ErrEntityNotFound, reflect.TypeOf(AssetRate{}).Name()) + } + defer rows.Close() + + spanQuery.End() + + for rows.Next() { + var assetRate AssetRatePostgreSQLModel + if err := rows.Scan( + &assetRate.ID, + &assetRate.OrganizationID, + &assetRate.LedgerID, + &assetRate.ExternalID, + &assetRate.From, + &assetRate.To, + &assetRate.Rate, + &assetRate.RateScale, + &assetRate.Source, + &assetRate.TTL, + &assetRate.CreatedAt, + &assetRate.UpdatedAt, + ); err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to scan row", err) + + return nil, err + } + + assetRates = append(assetRates, assetRate.ToEntity()) + } + + if err := rows.Err(); err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get rows", err) + + return nil, err + } + + return assetRates, nil +} + // Update an AssetRate entity into Postgresql and returns the AssetRate updated. func (r *AssetRatePostgreSQLRepository) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, assetRate *AssetRate) (*AssetRate, error) { tracer := pkg.NewTracerFromContext(ctx) diff --git a/components/transaction/internal/services/query/get-all-assetrates-assetcode.go b/components/transaction/internal/services/query/get-all-assetrates-assetcode.go new file mode 100644 index 00000000..6270f8bd --- /dev/null +++ b/components/transaction/internal/services/query/get-all-assetrates-assetcode.go @@ -0,0 +1,58 @@ +package query + +import ( + "context" + "github.com/LerianStudio/midaz/pkg/net/http" + "reflect" + + "github.com/LerianStudio/midaz/components/transaction/internal/adapters/postgres/assetrate" + "github.com/LerianStudio/midaz/pkg" + "github.com/LerianStudio/midaz/pkg/mopentelemetry" + + "github.com/google/uuid" +) + +// GetAllAssetRatesByAssetCode returns all asset rates by asset codes. +func (uc *UseCase) GetAllAssetRatesByAssetCode(ctx context.Context, organizationID, ledgerID uuid.UUID, fromAssetCode string, filter http.QueryHeader) ([]*assetrate.AssetRate, error) { + logger := pkg.NewLoggerFromContext(ctx) + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "query.get_asset_rate_by_asset_codes") + defer span.End() + + logger.Infof("Trying to get asset rate by source asset code: %s and target asset codes: %v", fromAssetCode, filter.ToAssetCodes) + + assetRates, err := uc.AssetRateRepo.FindAllByAssetCodes(ctx, organizationID, ledgerID, fromAssetCode, filter.ToAssetCodes, filter.Limit, filter.Page) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get asset rate by asset codes on repository", err) + + logger.Errorf("Error getting asset rate: %v", err) + + return nil, err + } + + if assetRates != nil { + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(assetrate.AssetRate{}).Name(), filter) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get metadata on mongodb asset rate", err) + + logger.Errorf("Error get metadata on mongodb asset rate: %v", err) + + return nil, err + } + + metadataMap := make(map[string]map[string]any, len(metadata)) + + for _, meta := range metadata { + metadataMap[meta.EntityID] = meta.Data + } + + for i := range assetRates { + if data, ok := metadataMap[assetRates[i].ID]; ok { + assetRates[i].Metadata = data + } + } + } + + return assetRates, nil +} diff --git a/components/transaction/internal/services/query/get-all-assetrates-assetcode_test.go b/components/transaction/internal/services/query/get-all-assetrates-assetcode_test.go new file mode 100644 index 00000000..46bf968c --- /dev/null +++ b/components/transaction/internal/services/query/get-all-assetrates-assetcode_test.go @@ -0,0 +1,78 @@ +package query + +import ( + "context" + "errors" + "github.com/LerianStudio/midaz/pkg/mpointers" + "go.uber.org/mock/gomock" + "testing" + + "github.com/LerianStudio/midaz/components/transaction/internal/adapters/postgres/assetrate" + "github.com/LerianStudio/midaz/pkg" + + "github.com/stretchr/testify/assert" +) + +// GetAllAssetRatesByAssetCode is responsible to test GetAllAssetRatesByAssetCode +func GetAllAssetRatesByAssetCode(t *testing.T) { + id := pkg.GenerateUUIDv7() + orgID := pkg.GenerateUUIDv7() + ledgerID := pkg.GenerateUUIDv7() + fromAssetCode := "USD" + toAssetCodes := []string{"BRL"} + limit := 10 + page := 1 + + assetRate := &assetrate.AssetRate{ + ID: id.String(), + OrganizationID: orgID.String(), + LedgerID: ledgerID.String(), + ExternalID: pkg.GenerateUUIDv7().String(), + From: fromAssetCode, + To: toAssetCodes[0], + Rate: 100, + Scale: mpointers.Float64(2), + Source: mpointers.String("External System"), + TTL: 3600, + } + + uc := UseCase{ + AssetRateRepo: assetrate.NewMockRepository(gomock.NewController(t)), + } + + uc.AssetRateRepo.(*assetrate.MockRepository). + EXPECT(). + FindAllByAssetCodes(gomock.Any(), orgID, ledgerID, fromAssetCode, toAssetCodes, limit, page). + Return(assetRate, nil). + Times(1) + res, err := uc.AssetRateRepo.FindAllByAssetCodes(context.TODO(), orgID, ledgerID, fromAssetCode, toAssetCodes, limit, page) + + assert.Equal(t, assetRate, res) + assert.Nil(t, err) +} + +// GetAllAssetRatesByAssetCodeError is responsible to test GetAllAssetRatesByAssetCode with error +func GetAllAssetRatesByAssetCodeError(t *testing.T) { + orgID := pkg.GenerateUUIDv7() + ledgerID := pkg.GenerateUUIDv7() + fromAssetCode := "USD" + toAssetCodes := []string{"BRL"} + limit := 10 + page := 1 + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + AssetRateRepo: assetrate.NewMockRepository(gomock.NewController(t)), + } + + uc.AssetRateRepo.(*assetrate.MockRepository). + EXPECT(). + FindAllByAssetCodes(gomock.Any(), orgID, ledgerID, fromAssetCode, toAssetCodes, limit, page). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.AssetRateRepo.FindAllByAssetCodes(context.TODO(), orgID, ledgerID, fromAssetCode, toAssetCodes, limit, page) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/pkg/constant/http.go b/pkg/constant/http.go new file mode 100644 index 00000000..ebdb59fc --- /dev/null +++ b/pkg/constant/http.go @@ -0,0 +1,15 @@ +package constant + +var UUIDPathParameters = []string{ + "id", + "organization_id", + "ledger_id", + "asset_id", + "portfolio_id", + "product_id", + "account_id", + "transaction_id", + "operation_id", + "asset_rate_id", + "external_id", +} diff --git a/pkg/net/http/httputils.go b/pkg/net/http/httputils.go index 5653f261..e42b7303 100644 --- a/pkg/net/http/httputils.go +++ b/pkg/net/http/httputils.go @@ -17,11 +17,12 @@ import ( // QueryHeader entity from query parameter from get apis type QueryHeader struct { - Metadata *bson.M - Limit int - Page int - UseMetadata bool - PortfolioID string + Metadata *bson.M + Limit int + Page int + UseMetadata bool + PortfolioID string + ToAssetCodes []string } // ValidateParameters validate and return struct of default parameters @@ -36,6 +37,8 @@ func ValidateParameters(params map[string]string) *QueryHeader { var portfolioID string + var toAssetCodes []string + for key, value := range params { switch { case strings.Contains(key, "metadata."): @@ -47,15 +50,18 @@ func ValidateParameters(params map[string]string) *QueryHeader { page, _ = strconv.Atoi(value) case strings.Contains(key, "portfolio_id"): portfolioID = value + case strings.Contains(key, "to"): + toAssetCodes = strings.Split(value, ",") } } query := &QueryHeader{ - Metadata: metadata, - Limit: limit, - Page: page, - UseMetadata: useMetadata, - PortfolioID: portfolioID, + Metadata: metadata, + Limit: limit, + Page: page, + UseMetadata: useMetadata, + PortfolioID: portfolioID, + ToAssetCodes: toAssetCodes, } return query diff --git a/pkg/net/http/withBody.go b/pkg/net/http/withBody.go index b29b06ec..35f596de 100644 --- a/pkg/net/http/withBody.go +++ b/pkg/net/http/withBody.go @@ -170,14 +170,25 @@ func ParseUUIDPathParameters(c *fiber.Ctx) error { var invalidUUIDs []string + validPathParamsMap := make(map[string]any) + for param, value := range params { + if !pkg.Contains[string](cn.UUIDPathParameters, param) { + validPathParamsMap[param] = value + continue + } + parsedUUID, err := uuid.Parse(value) if err != nil { invalidUUIDs = append(invalidUUIDs, param) continue } - c.Locals(param, parsedUUID) + validPathParamsMap[param] = parsedUUID + } + + for param, value := range validPathParamsMap { + c.Locals(param, value) } if len(invalidUUIDs) > 0 { diff --git a/postman/MIDAZ.postman_collection.json b/postman/MIDAZ.postman_collection.json index d84e27c3..82614923 100644 --- a/postman/MIDAZ.postman_collection.json +++ b/postman/MIDAZ.postman_collection.json @@ -2795,7 +2795,24 @@ "listen": "test", "script": { "exec": [ - "" + "const jsonData = JSON.parse(responseBody);\r", + "if (jsonData.hasOwnProperty('id')) {\r", + " console.log(\"external_id before: \" + pm.collectionVariables.get(\"external_id\"));\r", + " pm.collectionVariables.set(\"external_id\", jsonData.id);\r", + " console.log(\"external_id after: \" + pm.collectionVariables.get(\"external_id\"));\r", + "}\r", + "\r", + "if (jsonData.hasOwnProperty('from')) {\r", + " console.log(\"asset_rate_from_asset_code before: \" + pm.collectionVariables.get(\"asset_rate_from_asset_code\"));\r", + " pm.collectionVariables.set(\"asset_rate_from_asset_code\", jsonData.from);\r", + " console.log(\"asset_rate_from_asset_code after: \" + pm.collectionVariables.get(\"asset_rate_from_asset_code\"));\r", + "}\r", + "\r", + "if (jsonData.hasOwnProperty('to')) {\r", + " console.log(\"asset_rate_to_asset_code before: \" + pm.collectionVariables.get(\"asset_rate_to_asset_code\"));\r", + " pm.collectionVariables.set(\"asset_rate_to_asset_code\", jsonData.from);\r", + " console.log(\"asset_rate_to_asset_code after: \" + pm.collectionVariables.get(\"asset_rate_to_asset_code\"));\r", + "}" ], "type": "text/javascript", "packages": {} @@ -2915,7 +2932,94 @@ ] }, { - "name": "Asset rates", + "name": "Asset Rates by External ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Midaz-ID", + "value": "{{$randomUUID}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{url_transaction}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/asset-rates/{{external_id}}", + "host": [ + "{{url_transaction}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "asset-rates", + "{{external_id}}" + ] + }, + "description": "This is a POST request, submitting data to an API via the request body. This request submits JSON data, and the data is reflected in the response.\n\nA successful POST request typically returns a `200 OK` or `201 Created` response code." + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Midaz-ID", + "value": "{{$randomUUID}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{url_transaction}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/asset-rates/{{external_id}}", + "host": [ + "{{url_transaction}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "asset-rates", + "{{external_id}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"id\": \"01939984-0ad7-7b34-aecd-2fc17b7bf9f5\",\n \"organizationId\": \"01939983-8fa8-7b57-bc3e-1f24d0675781\",\n \"ledgerId\": \"01939983-b0dd-7053-a0ba-6a31df62f7c0\",\n \"externalId\": \"04089c2e-75d3-4672-add3-daa73388af84\",\n \"from\": \"BRL\",\n \"to\": \"USD\",\n \"rate\": 600,\n \"scale\": 2,\n \"source\": \"External System\",\n \"ttl\": 3600,\n \"createdAt\": \"2024-12-05T22:09:47.863527-03:00\",\n \"updatedAt\": \"2024-12-05T22:09:47.863527-03:00\",\n \"metadata\": {\n \"int\": 1,\n \"string\": \"string\"\n }\n}" + } + ] + }, + { + "name": "Asset Rates by Asset Codes", "event": [ { "listen": "test", @@ -2926,6 +3030,18 @@ " console.log(\"asset_rate_id before: \" + pm.collectionVariables.get(\"asset_rate_id\"));\r", " pm.collectionVariables.set(\"ledger_id\", jsonData.id);\r", " console.log(\"asset_rate_id after: \" + pm.collectionVariables.get(\"asset_rate_id\"));\r", + "}\r", + "\r", + "if (jsonData.hasOwnProperty('from')) {\r", + " console.log(\"asset_rate_from_asset_code before: \" + pm.collectionVariables.get(\"asset_rate_from_asset_code\"));\r", + " pm.collectionVariables.set(\"asset_rate_from_asset_code\", jsonData.from);\r", + " console.log(\"asset_rate_from_asset_code after: \" + pm.collectionVariables.get(\"asset_rate_from_asset_code\"));\r", + "}\r", + "\r", + "if (jsonData.hasOwnProperty('to')) {\r", + " console.log(\"asset_rate_to_asset_code before: \" + pm.collectionVariables.get(\"asset_rate_to_asset_code\"));\r", + " pm.collectionVariables.set(\"asset_rate_to_asset_code\", jsonData.from);\r", + " console.log(\"asset_rate_to_asset_code after: \" + pm.collectionVariables.get(\"asset_rate_to_asset_code\"));\r", "}" ], "type": "text/javascript", @@ -2944,7 +3060,7 @@ } ], "url": { - "raw": "{{url_transaction}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/asset-rates/{{asset_rate_id}}", + "raw": "{{url_transaction}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/asset-rates/from/{{asset_rate_from_asset_code}}", "host": [ "{{url_transaction}}" ], @@ -2955,12 +3071,79 @@ "ledgers", "{{ledger_id}}", "asset-rates", - "{{asset_rate_id}}" + "from", + "{{asset_rate_from_asset_code}}" + ], + "query": [ + { + "key": "to", + "value": "BRL,USD,SGD", + "disabled": true + } ] }, "description": "This is a POST request, submitting data to an API via the request body. This request submits JSON data, and the data is reflected in the response.\n\nA successful POST request typically returns a `200 OK` or `201 Created` response code." }, - "response": [] + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Midaz-ID", + "value": "{{$randomUUID}}", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{url_transaction}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/asset-rates/from/{{asset_rate_from_asset_code}}?to=USD,BRL,EUR&limit=10&page=1", + "host": [ + "{{url_transaction}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "asset-rates", + "from", + "{{asset_rate_from_asset_code}}" + ], + "query": [ + { + "key": "to", + "value": "USD,BRL,EUR" + }, + { + "key": "limit", + "value": "10" + }, + { + "key": "page", + "value": "1" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"items\": [\n {\n \"id\": \"01939984-0ad7-7b34-aecd-2fc17b7bf9f5\",\n \"organizationId\": \"01939983-8fa8-7b57-bc3e-1f24d0675781\",\n \"ledgerId\": \"01939983-b0dd-7053-a0ba-6a31df62f7c0\",\n \"externalId\": \"04089c2e-75d3-4672-add3-daa73388af84\",\n \"from\": \"BRL\",\n \"to\": \"USD\",\n \"rate\": 600,\n \"scale\": 2,\n \"source\": \"External System\",\n \"ttl\": 3600,\n \"createdAt\": \"2024-12-05T22:09:47.863527-03:00\",\n \"updatedAt\": \"2024-12-05T22:09:47.863527-03:00\",\n \"metadata\": {\n \"int\": 1,\n \"string\": \"string\"\n }\n },\n {\n \"id\": \"01939983-f365-77c1-8223-732f60763a77\",\n \"organizationId\": \"01939983-8fa8-7b57-bc3e-1f24d0675781\",\n \"ledgerId\": \"01939983-b0dd-7053-a0ba-6a31df62f7c0\",\n \"externalId\": \"04089c2e-75d3-4672-add3-daa73388af84\",\n \"from\": \"BRL\",\n \"to\": \"EUR\",\n \"rate\": 600,\n \"scale\": 2,\n \"source\": \"External System\",\n \"ttl\": 3600,\n \"createdAt\": \"2024-12-05T22:09:41.861465-03:00\",\n \"updatedAt\": \"2024-12-05T22:09:41.861465-03:00\",\n \"metadata\": {\n \"int\": 1,\n \"string\": \"string\"\n }\n }\n ],\n \"page\": 1,\n \"limit\": 10\n}" + } + ] } ] } From da49821948f382c175fd5f5701d1fd539fda4bd1 Mon Sep 17 00:00:00 2001 From: Brecci Date: Thu, 5 Dec 2024 23:54:47 -0300 Subject: [PATCH 4/4] refactor(asset-rate): add code validation to the get all asset rates by asset codes endpoint :hammer: --- .../services/query/get-all-assetrates-assetcode.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/components/transaction/internal/services/query/get-all-assetrates-assetcode.go b/components/transaction/internal/services/query/get-all-assetrates-assetcode.go index 6270f8bd..42b7209d 100644 --- a/components/transaction/internal/services/query/get-all-assetrates-assetcode.go +++ b/components/transaction/internal/services/query/get-all-assetrates-assetcode.go @@ -22,6 +22,20 @@ func (uc *UseCase) GetAllAssetRatesByAssetCode(ctx context.Context, organization logger.Infof("Trying to get asset rate by source asset code: %s and target asset codes: %v", fromAssetCode, filter.ToAssetCodes) + if err := pkg.ValidateCode(fromAssetCode); err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to validate 'from' asset code", err) + + return nil, pkg.ValidateBusinessError(err, reflect.TypeOf(assetrate.AssetRate{}).Name()) + } + + for _, toAssetCode := range filter.ToAssetCodes { + if err := pkg.ValidateCode(toAssetCode); err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to validate 'to' asset codes", err) + + return nil, pkg.ValidateBusinessError(err, reflect.TypeOf(assetrate.AssetRate{}).Name()) + } + } + assetRates, err := uc.AssetRateRepo.FindAllByAssetCodes(ctx, organizationID, ledgerID, fromAssetCode, filter.ToAssetCodes, filter.Limit, filter.Page) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to get asset rate by asset codes on repository", err)