Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ type Repository interface {
FindByAlias(ctx context.Context, organizationID, ledgerID uuid.UUID, alias string) (bool, error)
ListByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, ids []uuid.UUID) ([]*mmodel.Account, error)
ListByAlias(ctx context.Context, organizationID, ledgerID, portfolioID uuid.UUID, alias []string) ([]*mmodel.Account, error)
Update(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, id uuid.UUID, acc *mmodel.Account) (*mmodel.Account, error)
Update(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, id uuid.UUID, uai *mmodel.UpdateAccountInput) (*mmodel.Account, error)
Delete(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, id uuid.UUID) error
ListAccountsByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*mmodel.Account, error)
ListAccountsByAlias(ctx context.Context, organizationID, ledgerID uuid.UUID, aliases []string) ([]*mmodel.Account, error)
Expand Down Expand Up @@ -806,7 +806,7 @@ func (r *AccountPostgreSQLRepository) ListByAlias(ctx context.Context, organizat
}

// Update an Account entity into Postgresql and returns the Account updated.
func (r *AccountPostgreSQLRepository) Update(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, id uuid.UUID, acc *mmodel.Account) (*mmodel.Account, error) {
func (r *AccountPostgreSQLRepository) Update(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, id uuid.UUID, uai *mmodel.UpdateAccountInput) (*mmodel.Account, error) {
logger, tracer, _, _ := libCommons.NewTrackingFromContext(ctx)

ctx, span := tracer.Start(ctx, "postgres.update_account")
Expand All @@ -821,42 +821,48 @@ func (r *AccountPostgreSQLRepository) Update(ctx context.Context, organizationID
return nil, err
}

record := &AccountPostgreSQLModel{}
record.FromEntity(acc)

builder := squirrel.Update(r.tableName)

if acc.Name != "" {
builder = builder.Set("name", record.Name)
}

if !acc.Status.IsEmpty() {
builder = builder.Set("status", record.Status)
builder = builder.Set("status_description", record.StatusDescription)
if uai.Name != "" {
builder = builder.Set("name", uai.Name)
}

if !libCommons.IsNilOrEmpty(acc.Alias) {
builder = builder.Set("alias", record.Alias)
if !uai.Status.IsEmpty() {
builder = builder.Set("status", uai.Status.Code)
builder = builder.Set("status_description", uai.Status.Description)
}

if acc.Blocked != nil {
builder = builder.Set("blocked", *acc.Blocked)
if uai.Blocked != nil {
builder = builder.Set("blocked", *uai.Blocked)
}

if !libCommons.IsNilOrEmpty(acc.SegmentID) {
builder = builder.Set("segment_id", record.SegmentID)
// Handle nullable fields - these can be set to NULL explicitly
if uai.SegmentID.ShouldUpdate() {
if uai.SegmentID.ShouldSetNull() {
builder = builder.Set("segment_id", nil)
} else {
builder = builder.Set("segment_id", uai.SegmentID.Value)
}
}

if !libCommons.IsNilOrEmpty(acc.EntityID) {
builder = builder.Set("entity_id", record.EntityID)
if uai.EntityID.ShouldUpdate() {
if uai.EntityID.ShouldSetNull() {
builder = builder.Set("entity_id", nil)
} else {
builder = builder.Set("entity_id", uai.EntityID.Value)
}
}

if !libCommons.IsNilOrEmpty(acc.PortfolioID) {
builder = builder.Set("portfolio_id", record.PortfolioID)
if uai.PortfolioID.ShouldUpdate() {
if uai.PortfolioID.ShouldSetNull() {
builder = builder.Set("portfolio_id", nil)
} else {
builder = builder.Set("portfolio_id", uai.PortfolioID.Value)
}
}

record.UpdatedAt = time.Now()
builder = builder.Set("updated_at", record.UpdatedAt)
updatedAt := time.Now()
builder = builder.Set("updated_at", updatedAt)

builder = builder.Where(squirrel.Eq{"organization_id": organizationID})
builder = builder.Where(squirrel.Eq{"ledger_id": ledgerID})
Expand Down Expand Up @@ -919,7 +925,8 @@ func (r *AccountPostgreSQLRepository) Update(ctx context.Context, organizationID
return nil, err
}

return record.ToEntity(), nil
// Fetch the updated account to return
return r.Find(ctx, organizationID, ledgerID, portfolioID, id)
}

// Delete an Account entity from the database (soft delete) using the provided ID.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,7 @@ func (uc *UseCase) UpdateAccount(ctx context.Context, organizationID, ledgerID u
return nil, pkg.ValidateBusinessError(constant.ErrForbiddenExternalAccountManipulation, reflect.TypeOf(mmodel.Account{}).Name())
}

account := &mmodel.Account{
Name: uai.Name,
Status: uai.Status,
EntityID: uai.EntityID,
SegmentID: uai.SegmentID,
PortfolioID: uai.PortfolioID,
Metadata: uai.Metadata,
Blocked: uai.Blocked,
}

accountUpdated, err := uc.AccountRepo.Update(ctx, organizationID, ledgerID, portfolioID, id, account)
accountUpdated, err := uc.AccountRepo.Update(ctx, organizationID, ledgerID, portfolioID, id, uai)
if err != nil {
logger.Errorf("Error updating account on repo by id: %v", err)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package command
import (
"context"
"errors"
"testing"

"github.com/LerianStudio/midaz/v3/components/onboarding/internal/adapters/mongodb"
"github.com/LerianStudio/midaz/v3/components/onboarding/internal/adapters/postgres/account"
"github.com/LerianStudio/midaz/v3/components/onboarding/internal/services"
"github.com/LerianStudio/midaz/v3/pkg/mmodel"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"testing"
)

func TestUpdateAccount(t *testing.T) {
Expand Down Expand Up @@ -46,8 +47,8 @@ func TestUpdateAccount(t *testing.T) {
Status: mmodel.Status{
Code: "active",
},
SegmentID: nil,
Metadata: map[string]any{"key": "value"},
// SegmentID omitted - uses zero value (unset)
Metadata: map[string]any{"key": "value"},
},
mockSetup: func() {
mockAccountRepo.EXPECT().
Expand Down Expand Up @@ -76,8 +77,8 @@ func TestUpdateAccount(t *testing.T) {
Status: mmodel.Status{
Code: "active",
},
SegmentID: nil,
Metadata: nil,
// SegmentID omitted - uses zero value (unset)
Metadata: nil,
},
mockSetup: func() {
mockAccountRepo.EXPECT().
Expand All @@ -97,8 +98,8 @@ func TestUpdateAccount(t *testing.T) {
Status: mmodel.Status{
Code: "active",
},
SegmentID: nil,
Metadata: map[string]any{"key": "value"},
// SegmentID omitted - uses zero value (unset)
Metadata: map[string]any{"key": "value"},
},
mockSetup: func() {
mockAccountRepo.EXPECT().
Expand Down Expand Up @@ -163,12 +164,12 @@ func TestUpdateAccount_BlockedProvidedTrue(t *testing.T) {

mockAccountRepo.EXPECT().
Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, _ uuid.UUID, _ uuid.UUID, _ *uuid.UUID, _ uuid.UUID, acc *mmodel.Account) (*mmodel.Account, error) {
if acc.Blocked == nil || !*acc.Blocked {
t.Fatalf("expected acc.Blocked to be true and non-nil")
DoAndReturn(func(_ context.Context, _ uuid.UUID, _ uuid.UUID, _ *uuid.UUID, _ uuid.UUID, uai *mmodel.UpdateAccountInput) (*mmodel.Account, error) {
if uai.Blocked == nil || !*uai.Blocked {
t.Fatalf("expected uai.Blocked to be true and non-nil")
}
// Echo back
return &mmodel.Account{ID: accountID.String(), Name: "Updated Account", Status: mmodel.Status{Code: "active"}, Blocked: acc.Blocked}, nil
return &mmodel.Account{ID: accountID.String(), Name: "Updated Account", Status: mmodel.Status{Code: "active"}, Blocked: uai.Blocked}, nil
})

mockMetadataRepo.EXPECT().
Expand Down Expand Up @@ -218,9 +219,9 @@ func TestUpdateAccount_BlockedOmitted(t *testing.T) {

mockAccountRepo.EXPECT().
Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, _ uuid.UUID, _ uuid.UUID, _ *uuid.UUID, _ uuid.UUID, acc *mmodel.Account) (*mmodel.Account, error) {
if acc.Blocked != nil {
t.Fatalf("expected acc.Blocked to be nil when omitted")
DoAndReturn(func(_ context.Context, _ uuid.UUID, _ uuid.UUID, _ *uuid.UUID, _ uuid.UUID, uai *mmodel.UpdateAccountInput) (*mmodel.Account, error) {
if uai.Blocked != nil {
t.Fatalf("expected uai.Blocked to be nil when omitted")
}
return &mmodel.Account{ID: accountID.String(), Name: "Updated Account"}, nil
})
Expand Down
20 changes: 14 additions & 6 deletions pkg/mmodel/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package mmodel
import (
"time"

"github.com/LerianStudio/midaz/v3/pkg/nullable"
"github.com/google/uuid"
)

// CreateAccountInput is a struct designed to encapsulate request create payload data.
//
// swagger:model CreateAccountInput
//
// @Description Request payload for creating a new account within a ledger. Accounts represent individual financial entities such as bank accounts, credit cards, expense categories, or any other financial buckets within a ledger. Accounts are identified by a unique ID, can have aliases for easy reference, and are associated with a specific asset type.
//
// @example {
Expand Down Expand Up @@ -89,6 +91,7 @@ type CreateAccountInput struct {
// UpdateAccountInput is a struct designed to encapsulate request update payload data.
//
// swagger:model UpdateAccountInput
//
// @Description Request payload for updating an existing account. All fields are optional - only specified fields will be updated. Omitted fields will remain unchanged. This allows partial updates to account properties such as name, status, portfolio, segment, and metadata.
//
// @example {
Expand All @@ -109,21 +112,21 @@ type UpdateAccountInput struct {
// maxLength: 256
Name string `json:"name" validate:"max=256" example:"Primary Corporate Checking Account" maxLength:"256"`

// Updated segment ID for the account
// Updated segment ID for the account (use null to unlink from segment)
// required: false
// format: uuid
SegmentID *string `json:"segmentId" validate:"omitempty,uuid" format:"uuid"`
SegmentID nullable.Nullable[string] `json:"segmentId" validate:"omitempty,uuid" format:"uuid" swaggertype:"string"`

// Updated portfolio ID for the account
// Updated portfolio ID for the account (use null to unlink from portfolio)
// required: false
// format: uuid
PortfolioID *string `json:"portfolioId" validate:"omitempty,uuid" format:"uuid"`
PortfolioID nullable.Nullable[string] `json:"portfolioId" validate:"omitempty,uuid" format:"uuid" swaggertype:"string"`

// Optional external identifier for linking to external systems
// Optional external identifier for linking to external systems (use null to remove)
// required: false
// example: EXT-ACC-12345
// maxLength: 256
EntityID *string `json:"entityId" validate:"omitempty,max=256" example:"EXT-ACC-12345" maxLength:"256"`
EntityID nullable.Nullable[string] `json:"entityId" validate:"omitempty,max=256" example:"EXT-ACC-12345" maxLength:"256" swaggertype:"string"`

// Updated status of the account
// required: false
Expand All @@ -142,6 +145,7 @@ type UpdateAccountInput struct {
// Account is a struct designed to encapsulate response payload data.
//
// swagger:model Account
//
// @Description Complete account entity containing all fields including system-generated fields like ID, creation timestamps, and metadata. This is the response format for account operations. Accounts represent individual financial entities (bank accounts, cards, expense categories, etc.) within a ledger and are the primary structures for tracking balances and transactions.
//
// @example {
Expand Down Expand Up @@ -256,6 +260,7 @@ func (a *Account) IDtoUUID() uuid.UUID {
// Accounts struct to return a paginated list of accounts.
//
// swagger:model Accounts
//
// @Description Paginated list of accounts with metadata about the current page, limit, and the account items themselves. Used for list operations.
//
// @example {
Expand Down Expand Up @@ -310,6 +315,7 @@ type Accounts struct {
// AccountResponse represents a success response containing a single account.
//
// swagger:response AccountResponse
//
// @Description Successful response containing a single account entity.
type AccountResponse struct {
// in: body
Expand All @@ -319,6 +325,7 @@ type AccountResponse struct {
// AccountsResponse represents a success response containing a paginated list of accounts.
//
// swagger:response AccountsResponse
//
// @Description Successful response containing a paginated list of accounts.
type AccountsResponse struct {
// in: body
Expand All @@ -328,6 +335,7 @@ type AccountsResponse struct {
// AccountErrorResponse represents an error response for account operations.
//
// swagger:response AccountErrorResponse
//
// @Description Error response for account operations with error code and message.
//
// @example {
Expand Down
12 changes: 12 additions & 0 deletions pkg/net/http/withBody.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
libOpentelemetry "github.com/LerianStudio/lib-commons/v2/commons/opentelemetry"
"github.com/LerianStudio/midaz/v3/pkg"
cn "github.com/LerianStudio/midaz/v3/pkg/constant"
"github.com/LerianStudio/midaz/v3/pkg/nullable"
pkgTransaction "github.com/LerianStudio/midaz/v3/pkg/transaction"
"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
Expand Down Expand Up @@ -218,6 +219,17 @@ func newValidator() (*validator.Validate, ut.Translator) {
panic(err)
}

// Register custom type func for nullable.Nullable[string] to extract inner value for validation
v.RegisterCustomTypeFunc(func(field reflect.Value) any {
if n, ok := field.Interface().(nullable.Nullable[string]); ok {
if n.IsSet && !n.IsNull {
return n.Value
}
}

return nil
}, nullable.Nullable[string]{})

v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
Expand Down
Loading