From 37d725d9247f262b9d597d81364d98907ef633b3 Mon Sep 17 00:00:00 2001 From: maxwelbm Date: Tue, 7 Jan 2025 19:13:55 -0300 Subject: [PATCH 01/49] chore: update text pt to en :wrench: --- chocolatey/tools/chocolateyinstall.ps1 | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/chocolatey/tools/chocolateyinstall.ps1 b/chocolatey/tools/chocolateyinstall.ps1 index 5565cc71..69e8bce3 100644 --- a/chocolatey/tools/chocolateyinstall.ps1 +++ b/chocolatey/tools/chocolateyinstall.ps1 @@ -8,12 +8,12 @@ $outputFile = Join-Path $toolsDir 'mdz.exe' $versionFmt = $version -replace '^v', '' -# URL do arquivo zipado +# Zipped file URL $url = "https://github.com/LerianStudio/midaz/releases/download/"+$version+"/midaz_"+$versionFmt+"_windows_amd64.zip" $checksum = '{{CHECKSUM}}' $silentArgs = '' -# Argumentos do pacote +# Package arguments $packageArgs = @{ packageName = 'mdz' unzipLocation = $toolsDir @@ -23,22 +23,22 @@ $packageArgs = @{ checksumType = 'sha256' } -# Instalar e descompactar o pacote +# Install and unzip the package Install-ChocolateyZipPackage @packageArgs -# Verificar se o arquivo .exe foi extraído corretamente +# Check that the .exe file has been extracted correctly if (-Not (Test-Path $outputFile)) { - throw "O arquivo mdz.exe não foi encontrado após a extração do zip." + throw "The file mdz.exe was not found after extracting the zip." } -# Certificar-se de que o diretório global 'bin' existe +# Make sure the global directory 'bin' exists if (-Not (Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir | Out-Null } -# Mover o executável para o diretório global -Write-Host "Copiando $outputFile para $binDir" +# Move the executable to the global directory +Write-Host "Copying $outputFile to $binDir" Copy-Item -Path $outputFile -Destination $binDir -Force -# Confirmar a instalação -Write-Host "Instalação completa. O executável mdz está disponível globalmente." +# Confirm installation +Write-Host "Installation complete. The mdz executable is available globally. From 932ec11f55cadef65309583d1c531457f82b8633 Mon Sep 17 00:00:00 2001 From: maxwelbm Date: Wed, 8 Jan 2025 09:11:34 -0300 Subject: [PATCH 02/49] chore: added verification and license on package choco :wrench: --- .github/workflows/packages-manager.yml | 6 +- chocolatey/tools/LICENSE.txt | 201 +++++++++++++++++++++++++ chocolatey/tools/VERIFICATION.txt | 9 ++ 3 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 chocolatey/tools/LICENSE.txt create mode 100644 chocolatey/tools/VERIFICATION.txt diff --git a/.github/workflows/packages-manager.yml b/.github/workflows/packages-manager.yml index 012c57b6..8baed628 100644 --- a/.github/workflows/packages-manager.yml +++ b/.github/workflows/packages-manager.yml @@ -105,11 +105,15 @@ jobs: echo "checksum=$checksum" >> $GITHUB_OUTPUT shell: pwsh - - name: Replace checksum in chocolateyinstall.ps1 + - name: Replace checksum in chocolateyinstall.ps1 and VERIFICATION.txt run: | (Get-Content .\chocolatey\tools\chocolateyinstall.ps1) ` -replace '{{CHECKSUM}}', '${{ steps.calculate-checksum.outputs.checksum }}' ` | Set-Content .\chocolatey\tools\chocolateyinstall.ps1 + + (Get-Content .\chocolatey\tools\VERIFICATION.txt) ` + -replace '{{CHECKSUM}}', '${{ steps.calculate-checksum.outputs.checksum }}' ` + | Set-Content .\chocolatey\tools\VERIFICATION.txt shell: pwsh - name: Publish Chocolatey package diff --git a/chocolatey/tools/LICENSE.txt b/chocolatey/tools/LICENSE.txt new file mode 100644 index 00000000..fdc5bdb0 --- /dev/null +++ b/chocolatey/tools/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Leriand LTDA + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/chocolatey/tools/VERIFICATION.txt b/chocolatey/tools/VERIFICATION.txt new file mode 100644 index 00000000..d76d72c4 --- /dev/null +++ b/chocolatey/tools/VERIFICATION.txt @@ -0,0 +1,9 @@ +VERIFICATION +Verification is intended to help moderators and the Chocolatey community +community in verifying that the contents of this package can be trusted. + +The package was generated by our CI system. + +Download the zipped application `windows_amd64.zip` from the github release tab + +It's hash should match: "{{CHECKSUM}}" From 202b58219728e51a97523d569c86fed0198175db Mon Sep 17 00:00:00 2001 From: lerian-studio Date: Wed, 8 Jan 2025 12:42:24 +0000 Subject: [PATCH 03/49] chore(release): 1.44.1-beta.1 ## [1.44.1-beta.1](https://github.com/LerianStudio/midaz/compare/v1.44.0...v1.44.1-beta.1) (2025-01-08) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e209aa..c52d1834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## [1.44.1-beta.1](https://github.com/LerianStudio/midaz/compare/v1.44.0...v1.44.1-beta.1) (2025-01-08) + ## [1.44.0](https://github.com/LerianStudio/midaz/compare/v1.43.0...v1.44.0) (2025-01-08) From 6bb89dd46e163b057cb4c7c32cdd8e3a8c418147 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Wed, 8 Jan 2025 20:34:06 +0100 Subject: [PATCH 04/49] feature: new race condition implementation --- .../internal/adapters/http/in/transaction.go | 63 ++++++++++++++----- .../internal/adapters/redis/consumer.redis.go | 49 +++++++++++++++ .../internal/adapters/redis/redis.mock.go | 15 +++++ .../command/create-idempotency-key.go | 2 +- .../command/create-lock-race-condition.go | 49 +++++++++++++++ pkg/utils.go | 12 ++++ 6 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 components/transaction/internal/services/command/create-lock-race-condition.go diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index b200ab1c..f1b225b4 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -62,7 +62,7 @@ func (handler *TransactionHandler) CreateTransactionJSON(p any, c *fiber.Ctx) er parserDSL := input.FromDSl() logger.Infof("Request to create an transaction with details: %#v", parserDSL) - response := handler.createTransaction(c, logger, *parserDSL) + response := handler.createTransaction(c, logger, *parserDSL, false) return response } @@ -130,7 +130,7 @@ func (handler *TransactionHandler) CreateTransactionDSL(c *fiber.Ctx) error { return http.WithError(c, err) } - response := handler.createTransaction(c, logger, parserDSL) + response := handler.createTransaction(c, logger, parserDSL, false) return response } @@ -379,42 +379,71 @@ func (handler *TransactionHandler) GetAllTransactions(c *fiber.Ctx) error { } // createTransaction func that received struct from DSL parsed and create Transaction -func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.Logger, parserDSL goldModel.Transaction) error { +func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.Logger, parserDSL goldModel.Transaction, isLocked bool) error { ctx := c.UserContext() tracer := pkg.NewTracerFromContext(ctx) organizationID := c.Locals("organization_id").(uuid.UUID) ledgerID := c.Locals("ledger_id").(uuid.UUID) - _, spanRedis := tracer.Start(ctx, "handler.create_transaction_idempotency") - - ts, _ := pkg.StructToJSONString(parserDSL) - hash := pkg.HashSHA256(ts) - key, ttl := http.GetIdempotencyKeyAndTTL(c) + _, spanValidateDSL := tracer.Start(ctx, "handler.create_transaction_validate_dsl") - err := handler.Command.CreateOrCheckIdempotencyKey(ctx, organizationID, ledgerID, key, hash, ttl) + validate, err := goldModel.ValidateSendSourceAndDistribute(parserDSL) if err != nil { - mopentelemetry.HandleSpanError(&spanRedis, "Redis idempotency key", err) + mopentelemetry.HandleSpanError(&spanValidateDSL, "Failed to validate send source and distribute", err) - logger.Error("Redis idempotency key:", err.Error()) + logger.Error("Validation failed:", err.Error()) return http.WithError(c, err) } - spanRedis.End() + spanValidateDSL.End() - _, spanValidateDSL := tracer.Start(ctx, "handler.create_transaction_validate_dsl") + _, spanRaceCondition := tracer.Start(ctx, "handler.create_transaction.race_condition") - validate, err := goldModel.ValidateSendSourceAndDistribute(parserDSL) + for _, alias := range validate.Aliases { + if !isLocked { + isLocked, err = handler.Command.CreateLockAccount(ctx, organizationID, ledgerID, alias) + if err != nil { + mopentelemetry.HandleSpanError(&spanRaceCondition, "Race Condition", err) + + logger.Error("Race Condition:", err.Error()) + + return http.WithError(c, err) + } + } + + for !isLocked { + err = handler.createTransaction(c, logger, parserDSL, isLocked) + if err != nil { + mopentelemetry.HandleSpanError(&spanRaceCondition, "Race Condition", err) + + logger.Error("Race Condition:", err.Error()) + + return http.WithError(c, err) + } + } + + } + + spanRaceCondition.End() + + _, spanRedis := tracer.Start(ctx, "handler.create_transaction_idempotency") + + ts, _ := pkg.StructToJSONString(parserDSL) + hash := pkg.HashSHA256(ts) + key, ttl := http.GetIdempotencyKeyAndTTL(c) + + err = handler.Command.CreateOrCheckIdempotencyKey(ctx, organizationID, ledgerID, key, hash, ttl) if err != nil { - mopentelemetry.HandleSpanError(&spanValidateDSL, "Failed to validate send source and distribute", err) + mopentelemetry.HandleSpanError(&spanRedis, "Redis idempotency key", err) - logger.Error("Validation failed:", err.Error()) + logger.Error("Redis idempotency key:", err.Error()) return http.WithError(c, err) } - spanValidateDSL.End() + spanRedis.End() ctxGetAccounts, spanGetAccounts := tracer.Start(ctx, "handler.create_transaction.get_accounts") diff --git a/components/transaction/internal/adapters/redis/consumer.redis.go b/components/transaction/internal/adapters/redis/consumer.redis.go index 22bb5574..29911af5 100644 --- a/components/transaction/internal/adapters/redis/consumer.redis.go +++ b/components/transaction/internal/adapters/redis/consumer.redis.go @@ -14,6 +14,7 @@ import ( //go:generate mockgen --destination=redis.mock.go --package=redis . RedisRepository type RedisRepository interface { Set(ctx context.Context, key, value string, ttl time.Duration) error + SetNX(ctx context.Context, key, value string, ttl time.Duration) (bool, error) Get(ctx context.Context, key string) (string, error) Del(ctx context.Context, key string) error } @@ -61,6 +62,32 @@ func (rr *RedisConsumerRepository) Set(ctx context.Context, key, value string, t return nil } +func (rr *RedisConsumerRepository) SetNX(ctx context.Context, key, value string, ttl time.Duration) (bool, error) { + logger := pkg.NewLoggerFromContext(ctx) + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "redis.set") + defer span.End() + + rds, err := rr.conn.GetClient(ctx) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get redis", err) + + return false, err + } + + logger.Infof("value of ttl: %v", ttl*time.Second) + + isLocked, err := rds.SetNX(ctx, key, value, ttl*time.Second).Result() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to set on redis", err) + + return false, err + } + + return isLocked, nil +} + func (rr *RedisConsumerRepository) Get(ctx context.Context, key string) (string, error) { logger := pkg.NewLoggerFromContext(ctx) tracer := pkg.NewTracerFromContext(ctx) @@ -88,5 +115,27 @@ func (rr *RedisConsumerRepository) Get(ctx context.Context, key string) (string, } func (rr *RedisConsumerRepository) Del(ctx context.Context, key string) error { + logger := pkg.NewLoggerFromContext(ctx) + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "redis.del") + defer span.End() + + rds, err := rr.conn.GetClient(ctx) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to del redis", err) + + return err + } + + val, err := rds.Del(ctx, key).Result() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to del on redis", err) + + return err + } + + logger.Infof("value : %v", val) + return nil } diff --git a/components/transaction/internal/adapters/redis/redis.mock.go b/components/transaction/internal/adapters/redis/redis.mock.go index 3506c88b..3e610dc1 100644 --- a/components/transaction/internal/adapters/redis/redis.mock.go +++ b/components/transaction/internal/adapters/redis/redis.mock.go @@ -82,3 +82,18 @@ func (mr *MockRedisRepositoryMockRecorder) Set(arg0, arg1, arg2, arg3 any) *gomo mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockRedisRepository)(nil).Set), arg0, arg1, arg2, arg3) } + +// SetNX mocks base method. +func (m *MockRedisRepository) SetNX(arg0 context.Context, arg1, arg2 string, arg3 time.Duration) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetNX", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SetNX indicates an expected call of SetNX. +func (mr *MockRedisRepositoryMockRecorder) SetNX(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNX", reflect.TypeOf((*MockRedisRepository)(nil).SetNX), arg0, arg1, arg2, arg3) +} diff --git a/components/transaction/internal/services/command/create-idempotency-key.go b/components/transaction/internal/services/command/create-idempotency-key.go index 15f8a393..1a7e0d6b 100644 --- a/components/transaction/internal/services/command/create-idempotency-key.go +++ b/components/transaction/internal/services/command/create-idempotency-key.go @@ -22,7 +22,7 @@ func (uc *UseCase) CreateOrCheckIdempotencyKey(ctx context.Context, organization key = hash } - internalKey := organizationID.String() + ":" + ledgerID.String() + ":" + key + internalKey := pkg.InternalKey(organizationID, ledgerID, key) value, err := uc.RedisRepo.Get(ctx, internalKey) if err != nil { diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go new file mode 100644 index 00000000..5bfe0806 --- /dev/null +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -0,0 +1,49 @@ +package command + +import ( + "context" + "github.com/LerianStudio/midaz/pkg" + "github.com/LerianStudio/midaz/pkg/mopentelemetry" + "github.com/google/uuid" +) + +const TimeSetLock = 30 + +func (uc *UseCase) CreateLockAccount(ctx context.Context, organizationID, ledgerID uuid.UUID, key string) (bool, error) { + logger := pkg.NewLoggerFromContext(context.Background()) + tracer := pkg.NewTracerFromContext(context.Background()) + + ctx, span := tracer.Start(ctx, "query.create_lock_race_condition") + defer span.End() + + internalKey := pkg.LockInternalKey(organizationID, ledgerID, key) + + isLocked, err := uc.RedisRepo.SetNX(ctx, internalKey, "processing account...", TimeSetLock) + if err != nil { + logger.Infof("Error to acquire lock: %v", err) + + return false, err + } + + logger.Infof("Account lock on redis: %v", internalKey) + return isLocked, nil +} + +func (uc *UseCase) ReleaseLockAccount(ctx context.Context, organizationID, ledgerID uuid.UUID, key string) { + logger := pkg.NewLoggerFromContext(context.Background()) + tracer := pkg.NewTracerFromContext(context.Background()) + + ctx, span := tracer.Start(ctx, "query.release_lock_account") + defer span.End() + + internalKey := pkg.LockInternalKey(organizationID, ledgerID, key) + + logger.Infof("Account release lock on redis: %v", internalKey) + + err := uc.RedisRepo.Del(ctx, internalKey) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to release Accounts lock", err) + + logger.Errorf("Failed to release Accounts lock: %v", err) + } +} diff --git a/pkg/utils.go b/pkg/utils.go index 00b342fd..de15311b 100644 --- a/pkg/utils.go +++ b/pkg/utils.go @@ -278,3 +278,15 @@ func Reverse[T any](s []T) []T { return s } + +func InternalKey(organizationID, ledgerID uuid.UUID, key string) string { + internalKey := organizationID.String() + ":" + ledgerID.String() + ":" + key + + return internalKey +} + +func LockInternalKey(organizationID, ledgerID uuid.UUID, key string) string { + lockInternalKey := "lock:" + InternalKey(organizationID, ledgerID, key) + + return lockInternalKey +} From 3248ee7e8fca50dc8882327e94f4c9fcbfd3529e Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Thu, 9 Jan 2025 12:07:34 +0100 Subject: [PATCH 05/49] feature: create race condition using gorotine and chanel --- .../internal/adapters/http/in/transaction.go | 65 +++++++---------- .../command/create-lock-race-condition.go | 70 ++++++++++++++----- 2 files changed, 76 insertions(+), 59 deletions(-) diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index f1b225b4..8f1d7f09 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -386,6 +386,23 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L organizationID := c.Locals("organization_id").(uuid.UUID) ledgerID := c.Locals("ledger_id").(uuid.UUID) + _, spanIdempotency := tracer.Start(ctx, "handler.create_transaction_idempotency") + + ts, _ := pkg.StructToJSONString(parserDSL) + hash := pkg.HashSHA256(ts) + key, ttl := http.GetIdempotencyKeyAndTTL(c) + + err := handler.Command.CreateOrCheckIdempotencyKey(ctx, organizationID, ledgerID, key, hash, ttl) + if err != nil { + mopentelemetry.HandleSpanError(&spanIdempotency, "Redis idempotency key", err) + + logger.Infof("Redis idempotency key: %v", err.Error()) + + return http.WithError(c, err) + } + + spanIdempotency.End() + _, spanValidateDSL := tracer.Start(ctx, "handler.create_transaction_validate_dsl") validate, err := goldModel.ValidateSendSourceAndDistribute(parserDSL) @@ -401,50 +418,10 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L _, spanRaceCondition := tracer.Start(ctx, "handler.create_transaction.race_condition") - for _, alias := range validate.Aliases { - if !isLocked { - isLocked, err = handler.Command.CreateLockAccount(ctx, organizationID, ledgerID, alias) - if err != nil { - mopentelemetry.HandleSpanError(&spanRaceCondition, "Race Condition", err) - - logger.Error("Race Condition:", err.Error()) - - return http.WithError(c, err) - } - } - - for !isLocked { - err = handler.createTransaction(c, logger, parserDSL, isLocked) - if err != nil { - mopentelemetry.HandleSpanError(&spanRaceCondition, "Race Condition", err) - - logger.Error("Race Condition:", err.Error()) - - return http.WithError(c, err) - } - } - - } + handler.Command.AllKeysUnlocked(ctx, organizationID, ledgerID, validate.Aliases) spanRaceCondition.End() - _, spanRedis := tracer.Start(ctx, "handler.create_transaction_idempotency") - - ts, _ := pkg.StructToJSONString(parserDSL) - hash := pkg.HashSHA256(ts) - key, ttl := http.GetIdempotencyKeyAndTTL(c) - - err = handler.Command.CreateOrCheckIdempotencyKey(ctx, organizationID, ledgerID, key, hash, ttl) - if err != nil { - mopentelemetry.HandleSpanError(&spanRedis, "Redis idempotency key", err) - - logger.Error("Redis idempotency key:", err.Error()) - - return http.WithError(c, err) - } - - spanRedis.End() - ctxGetAccounts, spanGetAccounts := tracer.Start(ctx, "handler.create_transaction.get_accounts") token := http.GetTokenHeader(c) @@ -526,6 +503,12 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L spanProcessAccounts.End() + _, spanReleaseLock := tracer.Start(ctx, "handler.create_transaction.delete_race_condition") + + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases) + + spanReleaseLock.End() + ctxUpdateTransactionStatus, spanUpdateTransactionStatus := tracer.Start(ctx, "handler.create_transaction.update_transaction_status") //TODO: use event driven and broken and parts diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index 5bfe0806..52cf84c1 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -5,45 +5,79 @@ import ( "github.com/LerianStudio/midaz/pkg" "github.com/LerianStudio/midaz/pkg/mopentelemetry" "github.com/google/uuid" + "sync" + "time" ) -const TimeSetLock = 30 +const TimeSetLock = 2 -func (uc *UseCase) CreateLockAccount(ctx context.Context, organizationID, ledgerID uuid.UUID, key string) (bool, error) { +func (uc *UseCase) AllKeysUnlocked(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string) { logger := pkg.NewLoggerFromContext(context.Background()) tracer := pkg.NewTracerFromContext(context.Background()) - ctx, span := tracer.Start(ctx, "query.create_lock_race_condition") + ctx, span := tracer.Start(ctx, "redis.all_keys_unlocked") defer span.End() - internalKey := pkg.LockInternalKey(organizationID, ledgerID, key) + var wg sync.WaitGroup + resultChan := make(chan bool, len(keys)) - isLocked, err := uc.RedisRepo.SetNX(ctx, internalKey, "processing account...", TimeSetLock) - if err != nil { - logger.Infof("Error to acquire lock: %v", err) + for _, key := range keys { + internalKey := pkg.LockInternalKey(organizationID, ledgerID, key) - return false, err + logger.Infof("Account try to lock on redis: %v", internalKey) + + wg.Add(1) + go uc.checkAndReleaseLock(ctx, &wg, internalKey, resultChan) } - logger.Infof("Account lock on redis: %v", internalKey) - return isLocked, nil + wg.Wait() + close(resultChan) +} + +func (uc *UseCase) checkAndReleaseLock(ctx context.Context, wg *sync.WaitGroup, internalKey string, resultChan chan bool) { + logger := pkg.NewLoggerFromContext(context.Background()) + tracer := pkg.NewTracerFromContext(context.Background()) + + ctx, span := tracer.Start(ctx, "redis.check_and_release_lock") + defer span.End() + + defer wg.Done() + + for { + success, err := uc.RedisRepo.SetNX(context.Background(), internalKey, "processing account...", TimeSetLock) + if err != nil { + resultChan <- false + return + } + + logger.Infof("Account locked on redis: %v", internalKey) + + if success { + resultChan <- true + return + } + + time.Sleep(500 * time.Millisecond) + } } -func (uc *UseCase) ReleaseLockAccount(ctx context.Context, organizationID, ledgerID uuid.UUID, key string) { +func (uc *UseCase) DeleteLocks(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string) { logger := pkg.NewLoggerFromContext(context.Background()) tracer := pkg.NewTracerFromContext(context.Background()) - ctx, span := tracer.Start(ctx, "query.release_lock_account") + ctx, span := tracer.Start(ctx, "redis.delete_locks") defer span.End() - internalKey := pkg.LockInternalKey(organizationID, ledgerID, key) + for _, key := range keys { + internalKey := pkg.LockInternalKey(organizationID, ledgerID, key) - logger.Infof("Account release lock on redis: %v", internalKey) + logger.Infof("Account releasing lock on redis: %v", internalKey) - err := uc.RedisRepo.Del(ctx, internalKey) - if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to release Accounts lock", err) + err := uc.RedisRepo.Del(ctx, internalKey) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to release Accounts lock", err) - logger.Errorf("Failed to release Accounts lock: %v", err) + logger.Errorf("Failed to release Accounts lock: %v", err) + } } } From 5a7988e161e0bb64a64149c1871ba2f0c9f2dbd5 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Thu, 9 Jan 2025 12:08:07 +0100 Subject: [PATCH 06/49] fix: improve idempotency using setnx; :bug: --- .../services/command/create-idempotency-key.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/components/transaction/internal/services/command/create-idempotency-key.go b/components/transaction/internal/services/command/create-idempotency-key.go index 1a7e0d6b..bc09f625 100644 --- a/components/transaction/internal/services/command/create-idempotency-key.go +++ b/components/transaction/internal/services/command/create-idempotency-key.go @@ -24,17 +24,12 @@ func (uc *UseCase) CreateOrCheckIdempotencyKey(ctx context.Context, organization internalKey := pkg.InternalKey(organizationID, ledgerID, key) - value, err := uc.RedisRepo.Get(ctx, internalKey) + success, err := uc.RedisRepo.SetNX(context.Background(), internalKey, hash, ttl) if err != nil { - logger.Error("Error to get idempotency key on redis failed:", err.Error()) + logger.Error("Error to lock idempotency key on redis failed:", err.Error()) } - if value == "" { - err = uc.RedisRepo.Set(ctx, internalKey, hash, ttl) - if err != nil { - logger.Error("Error to set idempotency key on redis failed:", err.Error()) - } - } else { + if !success { err = pkg.ValidateBusinessError(constant.ErrIdempotencyKey, "CreateOrCheckIdempotencyKey", key) mopentelemetry.HandleSpanError(&span, "Failed exists value on redis with this key", err) From b84dc7a5c886496746a382bda6059afb77583c47 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Thu, 9 Jan 2025 13:53:53 +0100 Subject: [PATCH 07/49] refactor: change get accounts to command to test :hammer: --- components/ledger/internal/adapters/grpc/in/account.go | 6 +++--- .../services/{query => command}/get-alias-accounts.go | 2 +- .../services/{query => command}/get-alias-accounts_test.go | 5 +++-- .../services/{query => command}/get-ids-accounts.go | 2 +- .../services/{query => command}/get-ids-accounts_test.go | 5 +++-- 5 files changed, 11 insertions(+), 9 deletions(-) rename components/ledger/internal/services/{query => command}/get-alias-accounts.go (98%) rename components/ledger/internal/services/{query => command}/get-alias-accounts_test.go (96%) rename components/ledger/internal/services/{query => command}/get-ids-accounts.go (98%) rename components/ledger/internal/services/{query => command}/get-ids-accounts_test.go (96%) diff --git a/components/ledger/internal/adapters/grpc/in/account.go b/components/ledger/internal/adapters/grpc/in/account.go index 31aa45b5..a2915fa2 100644 --- a/components/ledger/internal/adapters/grpc/in/account.go +++ b/components/ledger/internal/adapters/grpc/in/account.go @@ -60,7 +60,7 @@ func (ap *AccountProto) GetAccountsByIds(ctx context.Context, ids *account.Accou return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), strings.Join(invalidUUIDs, ", ")) } - acc, err := ap.Query.ListAccountsByIDs(ctx, organizationUUID, ledgerUUID, uuids) + acc, err := ap.Command.ListAccountsByIDs(ctx, organizationUUID, ledgerUUID, uuids) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to retrieve Accounts by ids for grpc", err) @@ -99,7 +99,7 @@ func (ap *AccountProto) GetAccountsByAliases(ctx context.Context, aliases *accou return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), ledgerUUID) } - acc, err := ap.Query.ListAccountsByAlias(ctx, organizationUUID, ledgerUUID, aliases.GetAliases()) + acc, err := ap.Command.ListAccountsByAlias(ctx, organizationUUID, ledgerUUID, aliases.GetAliases()) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to retrieve Accounts by aliases for grpc", err) @@ -177,7 +177,7 @@ func (ap *AccountProto) UpdateAccounts(ctx context.Context, update *account.Acco organizationID := update.GetOrganizationId() ledgerID := update.GetLedgerId() - acc, err := ap.Query.ListAccountsByIDs(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuids) + acc, err := ap.Command.ListAccountsByIDs(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuids) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to retrieve Accounts by ids for grpc", err) diff --git a/components/ledger/internal/services/query/get-alias-accounts.go b/components/ledger/internal/services/command/get-alias-accounts.go similarity index 98% rename from components/ledger/internal/services/query/get-alias-accounts.go rename to components/ledger/internal/services/command/get-alias-accounts.go index 4b5ed64c..ee43040e 100644 --- a/components/ledger/internal/services/query/get-alias-accounts.go +++ b/components/ledger/internal/services/command/get-alias-accounts.go @@ -1,4 +1,4 @@ -package query +package command import ( "context" diff --git a/components/ledger/internal/services/query/get-alias-accounts_test.go b/components/ledger/internal/services/command/get-alias-accounts_test.go similarity index 96% rename from components/ledger/internal/services/query/get-alias-accounts_test.go rename to components/ledger/internal/services/command/get-alias-accounts_test.go index dbe04334..2b86d9f5 100644 --- a/components/ledger/internal/services/query/get-alias-accounts_test.go +++ b/components/ledger/internal/services/command/get-alias-accounts_test.go @@ -1,8 +1,9 @@ -package query +package command import ( "context" "errors" + "github.com/LerianStudio/midaz/components/ledger/internal/services/query" "testing" "github.com/LerianStudio/midaz/components/ledger/internal/adapters/postgres/account" @@ -20,7 +21,7 @@ func TestListAccountsByAlias(t *testing.T) { mockAccountRepo := account.NewMockRepository(ctrl) - uc := &UseCase{ + uc := &query.UseCase{ AccountRepo: mockAccountRepo, } diff --git a/components/ledger/internal/services/query/get-ids-accounts.go b/components/ledger/internal/services/command/get-ids-accounts.go similarity index 98% rename from components/ledger/internal/services/query/get-ids-accounts.go rename to components/ledger/internal/services/command/get-ids-accounts.go index 7074f0de..bcc55fd0 100644 --- a/components/ledger/internal/services/query/get-ids-accounts.go +++ b/components/ledger/internal/services/command/get-ids-accounts.go @@ -1,4 +1,4 @@ -package query +package command import ( "context" diff --git a/components/ledger/internal/services/query/get-ids-accounts_test.go b/components/ledger/internal/services/command/get-ids-accounts_test.go similarity index 96% rename from components/ledger/internal/services/query/get-ids-accounts_test.go rename to components/ledger/internal/services/command/get-ids-accounts_test.go index e690770c..6044d939 100644 --- a/components/ledger/internal/services/query/get-ids-accounts_test.go +++ b/components/ledger/internal/services/command/get-ids-accounts_test.go @@ -1,8 +1,9 @@ -package query +package command import ( "context" "errors" + "github.com/LerianStudio/midaz/components/ledger/internal/services/query" "testing" "github.com/LerianStudio/midaz/components/ledger/internal/adapters/postgres/account" @@ -19,7 +20,7 @@ func TestListAccountsByIDs(t *testing.T) { mockAccountRepo := account.NewMockRepository(ctrl) - uc := &UseCase{ + uc := &query.UseCase{ AccountRepo: mockAccountRepo, } From ddb6a60ca16b3560d6b8c0802e12fb9a246894bf Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Thu, 9 Jan 2025 14:03:15 +0100 Subject: [PATCH 08/49] fix: reduce lock time :bug: --- .../internal/services/command/create-lock-race-condition.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index 52cf84c1..e324d608 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -9,7 +9,7 @@ import ( "time" ) -const TimeSetLock = 2 +const TimeSetLock = 1 func (uc *UseCase) AllKeysUnlocked(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string) { logger := pkg.NewLoggerFromContext(context.Background()) @@ -57,7 +57,7 @@ func (uc *UseCase) checkAndReleaseLock(ctx context.Context, wg *sync.WaitGroup, return } - time.Sleep(500 * time.Millisecond) + time.Sleep(250 * time.Millisecond) } } From 5b57ec0ebbc9d3847058b69d95b7c80e5fde3cb0 Mon Sep 17 00:00:00 2001 From: maxwelbm Date: Thu, 9 Jan 2025 11:20:32 -0300 Subject: [PATCH 09/49] chore: remove query param alias from GetAccountsAll :wrench: --- components/ledger/internal/adapters/http/in/account.go | 2 -- .../adapters/postgres/account/account.postgresql.go | 4 ---- pkg/mpostgres/pagination.go | 1 - pkg/net/http/httputils.go | 7 ------- 4 files changed, 14 deletions(-) diff --git a/components/ledger/internal/adapters/http/in/account.go b/components/ledger/internal/adapters/http/in/account.go index c89a5a3a..2a57fa34 100644 --- a/components/ledger/internal/adapters/http/in/account.go +++ b/components/ledger/internal/adapters/http/in/account.go @@ -89,7 +89,6 @@ func (handler *AccountHandler) CreateAccount(i any, c *fiber.Ctx) error { // @Param start_date query string false "Start Date" example "2021-01-01" // @Param end_date query string false "End Date" example "2021-01-01" // @Param sort_order query string false "Sort Order" Enums(asc,desc) -// @Param alias query string false "Find alias" example "@wallet_12345123" // @Success 200 {object} mpostgres.Pagination{items=[]mmodel.Account,page=int,limit=int} // @Router /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts [get] func (handler *AccountHandler) GetAllAccounts(c *fiber.Ctx) error { @@ -120,7 +119,6 @@ func (handler *AccountHandler) GetAllAccounts(c *fiber.Ctx) error { SortOrder: headerParams.SortOrder, StartDate: headerParams.StartDate, EndDate: headerParams.EndDate, - Alias: headerParams.Alias, } if !pkg.IsNilOrEmpty(&headerParams.PortfolioID) { diff --git a/components/ledger/internal/adapters/postgres/account/account.postgresql.go b/components/ledger/internal/adapters/postgres/account/account.postgresql.go index ebf8bf24..9acf88e6 100644 --- a/components/ledger/internal/adapters/postgres/account/account.postgresql.go +++ b/components/ledger/internal/adapters/postgres/account/account.postgresql.go @@ -177,10 +177,6 @@ func (r *AccountPostgreSQLRepository) FindAll(ctx context.Context, organizationI Where(squirrel.GtOrEq{"created_at": pkg.NormalizeDate(filter.StartDate, mpointers.Int(-1))}). Where(squirrel.LtOrEq{"created_at": pkg.NormalizeDate(filter.EndDate, mpointers.Int(1))}) - if len(filter.Alias) > 0 { - findAll = findAll.Where(squirrel.Expr("alias = ?", filter.Alias)) - } - findAll = findAll.Limit(pkg.SafeIntToUint64(filter.Limit)). Offset(pkg.SafeIntToUint64((filter.Page - 1) * filter.Limit)). PlaceholderFormat(squirrel.Dollar) diff --git a/pkg/mpostgres/pagination.go b/pkg/mpostgres/pagination.go index d22eb173..0e6727ea 100644 --- a/pkg/mpostgres/pagination.go +++ b/pkg/mpostgres/pagination.go @@ -15,7 +15,6 @@ type Pagination struct { SortOrder string `json:"-" example:"asc"` StartDate time.Time `json:"-" example:"2021-01-01"` EndDate time.Time `json:"-" example:"2021-12-31"` - Alias string `json:"-" example:"@wallet_12345123"` } // @name Pagination // SetItems set an array of any struct in items. diff --git a/pkg/net/http/httputils.go b/pkg/net/http/httputils.go index c1f550f1..3aecb305 100644 --- a/pkg/net/http/httputils.go +++ b/pkg/net/http/httputils.go @@ -28,7 +28,6 @@ type QueryHeader struct { SortOrder string StartDate time.Time EndDate time.Time - Alias string UseMetadata bool PortfolioID string ToAssetCodes []string @@ -42,7 +41,6 @@ type Pagination struct { SortOrder string StartDate time.Time EndDate time.Time - Alias string } // ValidateParameters validate and return struct of default parameters @@ -54,7 +52,6 @@ func ValidateParameters(params map[string]string) (*QueryHeader, error) { startDate time.Time endDate time.Time cursor string - alias string limit = 10 page = 1 sortOrder = "desc" @@ -82,8 +79,6 @@ func ValidateParameters(params map[string]string) (*QueryHeader, error) { portfolioID = value case strings.Contains(key, "to"): toAssetCodes = strings.Split(value, ",") - case key == "alias": - alias = value } } @@ -112,7 +107,6 @@ func ValidateParameters(params map[string]string) (*QueryHeader, error) { SortOrder: sortOrder, StartDate: startDate, EndDate: endDate, - Alias: alias, UseMetadata: useMetadata, PortfolioID: portfolioID, ToAssetCodes: toAssetCodes, @@ -276,7 +270,6 @@ func (qh *QueryHeader) ToOffsetPagination() Pagination { SortOrder: qh.SortOrder, StartDate: qh.StartDate, EndDate: qh.EndDate, - Alias: qh.Alias, } } From f1ee3ad132573c6715933cbe902001646f43ed85 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Thu, 9 Jan 2025 16:35:45 +0100 Subject: [PATCH 10/49] refactor: add update all in one :hammer: --- .../postgres/account/account.postgresql.go | 65 +++++++++++++++++++ .../services/command/update-account-id.go | 26 ++++++++ 2 files changed, 91 insertions(+) diff --git a/components/ledger/internal/adapters/postgres/account/account.postgresql.go b/components/ledger/internal/adapters/postgres/account/account.postgresql.go index ebf8bf24..75b18679 100644 --- a/components/ledger/internal/adapters/postgres/account/account.postgresql.go +++ b/components/ledger/internal/adapters/postgres/account/account.postgresql.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "github.com/LerianStudio/midaz/pkg/mgrpc/account" "reflect" "strconv" "strings" @@ -41,6 +42,7 @@ type Repository interface { 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) UpdateAccountByID(ctx context.Context, organizationID, ledgerID uuid.UUID, id uuid.UUID, acc *mmodel.Account) (*mmodel.Account, error) + UpdateAccounts(ctx context.Context, organizationID, ledgerID uuid.UUID, acc []*account.Account) error } // AccountPostgreSQLRepository is a Postgresql-specific implementation of the AccountRepository. @@ -939,3 +941,66 @@ func (r *AccountPostgreSQLRepository) UpdateAccountByID(ctx context.Context, org return record.ToEntity(), nil } + +func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organizationID, ledgerID uuid.UUID, accounts []*account.Account) error { + tracer := pkg.NewTracerFromContext(ctx) + ctx, span0 := tracer.Start(ctx, "postgres.update_accounts.get_db") + defer span0.End() + + db, err := r.connection.GetDB() + if err != nil { + mopentelemetry.HandleSpanError(&span0, "Failed to get database connection", err) + + return err + } + + ctx, span1 := tracer.Start(ctx, "postgres.update_accounts.begin") + defer span1.End() + + tx, err := db.Begin() + if err != nil { + mopentelemetry.HandleSpanError(&span1, "Failed to init transaction", err) + + return err + } + + for _, acc := range accounts { + var updates []string + var args []any + + updates = append(updates, "available_balance = $"+strconv.Itoa(len(args)+1)) + args = append(args, acc.Balance.Available) + + updates = append(updates, "on_hold_balance = $"+strconv.Itoa(len(args)+1)) + args = append(args, acc.Balance.OnHold) + + updates = append(updates, "balance_scale = $"+strconv.Itoa(len(args)+1)) + args = append(args, acc.Balance.Scale) + + updates = append(updates, "updated_at = $"+strconv.Itoa(len(args)+1)) + args = append(args, time.Now(), organizationID, ledgerID, acc.Id) + + query := `UPDATE account 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)) + + ` AND deleted_at IS NULL` + + _, err := tx.ExecContext(ctx, query, args...) + if err != nil { + mopentelemetry.HandleSpanError(&span1, "Failed to update account", err) + + return err + } + } + + if err := tx.Commit().Error; err != nil { + err := pkg.ValidateBusinessError(constant.ErrEntityNotFound, reflect.TypeOf(mmodel.Account{}).Name()) + + mopentelemetry.HandleSpanError(&span1, "Failed to commit accounts", err) + + return err + } + + return nil +} diff --git a/components/ledger/internal/services/command/update-account-id.go b/components/ledger/internal/services/command/update-account-id.go index 51a2c39c..29650a9c 100644 --- a/components/ledger/internal/services/command/update-account-id.go +++ b/components/ledger/internal/services/command/update-account-id.go @@ -3,6 +3,7 @@ package command import ( "context" "errors" + "github.com/LerianStudio/midaz/pkg/mgrpc/account" "reflect" "github.com/LerianStudio/midaz/components/ledger/internal/services" @@ -43,3 +44,28 @@ func (uc *UseCase) UpdateAccountByID(ctx context.Context, organizationID, ledger return accountUpdated, nil } + +func (uc *UseCase) UpdateAccounts(ctx context.Context, organizationID, ledgerID uuid.UUID, accounts []*account.Account) error { + logger := pkg.NewLoggerFromContext(ctx) + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "command.update_accounts") + defer span.End() + + logger.Infof("Trying to update accounts") + + err := uc.AccountRepo.UpdateAccounts(ctx, organizationID, ledgerID, accounts) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to update account on repo by id", err) + + logger.Errorf("Error updating account on repo by id: %v", err) + + if errors.Is(err, services.ErrDatabaseItemNotFound) { + return pkg.ValidateBusinessError(constant.ErrAccountIDNotFound, reflect.TypeOf(mmodel.Account{}).Name()) + } + + return err + } + + return nil +} From 022c3c90f6827149bc8ba4e78b2acb314895bbc8 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Thu, 9 Jan 2025 17:38:16 +0100 Subject: [PATCH 11/49] fix: updates to improve race condition :bug: --- .../internal/adapters/grpc/in/account.go | 151 +++++++++++------- .../postgres/account/account.postgresql.go | 19 ++- .../services/command/update-account-id.go | 4 +- .../internal/adapters/http/in/transaction.go | 7 +- .../command/create-lock-race-condition.go | 25 +-- .../services/command/update-transaction.go | 4 +- 6 files changed, 124 insertions(+), 86 deletions(-) diff --git a/components/ledger/internal/adapters/grpc/in/account.go b/components/ledger/internal/adapters/grpc/in/account.go index a2915fa2..656639ce 100644 --- a/components/ledger/internal/adapters/grpc/in/account.go +++ b/components/ledger/internal/adapters/grpc/in/account.go @@ -120,6 +120,83 @@ func (ap *AccountProto) GetAccountsByAliases(ctx context.Context, aliases *accou return &response, nil } +//// UpdateAccounts is a method that update Account balances by a given ids. +//func (ap *AccountProto) UpdateAccounts(ctx context.Context, update *account.AccountsRequest) (*account.AccountsResponse, error) { +// logger := pkg.NewLoggerFromContext(ctx) +// tracer := pkg.NewTracerFromContext(ctx) +// +// ctx, span := tracer.Start(ctx, "handler.UpdateAccounts") +// defer span.End() +// +// accounts := make([]*account.Account, 0) +// +// uuids := make([]uuid.UUID, 0) +// +// for _, getacc := range update.GetAccounts() { +// if pkg.IsNilOrEmpty(&getacc.Id) { +// mopentelemetry.HandleSpanError(&span, "Failed to update Accounts because id is empty", nil) +// +// logger.Errorf("Failed to update Accounts because id is empty") +// +// return nil, pkg.ValidateBusinessError(constant.ErrNoAccountIDsProvided, reflect.TypeOf(mmodel.Account{}).Name()) +// } +// +// balance := mmodel.Balance{ +// Available: &getacc.Balance.Available, +// OnHold: &getacc.Balance.OnHold, +// Scale: &getacc.Balance.Scale, +// } +// +// organizationUUID, err := uuid.Parse(getacc.GetOrganizationId()) +// if err != nil { +// return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), organizationUUID) +// } +// +// ledgerUUID, err := uuid.Parse(getacc.GetLedgerId()) +// if err != nil { +// return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), ledgerUUID) +// } +// +// accountUUID, err := uuid.Parse(getacc.GetId()) +// if err != nil { +// return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), accountUUID) +// } +// +// _, err = ap.Command.UpdateAccountByID(ctx, organizationUUID, ledgerUUID, accountUUID, &balance) +// if err != nil { +// mopentelemetry.HandleSpanError(&span, "Failed to update balance in Account by id", err) +// +// logger.Errorf("Failed to update balance in Account by id for organizationId %s and ledgerId %s in grpc, Error: %s", getacc.OrganizationId, getacc.LedgerId, err.Error()) +// +// return nil, pkg.ValidateBusinessError(constant.ErrBalanceUpdateFailed, reflect.TypeOf(mmodel.Account{}).Name()) +// } +// +// uuids = append(uuids, uuid.MustParse(getacc.Id)) +// } +// +// organizationID := update.GetOrganizationId() +// ledgerID := update.GetLedgerId() +// +// acc, err := ap.Command.ListAccountsByIDs(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuids) +// if err != nil { +// mopentelemetry.HandleSpanError(&span, "Failed to retrieve Accounts by ids for grpc", err) +// +// logger.Errorf("Failed to retrieve Accounts by ids for organizationId %s and ledgerId %s in grpc, Error: %s", organizationID, ledgerID, err.Error()) +// +// return nil, pkg.ValidateBusinessError(constant.ErrNoAccountsFound, reflect.TypeOf(mmodel.Account{}).Name()) +// } +// +// for _, ac := range acc { +// accounts = append(accounts, ac.ToProto()) +// } +// +// response := account.AccountsResponse{ +// Accounts: accounts, +// } +// +// return &response, nil +//} + // UpdateAccounts is a method that update Account balances by a given ids. func (ap *AccountProto) UpdateAccounts(ctx context.Context, update *account.AccountsRequest) (*account.AccountsResponse, error) { logger := pkg.NewLoggerFromContext(ctx) @@ -128,71 +205,31 @@ func (ap *AccountProto) UpdateAccounts(ctx context.Context, update *account.Acco ctx, span := tracer.Start(ctx, "handler.UpdateAccounts") defer span.End() - accounts := make([]*account.Account, 0) - uuids := make([]uuid.UUID, 0) - - for _, getacc := range update.GetAccounts() { - if pkg.IsNilOrEmpty(&getacc.Id) { - mopentelemetry.HandleSpanError(&span, "Failed to update Accounts because id is empty", nil) - - logger.Errorf("Failed to update Accounts because id is empty") - - return nil, pkg.ValidateBusinessError(constant.ErrNoAccountIDsProvided, reflect.TypeOf(mmodel.Account{}).Name()) - } - - balance := mmodel.Balance{ - Available: &getacc.Balance.Available, - OnHold: &getacc.Balance.OnHold, - Scale: &getacc.Balance.Scale, - } - - organizationUUID, err := uuid.Parse(getacc.GetOrganizationId()) - if err != nil { - return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), organizationUUID) - } - - ledgerUUID, err := uuid.Parse(getacc.GetLedgerId()) - if err != nil { - return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), ledgerUUID) - } - - accountUUID, err := uuid.Parse(getacc.GetId()) - if err != nil { - return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), accountUUID) - } - - _, err = ap.Command.UpdateAccountByID(ctx, organizationUUID, ledgerUUID, accountUUID, &balance) - if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to update balance in Account by id", err) - - logger.Errorf("Failed to update balance in Account by id for organizationId %s and ledgerId %s in grpc, Error: %s", getacc.OrganizationId, getacc.LedgerId, err.Error()) - - return nil, pkg.ValidateBusinessError(constant.ErrBalanceUpdateFailed, reflect.TypeOf(mmodel.Account{}).Name()) - } - - uuids = append(uuids, uuid.MustParse(getacc.Id)) + aliases := make([]string, 0) + for _, getAcc := range update.GetAccounts() { + uuids = append(uuids, uuid.MustParse(getAcc.Id)) + aliases = append(aliases, getAcc.Alias) } - organizationID := update.GetOrganizationId() - ledgerID := update.GetLedgerId() - - acc, err := ap.Command.ListAccountsByIDs(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuids) + organizationID, err := uuid.Parse(update.OrganizationId) if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to retrieve Accounts by ids for grpc", err) - - logger.Errorf("Failed to retrieve Accounts by ids for organizationId %s and ledgerId %s in grpc, Error: %s", organizationID, ledgerID, err.Error()) - - return nil, pkg.ValidateBusinessError(constant.ErrNoAccountsFound, reflect.TypeOf(mmodel.Account{}).Name()) + return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), organizationID) } - for _, ac := range acc { - accounts = append(accounts, ac.ToProto()) + ledgerID, err := uuid.Parse(update.LedgerId) + if err != nil { + return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), ledgerID) } - response := account.AccountsResponse{ - Accounts: accounts, + err = ap.Command.UpdateAccounts(ctx, organizationID, ledgerID, update.GetAccounts()) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to update balance in Account by id", err) + + logger.Errorf("Failed to update balance in Account by id for organizationId %v and ledgerId %v in grpc, Error: %v", organizationID, ledgerID, err.Error()) + + return nil, pkg.ValidateBusinessError(constant.ErrBalanceUpdateFailed, reflect.TypeOf(mmodel.Account{}).Name()) } - return &response, nil + return nil, nil } diff --git a/components/ledger/internal/adapters/postgres/account/account.postgresql.go b/components/ledger/internal/adapters/postgres/account/account.postgresql.go index 75b18679..284dfe4e 100644 --- a/components/ledger/internal/adapters/postgres/account/account.postgresql.go +++ b/components/ledger/internal/adapters/postgres/account/account.postgresql.go @@ -942,24 +942,23 @@ func (r *AccountPostgreSQLRepository) UpdateAccountByID(ctx context.Context, org return record.ToEntity(), nil } +// UpdateAccounts an update all Accounts entity by ID only into Postgresql. func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organizationID, ledgerID uuid.UUID, accounts []*account.Account) error { tracer := pkg.NewTracerFromContext(ctx) - ctx, span0 := tracer.Start(ctx, "postgres.update_accounts.get_db") - defer span0.End() + + ctx, span := tracer.Start(ctx, "postgres.update_accounts") + defer span.End() db, err := r.connection.GetDB() if err != nil { - mopentelemetry.HandleSpanError(&span0, "Failed to get database connection", err) + mopentelemetry.HandleSpanError(&span, "Failed to get database connection", err) return err } - ctx, span1 := tracer.Start(ctx, "postgres.update_accounts.begin") - defer span1.End() - tx, err := db.Begin() if err != nil { - mopentelemetry.HandleSpanError(&span1, "Failed to init transaction", err) + mopentelemetry.HandleSpanError(&span, "Failed to init transaction", err) return err } @@ -988,16 +987,16 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi _, err := tx.ExecContext(ctx, query, args...) if err != nil { - mopentelemetry.HandleSpanError(&span1, "Failed to update account", err) + mopentelemetry.HandleSpanError(&span, "Failed to update account", err) return err } } - if err := tx.Commit().Error; err != nil { + if err := tx.Commit(); err != nil { err := pkg.ValidateBusinessError(constant.ErrEntityNotFound, reflect.TypeOf(mmodel.Account{}).Name()) - mopentelemetry.HandleSpanError(&span1, "Failed to commit accounts", err) + mopentelemetry.HandleSpanError(&span, "Failed to commit accounts", err) return err } diff --git a/components/ledger/internal/services/command/update-account-id.go b/components/ledger/internal/services/command/update-account-id.go index 29650a9c..3f5f0b84 100644 --- a/components/ledger/internal/services/command/update-account-id.go +++ b/components/ledger/internal/services/command/update-account-id.go @@ -25,11 +25,11 @@ func (uc *UseCase) UpdateAccountByID(ctx context.Context, organizationID, ledger logger.Infof("Trying to update account by id: %v", id) - account := &mmodel.Account{ + acc := &mmodel.Account{ Balance: *balance, } - accountUpdated, err := uc.AccountRepo.UpdateAccountByID(ctx, organizationID, ledgerID, id, account) + accountUpdated, err := uc.AccountRepo.UpdateAccountByID(ctx, organizationID, ledgerID, id, acc) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to update account on repo by id", err) diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 8f1d7f09..53b9f4c7 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -418,7 +418,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L _, spanRaceCondition := tracer.Start(ctx, "handler.create_transaction.race_condition") - handler.Command.AllKeysUnlocked(ctx, organizationID, ledgerID, validate.Aliases) + handler.Command.AllKeysUnlocked(ctx, organizationID, ledgerID, validate.Aliases, hash) spanRaceCondition.End() @@ -505,13 +505,11 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L _, spanReleaseLock := tracer.Start(ctx, "handler.create_transaction.delete_race_condition") - handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases) + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) spanReleaseLock.End() ctxUpdateTransactionStatus, spanUpdateTransactionStatus := tracer.Start(ctx, "handler.create_transaction.update_transaction_status") - - //TODO: use event driven and broken and parts _, err = handler.Command.UpdateTransactionStatus(ctxUpdateTransactionStatus, organizationID, ledgerID, tran.IDtoUUID(), constant.APPROVED) if err != nil { mopentelemetry.HandleSpanError(&spanUpdateTransactionStatus, "Failed to update transaction status", err) @@ -525,7 +523,6 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L ctxGetTransaction, spanGetTransaction := tracer.Start(ctx, "handler.create_transaction.get_transaction") - //TODO: use event driven and broken and parts tran, err = handler.Query.GetTransactionByID(ctxGetTransaction, organizationID, ledgerID, tran.IDtoUUID()) if err != nil { mopentelemetry.HandleSpanError(&spanGetTransaction, "Failed to retrieve transaction", err) diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index e324d608..7b989449 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -2,16 +2,18 @@ package command import ( "context" + "errors" "github.com/LerianStudio/midaz/pkg" "github.com/LerianStudio/midaz/pkg/mopentelemetry" "github.com/google/uuid" + "github.com/redis/go-redis/v9" "sync" "time" ) const TimeSetLock = 1 -func (uc *UseCase) AllKeysUnlocked(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string) { +func (uc *UseCase) AllKeysUnlocked(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, hash string) { logger := pkg.NewLoggerFromContext(context.Background()) tracer := pkg.NewTracerFromContext(context.Background()) @@ -27,14 +29,14 @@ func (uc *UseCase) AllKeysUnlocked(ctx context.Context, organizationID, ledgerID logger.Infof("Account try to lock on redis: %v", internalKey) wg.Add(1) - go uc.checkAndReleaseLock(ctx, &wg, internalKey, resultChan) + go uc.checkAndReleaseLock(ctx, &wg, internalKey, hash, resultChan) } wg.Wait() close(resultChan) } -func (uc *UseCase) checkAndReleaseLock(ctx context.Context, wg *sync.WaitGroup, internalKey string, resultChan chan bool) { +func (uc *UseCase) checkAndReleaseLock(ctx context.Context, wg *sync.WaitGroup, internalKey, hash string, resultChan chan bool) { logger := pkg.NewLoggerFromContext(context.Background()) tracer := pkg.NewTracerFromContext(context.Background()) @@ -44,7 +46,7 @@ func (uc *UseCase) checkAndReleaseLock(ctx context.Context, wg *sync.WaitGroup, defer wg.Done() for { - success, err := uc.RedisRepo.SetNX(context.Background(), internalKey, "processing account...", TimeSetLock) + success, err := uc.RedisRepo.SetNX(context.Background(), internalKey, hash, TimeSetLock) if err != nil { resultChan <- false return @@ -57,11 +59,11 @@ func (uc *UseCase) checkAndReleaseLock(ctx context.Context, wg *sync.WaitGroup, return } - time.Sleep(250 * time.Millisecond) + time.Sleep(200 * time.Millisecond) } } -func (uc *UseCase) DeleteLocks(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string) { +func (uc *UseCase) DeleteLocks(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, hash string) { logger := pkg.NewLoggerFromContext(context.Background()) tracer := pkg.NewTracerFromContext(context.Background()) @@ -73,11 +75,14 @@ func (uc *UseCase) DeleteLocks(ctx context.Context, organizationID, ledgerID uui logger.Infof("Account releasing lock on redis: %v", internalKey) - err := uc.RedisRepo.Del(ctx, internalKey) - if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to release Accounts lock", err) + val, err := uc.RedisRepo.Get(ctx, key) + if !errors.Is(err, redis.Nil) && err != nil && val == hash { + err = uc.RedisRepo.Del(ctx, internalKey) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to release Accounts lock", err) - logger.Errorf("Failed to release Accounts lock: %v", err) + logger.Errorf("Failed to release Accounts lock: %v", err) + } } } } diff --git a/components/transaction/internal/services/command/update-transaction.go b/components/transaction/internal/services/command/update-transaction.go index dcca08b7..349bccd5 100644 --- a/components/transaction/internal/services/command/update-transaction.go +++ b/components/transaction/internal/services/command/update-transaction.go @@ -80,7 +80,7 @@ func (uc *UseCase) UpdateTransactionStatus(ctx context.Context, organizationID, Status: status, } - transUpdated, err := uc.TransactionRepo.Update(ctx, organizationID, ledgerID, transactionID, trans) + _, err := uc.TransactionRepo.Update(ctx, organizationID, ledgerID, transactionID, trans) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to update status transaction on repo by id", err) @@ -93,5 +93,5 @@ func (uc *UseCase) UpdateTransactionStatus(ctx context.Context, organizationID, return nil, err } - return transUpdated, nil + return nil, nil } From beb49216d8e7c7ddcc76a841c9c454304abd0e62 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Thu, 9 Jan 2025 17:51:24 +0100 Subject: [PATCH 12/49] feat: update time lock :sparkles: --- .../internal/services/command/create-lock-race-condition.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index 7b989449..9642aee1 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -11,7 +11,7 @@ import ( "time" ) -const TimeSetLock = 1 +const TimeSetLock = 3 func (uc *UseCase) AllKeysUnlocked(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, hash string) { logger := pkg.NewLoggerFromContext(context.Background()) From a2e8c99ec816149c23beb5962a600ca7d7bb0328 Mon Sep 17 00:00:00 2001 From: maxwelbm Date: Thu, 9 Jan 2025 15:13:41 -0300 Subject: [PATCH 13/49] feat: added router find account by alias :sparkles: --- components/ledger/api/docs.go | 62 +++++++++- components/ledger/api/openapi.yaml | 48 +++++++- components/ledger/api/swagger.json | 62 +++++++++- components/ledger/api/swagger.yaml | 42 ++++++- .../internal/adapters/http/in/account.go | 42 +++++++ .../internal/adapters/http/in/routes.go | 2 + .../adapters/postgres/account/account.mock.go | 19 ++- .../postgres/account/account.postgresql.go | 69 +++++++++++ .../services/query/get-alias-account.go | 54 +++++++++ .../services/query/get-alias-account_test.go | 112 ++++++++++++++++++ pkg/constant/errors.go | 1 + pkg/errors.go | 6 + 12 files changed, 496 insertions(+), 23 deletions(-) create mode 100644 components/ledger/internal/services/query/get-alias-account.go create mode 100644 components/ledger/internal/services/query/get-alias-account_test.go diff --git a/components/ledger/api/docs.go b/components/ledger/api/docs.go index 84168ed5..20d5b66e 100644 --- a/components/ledger/api/docs.go +++ b/components/ledger/api/docs.go @@ -680,12 +680,6 @@ const docTemplate = `{ "description": "Sort Order", "name": "sort_order", "in": "query" - }, - { - "type": "string", - "description": "Find alias", - "name": "alias", - "in": "query" } ], "responses": { @@ -778,6 +772,62 @@ const docTemplate = `{ } } }, + "/v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{alias}": { + "get": { + "description": "Get an Account with the input Alias", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Get an Account by Alias", + "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": "Alias", + "name": "alias", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Account" + } + } + } + } + }, "/v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{id}": { "get": { "description": "Get an Account with the input ID", diff --git a/components/ledger/api/openapi.yaml b/components/ledger/api/openapi.yaml index 7b89d7a1..4497b395 100644 --- a/components/ledger/api/openapi.yaml +++ b/components/ledger/api/openapi.yaml @@ -482,11 +482,6 @@ paths: - asc - desc type: string - - description: Find alias - in: query - name: alias - schema: - type: string responses: "200": content: @@ -541,6 +536,49 @@ paths: tags: - Accounts x-codegen-request-body-name: account + /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{alias}: + get: + description: Get an Account with the input Alias + parameters: + - description: Authorization Bearer Token + in: header + name: Authorization + required: true + schema: + type: string + - description: Request ID + in: header + name: Midaz-Id + schema: + type: string + - description: Organization ID + in: path + name: organization_id + required: true + schema: + type: string + - description: Ledger ID + in: path + name: ledger_id + required: true + schema: + type: string + - description: Alias + in: path + name: alias + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + description: OK + summary: Get an Account by Alias + tags: + - Accounts /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{id}: delete: description: Delete an Account with the input ID diff --git a/components/ledger/api/swagger.json b/components/ledger/api/swagger.json index 1bebc019..c6c18182 100644 --- a/components/ledger/api/swagger.json +++ b/components/ledger/api/swagger.json @@ -674,12 +674,6 @@ "description": "Sort Order", "name": "sort_order", "in": "query" - }, - { - "type": "string", - "description": "Find alias", - "name": "alias", - "in": "query" } ], "responses": { @@ -772,6 +766,62 @@ } } }, + "/v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{alias}": { + "get": { + "description": "Get an Account with the input Alias", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Get an Account by Alias", + "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": "Alias", + "name": "alias", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Account" + } + } + } + } + }, "/v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{id}": { "get": { "description": "Get an Account with the input ID", diff --git a/components/ledger/api/swagger.yaml b/components/ledger/api/swagger.yaml index 48d5818d..06e16ce5 100644 --- a/components/ledger/api/swagger.yaml +++ b/components/ledger/api/swagger.yaml @@ -986,10 +986,6 @@ paths: in: query name: sort_order type: string - - description: Find alias - in: query - name: alias - type: string produces: - application/json responses: @@ -1051,6 +1047,44 @@ paths: summary: Create an Account tags: - Accounts + /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{alias}: + get: + description: Get an Account with the input Alias + 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: Alias + in: path + name: alias + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/Account' + summary: Get an Account by Alias + tags: + - Accounts /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{id}: delete: description: Delete an Account with the input ID diff --git a/components/ledger/internal/adapters/http/in/account.go b/components/ledger/internal/adapters/http/in/account.go index 2a57fa34..9b1a2436 100644 --- a/components/ledger/internal/adapters/http/in/account.go +++ b/components/ledger/internal/adapters/http/in/account.go @@ -209,6 +209,48 @@ func (handler *AccountHandler) GetAccountByID(c *fiber.Ctx) error { return http.OK(c, account) } +// GetAccountByAlias is a method that retrieves Account information by a given account alias. +// +// @Summary Get an Account by Alias +// @Description Get an Account with the input Alias +// @Tags Accounts +// @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 alias path string true "Alias" +// @Success 200 {object} mmodel.Account +// @Router /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{alias} [get] +func (handler *AccountHandler) GetAccountByAlias(c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := pkg.NewLoggerFromContext(ctx) + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "handler.get_account_by_id") + defer span.End() + + organizationID := c.Locals("organization_id").(uuid.UUID) + ledgerID := c.Locals("ledger_id").(uuid.UUID) + alias := c.Params("alias") + + logger.Infof("Initiating retrieval of Account with Account Alias: %s", alias) + + account, err := handler.Query.GetAccountByAlias(ctx, organizationID, ledgerID, nil, alias) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to retrieve Account on query", err) + + logger.Errorf("Failed to retrieve Account with Account Alias: %s, Error: %s", alias, err.Error()) + + return http.WithError(c, err) + } + + logger.Infof("Successfully retrieved Account with Account Alias: %s", alias) + + return http.OK(c, account) +} + // UpdateAccount is a method that updates Account information. // // @Summary Update an Account diff --git a/components/ledger/internal/adapters/http/in/routes.go b/components/ledger/internal/adapters/http/in/routes.go index 17778c8c..b527c18b 100644 --- a/components/ledger/internal/adapters/http/in/routes.go +++ b/components/ledger/internal/adapters/http/in/routes.go @@ -65,6 +65,8 @@ func NewRouter(lg mlog.Logger, tl *mopentelemetry.Telemetry, cc *mcasdoor.Casdoo f.Patch("/v1/organizations/:organization_id/ledgers/:ledger_id/accounts/:id", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("account"), http.ParseUUIDPathParameters, http.WithBody(new(mmodel.UpdateAccountInput), ah.UpdateAccount)) f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/accounts", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("account"), http.ParseUUIDPathParameters, ah.GetAllAccounts) f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/accounts/:id", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("account"), http.ParseUUIDPathParameters, ah.GetAccountByID) + f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/accounts/alias/:alias", + jwt.ProtectHTTP(), jwt.WithPermissionHTTP("account"), http.ParseUUIDPathParameters, ah.GetAccountByAlias) f.Delete("/v1/organizations/:organization_id/ledgers/:ledger_id/accounts/:id", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("account"), http.ParseUUIDPathParameters, ah.DeleteAccountByID) // Will be deprecated in the future. Use "POST /v1/organizations/:organization_id/ledgers/:ledger_id/accounts" instead. f.Post("/v1/organizations/:organization_id/ledgers/:ledger_id/portfolios/:portfolio_id/accounts", jwt.ProtectHTTP(), jwt.WithPermissionHTTP("account"), http.ParseUUIDPathParameters, http.WithBody(new(mmodel.CreateAccountInput), ah.CreateAccountFromPortfolio)) diff --git a/components/ledger/internal/adapters/postgres/account/account.mock.go b/components/ledger/internal/adapters/postgres/account/account.mock.go index 0895340b..743eb60f 100644 --- a/components/ledger/internal/adapters/postgres/account/account.mock.go +++ b/components/ledger/internal/adapters/postgres/account/account.mock.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/LerianStudio/midaz/components/ledger/internal/adapters/postgres/account (interfaces: Repository) +// Source: /home/max/Workspace/midaz/components/ledger/internal/adapters/postgres/account/account.postgresql.go // // Generated by this command: // -// mockgen --destination=account.mock.go --package=account . Repository +// mockgen -source=/home/max/Workspace/midaz/components/ledger/internal/adapters/postgres/account/account.postgresql.go -destination=/home/max/Workspace/midaz/components/ledger/internal/adapters/postgres/account/account.postgresql_mock.go -package account // // Package account is a generated GoMock package. @@ -87,6 +87,21 @@ func (mr *MockRepositoryMockRecorder) Find(ctx, organizationID, ledgerID, portfo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockRepository)(nil).Find), ctx, organizationID, ledgerID, portfolioID, id) } +// FindAlias mocks base method. +func (m *MockRepository) FindAlias(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, alias string) (*mmodel.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAlias", ctx, organizationID, ledgerID, portfolioID, alias) + ret0, _ := ret[0].(*mmodel.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAlias indicates an expected call of FindAlias. +func (mr *MockRepositoryMockRecorder) FindAlias(ctx, organizationID, ledgerID, portfolioID, alias any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAlias", reflect.TypeOf((*MockRepository)(nil).FindAlias), ctx, organizationID, ledgerID, portfolioID, alias) +} + // FindAll mocks base method. func (m *MockRepository) FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, filter http.Pagination) ([]*mmodel.Account, error) { m.ctrl.T.Helper() diff --git a/components/ledger/internal/adapters/postgres/account/account.postgresql.go b/components/ledger/internal/adapters/postgres/account/account.postgresql.go index 9acf88e6..94c941e3 100644 --- a/components/ledger/internal/adapters/postgres/account/account.postgresql.go +++ b/components/ledger/internal/adapters/postgres/account/account.postgresql.go @@ -33,6 +33,7 @@ type Repository interface { FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, filter http.Pagination) ([]*mmodel.Account, error) Find(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, id uuid.UUID) (*mmodel.Account, error) FindWithDeleted(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, id uuid.UUID) (*mmodel.Account, error) + FindAlias(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, alias string) (*mmodel.Account, error) 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) @@ -378,6 +379,74 @@ func (r *AccountPostgreSQLRepository) FindWithDeleted(ctx context.Context, organ return account.ToEntity(), nil } +// FindAlias retrieves an Account entity from the database using the provided Alias (including soft-deleted ones). +func (r *AccountPostgreSQLRepository) FindAlias(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, alias string) (*mmodel.Account, error) { + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "postgres.find_alias") + defer span.End() + + db, err := r.connection.GetDB() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get database connection", err) + + return nil, err + } + + query := "SELECT * FROM account WHERE organization_id = $1 AND ledger_id = $2 AND alias = $3" + args := []any{organizationID, ledgerID, alias} + + if portfolioID != nil && *portfolioID != uuid.Nil { + query += " AND portfolio_id = $4" + + args = append(args, portfolioID) + } + + query += " ORDER BY created_at DESC" + + account := &AccountPostgreSQLModel{} + + ctx, spanQuery := tracer.Start(ctx, "postgres.find_with_deleted.query") + + row := db.QueryRowContext(ctx, query, args...) + + spanQuery.End() + + if err := row.Scan( + &account.ID, + &account.Name, + &account.ParentAccountID, + &account.EntityID, + &account.AssetCode, + &account.OrganizationID, + &account.LedgerID, + &account.PortfolioID, + &account.ProductID, + &account.AvailableBalance, + &account.OnHoldBalance, + &account.BalanceScale, + &account.Status, + &account.StatusDescription, + &account.AllowSending, + &account.AllowReceiving, + &account.Alias, + &account.Type, + &account.CreatedAt, + &account.UpdatedAt, + &account.DeletedAt, + ); err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to scan row", err) + + if errors.Is(err, sql.ErrNoRows) { + return nil, pkg.ValidateBusinessError(constant.ErrAccountAliasNotFound, reflect.TypeOf(mmodel.Account{}).Name()) + } + + return nil, err + } + + return account.ToEntity(), nil +} + // FindByAlias find account from the database using Organization and Ledger id and Alias. Returns true and ErrAliasUnavailability error if the alias is already taken. func (r *AccountPostgreSQLRepository) FindByAlias(ctx context.Context, organizationID, ledgerID uuid.UUID, alias string) (bool, error) { tracer := pkg.NewTracerFromContext(ctx) diff --git a/components/ledger/internal/services/query/get-alias-account.go b/components/ledger/internal/services/query/get-alias-account.go new file mode 100644 index 00000000..0b411269 --- /dev/null +++ b/components/ledger/internal/services/query/get-alias-account.go @@ -0,0 +1,54 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/components/ledger/internal/services" + "github.com/LerianStudio/midaz/pkg" + "github.com/LerianStudio/midaz/pkg/constant" + "github.com/LerianStudio/midaz/pkg/mmodel" + "github.com/LerianStudio/midaz/pkg/mopentelemetry" + "github.com/google/uuid" +) + +// GetAccountByAlias get an Account from the repository by given alias (including soft-deleted ones). +func (uc *UseCase) GetAccountByAlias(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, alias string) (*mmodel.Account, error) { + logger := pkg.NewLoggerFromContext(ctx) + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "query.get_account_by_alias") + + logger.Infof("Retrieving account for alias: %s", alias) + + account, err := uc.AccountRepo.FindAlias(ctx, organizationID, ledgerID, portfolioID, alias) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get account on repo by alias", err) + + logger.Errorf("Error getting account on repo by alias: %v", err) + + if errors.Is(err, services.ErrDatabaseItemNotFound) { + return nil, pkg.ValidateBusinessError(constant.ErrAccountAliasNotFound, reflect.TypeOf(mmodel.Account{}).Name()) + } + + return nil, err + } + + if account != nil { + metadata, err := uc.MetadataRepo.FindByEntity(ctx, reflect.TypeOf(mmodel.Account{}).Name(), alias) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get metadata on mongodb account", err) + + logger.Errorf("Error get metadata on mongodb account: %v", err) + + return nil, err + } + + if metadata != nil { + account.Metadata = metadata.Data + } + } + + return account, nil +} diff --git a/components/ledger/internal/services/query/get-alias-account_test.go b/components/ledger/internal/services/query/get-alias-account_test.go new file mode 100644 index 00000000..963fe2b8 --- /dev/null +++ b/components/ledger/internal/services/query/get-alias-account_test.go @@ -0,0 +1,112 @@ +package query + +import ( + "context" + "errors" + "testing" + + "github.com/LerianStudio/midaz/components/ledger/internal/adapters/mongodb" + "github.com/LerianStudio/midaz/components/ledger/internal/adapters/postgres/account" + "github.com/LerianStudio/midaz/components/ledger/internal/services" + "github.com/LerianStudio/midaz/pkg/mmodel" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestUseCase_GetAccountByAlias(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockAccountRepo := account.NewMockRepository(ctrl) + mockMetadataRepo := mongodb.NewMockRepository(ctrl) + + uc := &UseCase{ + AccountRepo: mockAccountRepo, + MetadataRepo: mockMetadataRepo, + } + + tests := []struct { + name string + organizationID uuid.UUID + ledgerID uuid.UUID + portfolioID *uuid.UUID + alias string + mockSetup func() + expectErr bool + expectedResult *mmodel.Account + }{ + { + name: "Success - Retrieve account with metadata", + organizationID: uuid.New(), + ledgerID: uuid.New(), + portfolioID: nil, + alias: "case01", + mockSetup: func() { + accountID := uuid.New() + mockAccountRepo.EXPECT(). + FindAlias(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(&mmodel.Account{ID: accountID.String(), Name: "Test Account", Status: mmodel.Status{Code: "active"}}, nil) + mockMetadataRepo.EXPECT(). + FindByEntity(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&mongodb.Metadata{Data: map[string]any{"key": "value"}}, nil) + }, + expectErr: false, + expectedResult: &mmodel.Account{ + ID: "valid-uuid", + Name: "Test Account", + Status: mmodel.Status{Code: "active"}, + Metadata: map[string]any{"key": "value"}, + }, + }, + { + name: "Error - Account not found", + organizationID: uuid.New(), + ledgerID: uuid.New(), + portfolioID: nil, + alias: "case02", + mockSetup: func() { + mockAccountRepo.EXPECT(). + FindAlias(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, services.ErrDatabaseItemNotFound) + }, + expectErr: true, + expectedResult: nil, + }, + { + name: "Error - Failed to retrieve metadata", + organizationID: uuid.New(), + ledgerID: uuid.New(), + portfolioID: nil, + alias: "case03", + mockSetup: func() { + accountID := uuid.New() + mockAccountRepo.EXPECT(). + FindAlias(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(&mmodel.Account{ID: accountID.String(), Name: "Test Account", Status: mmodel.Status{Code: "active"}}, nil) + mockMetadataRepo.EXPECT(). + FindByEntity(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, errors.New("metadata retrieval error")) + }, + expectErr: true, + expectedResult: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.mockSetup() + + ctx := context.Background() + result, err := uc.GetAccountByAlias(ctx, tt.organizationID, tt.ledgerID, tt.portfolioID, tt.alias) + + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + } + }) + } +} diff --git a/pkg/constant/errors.go b/pkg/constant/errors.go index 8dc2d27a..3a4d10b9 100644 --- a/pkg/constant/errors.go +++ b/pkg/constant/errors.go @@ -92,4 +92,5 @@ var ( ErrInvalidQueryParameter = errors.New("0082") ErrInvalidDateRange = errors.New("0083") ErrIdempotencyKey = errors.New("0084") + ErrAccountAliasNotFound = errors.New("0085") ) diff --git a/pkg/errors.go b/pkg/errors.go index dc418467..ef1fabc9 100644 --- a/pkg/errors.go +++ b/pkg/errors.go @@ -783,6 +783,12 @@ func ValidateBusinessError(err error, entityType string, args ...any) error { Title: "Duplicate Idempotency Key", Message: fmt.Sprintf("The idempotency key %v is already in use. Please provide a unique key and try again.", args), }, + constant.ErrAccountAliasNotFound: ValidationError{ + EntityType: entityType, + Code: constant.ErrAccountAliasNotFound.Error(), + Title: "Account Alias Not Found", + Message: "The provided account Alias does not exist in our records. Please verify the account Alias and try again.", + }, } if mappedError, found := errorMap[err]; found { From 6964ee7dff82294b0010ea357bb3e8fc12d43dc2 Mon Sep 17 00:00:00 2001 From: lerian-studio Date: Thu, 9 Jan 2025 18:16:25 +0000 Subject: [PATCH 14/49] chore(release): update version in .env.example to v1.44.1 --- chocolatey/tools/chocolateyinstall.ps1 | 2 +- components/audit/.env.example | 2 +- components/ledger/.env.example | 2 +- components/mdz/.env.example | 2 +- components/transaction/.env.example | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chocolatey/tools/chocolateyinstall.ps1 b/chocolatey/tools/chocolateyinstall.ps1 index b642f442..9f68bbc5 100644 --- a/chocolatey/tools/chocolateyinstall.ps1 +++ b/chocolatey/tools/chocolateyinstall.ps1 @@ -1,4 +1,4 @@ -$version = 'v1.44.0' +$version = 'v1.44.1' $ErrorActionPreference = 'Stop'; diff --git a/components/audit/.env.example b/components/audit/.env.example index e487209f..59735e70 100644 --- a/components/audit/.env.example +++ b/components/audit/.env.example @@ -2,7 +2,7 @@ # ENV_NAME=production # APP -VERSION=v1.44.0 +VERSION=v1.44.1 SERVER_PORT=3005 SERVER_ADDRESS=:${SERVER_PORT} diff --git a/components/ledger/.env.example b/components/ledger/.env.example index 3149fb88..48bb6090 100644 --- a/components/ledger/.env.example +++ b/components/ledger/.env.example @@ -4,7 +4,7 @@ # ENV_NAME=production # APP -VERSION=v1.44.0 +VERSION=v1.44.1 SERVER_PORT=3000 SERVER_ADDRESS=:${SERVER_PORT} diff --git a/components/mdz/.env.example b/components/mdz/.env.example index 5da79e4e..cfc5d36e 100644 --- a/components/mdz/.env.example +++ b/components/mdz/.env.example @@ -2,4 +2,4 @@ CLIENT_ID=9670e0ca55a29a466d31 CLIENT_SECRET=dd03f916cacf4a98c6a413d9c38ba102dce436a9 URL_API_AUTH=http://127.0.0.1:8080 URL_API_LEDGER=http://127.0.0.1:3000 -VERSION=v1.44.0 +VERSION=v1.44.1 diff --git a/components/transaction/.env.example b/components/transaction/.env.example index ce0b4635..2025762d 100644 --- a/components/transaction/.env.example +++ b/components/transaction/.env.example @@ -4,7 +4,7 @@ # ENV_NAME=production # APP -VERSION=v1.44.0 +VERSION=v1.44.1 APP_CONTEXT=/transaction/v1 SERVER_PORT=3002 SERVER_ADDRESS=:${SERVER_PORT} From 4d19380685d0bad020ed4f0b67e73dcac372876e Mon Sep 17 00:00:00 2001 From: maxwelbm Date: Thu, 9 Jan 2025 15:36:42 -0300 Subject: [PATCH 15/49] feat: push choco :sparkles: --- .github/workflows/packages-manager.yml | 55 +++++++++----------------- chocolatey/mdz.nuspec | 6 ++- chocolatey/tools/VERIFICATION.txt | 5 ++- chocolatey/tools/chocolateyinstall.ps1 | 2 +- 4 files changed, 28 insertions(+), 40 deletions(-) diff --git a/.github/workflows/packages-manager.yml b/.github/workflows/packages-manager.yml index 8baed628..71cc8e07 100644 --- a/.github/workflows/packages-manager.yml +++ b/.github/workflows/packages-manager.yml @@ -61,33 +61,13 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Set up Chocolatey - run: | - Set-ExecutionPolicy Bypass -Scope Process -Force - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 - Invoke-WebRequest https://chocolatey.org/install.ps1 -UseBasicParsing | Invoke-Expression - shell: pwsh - - - name: Update nuspec version - run: | - $nuspecPath = Resolve-Path .\chocolatey\mdz.nuspec - - if (-Not (Test-Path $nuspecPath)) { - Write-Error "The nuspec file was not found at $nuspecPath" - exit 1 - } - - Write-Host "Updating nuspec version to ${{ needs.get_branch.outputs.version }}" - (Get-Content $nuspecPath) -replace '.*', "${{ needs.get_branch.outputs.version }}" | Set-Content $nuspecPath - shell: pwsh - - name: Download and extract ZIP run: | $toolsDir = "$(Resolve-Path .\chocolatey\tools)" New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null $zipFile = Join-Path $toolsDir 'mdz.zip' $outputFile = Join-Path $toolsDir 'mdz.exe' - + $url = "https://github.com/LerianStudio/midaz/releases/download/v${{ needs.get_branch.outputs.version }}/midaz_${{ needs.get_branch.outputs.version }}_windows_amd64.zip" Write-Host "Downloading ZIP from $url to $zipFile" Invoke-WebRequest -Uri $url -OutFile $zipFile @@ -96,24 +76,26 @@ jobs: Expand-Archive -Path $zipFile -DestinationPath $toolsDir -Force shell: pwsh - - name: Calculate checksum - id: calculate-checksum + - name: Prepare run: | - $outputFile = "$(Resolve-Path .\chocolatey\tools\mdz.exe)" - $checksum = (Get-FileHash -Path $outputFile -Algorithm SHA256).Hash + # Checksum Calculate + $zipFile = "$(Resolve-Path .\chocolatey\tools\mdz.zip)" + $checksum = (Get-FileHash -Path $zipFile -Algorithm SHA256).Hash - echo "checksum=$checksum" >> $GITHUB_OUTPUT - shell: pwsh + Remove-Item $zipFile - - name: Replace checksum in chocolateyinstall.ps1 and VERIFICATION.txt - run: | - (Get-Content .\chocolatey\tools\chocolateyinstall.ps1) ` - -replace '{{CHECKSUM}}', '${{ steps.calculate-checksum.outputs.checksum }}' ` - | Set-Content .\chocolatey\tools\chocolateyinstall.ps1 + Write-Host "Updating nuspec version to ${{ needs.get_branch.outputs.version }}" + $nuspecPath = Resolve-Path .\chocolatey\mdz.nuspec + (Get-Content $nuspecPath) -replace '.*', "${{ needs.get_branch.outputs.version }}" | Set-Content $nuspecPath + + Write-Host "Updating Checksum files $checksum" + $chocoInstallPath = Resolve-Path .\chocolatey\tools\chocolateyinstall.ps1 + (Get-Content $chocoInstallPath) -replace '{{CHECKSUM}}', "$checksum" | Set-Content $chocoInstallPath + + $verificationPath = Resolve-Path .\chocolatey\tools\VERIFICATION.txt + (Get-Content $verificationPath) -replace '{{CHECKSUM}}', "$checksum" | Set-Content $verificationPath - (Get-Content .\chocolatey\tools\VERIFICATION.txt) ` - -replace '{{CHECKSUM}}', '${{ steps.calculate-checksum.outputs.checksum }}' ` - | Set-Content .\chocolatey\tools\VERIFICATION.txt + (Get-Content $verificationPath) -replace '{{VERSION}}', "${{ needs.get_branch.outputs.version }}" -replace '{{VERSION}}', "${{ needs.get_branch.outputs.version }}" | Set-Content $verificationPath shell: pwsh - name: Publish Chocolatey package @@ -122,12 +104,11 @@ jobs: shell: pwsh run: | choco pack chocolatey/mdz.nuspec - ls # install local test choco install mdz --version=${{ needs.get_branch.outputs.version }} --prerelease --source="D:\a\midaz\midaz" mdz version - + choco apikey add -s="https://push.chocolatey.org/" -k="$env:CHOCO_API_KEY" choco push mdz.${{ needs.get_branch.outputs.version }}.nupkg --source https://push.chocolatey.org/ diff --git a/chocolatey/mdz.nuspec b/chocolatey/mdz.nuspec index b99cdb2c..d2bd796f 100644 --- a/chocolatey/mdz.nuspec +++ b/chocolatey/mdz.nuspec @@ -2,12 +2,16 @@ mdz - 2.0.0 + 1.0.0 mdz Lerian Studio https://github.com/LerianStudio/midaz https://github.com/LerianStudio/midaz/blob/main/LICENSE https://avatars.githubusercontent.com/u/148895005 + https://docs.lerian.studio/docs/midaz-cli + https://discord.gg/DnhqKwkGv3 + https://github.com/LerianStudio/midaz/issues + https://github.com/LerianStudio/midaz mdz cli ledger golang financial An open-source ledger for multi-asset, multi-currency financial systems. diff --git a/chocolatey/tools/VERIFICATION.txt b/chocolatey/tools/VERIFICATION.txt index 70e5e75d..571e8fd3 100644 --- a/chocolatey/tools/VERIFICATION.txt +++ b/chocolatey/tools/VERIFICATION.txt @@ -10,4 +10,7 @@ https://github.com/LerianStudio/midaz/releases Download the zipped application `windows_amd64.zip` from the github release tab -It's hash should match: "{{CHECKSUM}}" \ No newline at end of file +Inside the zip is an mdz.exe file +Downloaded from: https://github.com/LerianStudio/midaz/releases/download/v{{VERSION}}/midaz_{{VERSION}}_windows_amd64.zip + +It's hash should match SHA256: "{{CHECKSUM}}" diff --git a/chocolatey/tools/chocolateyinstall.ps1 b/chocolatey/tools/chocolateyinstall.ps1 index 9f68bbc5..b817fc46 100644 --- a/chocolatey/tools/chocolateyinstall.ps1 +++ b/chocolatey/tools/chocolateyinstall.ps1 @@ -41,4 +41,4 @@ Write-Host "Copying $outputFile to $binDir" Copy-Item -Path $outputFile -Destination $binDir -Force # Confirm installation -Write-Host "Installation complete. The mdz executable is available globally. +Write-Host "Installation complete. The mdz executable is available globally." From e252b4282e3263496da2fa1ed40140f622763908 Mon Sep 17 00:00:00 2001 From: lerian-studio Date: Thu, 9 Jan 2025 19:09:31 +0000 Subject: [PATCH 16/49] chore(release): 1.45.0-beta.1 ## [1.45.0-beta.1](https://github.com/LerianStudio/midaz/compare/v1.44.1-beta.1...v1.45.0-beta.1) (2025-01-09) ### Features * added router find account by alias :sparkles: ([a2e8c99](https://github.com/LerianStudio/midaz/commit/a2e8c99ec816149c23beb5962a600ca7d7bb0328)) * push choco :sparkles: ([4d19380](https://github.com/LerianStudio/midaz/commit/4d19380685d0bad020ed4f0b67e73dcac372876e)) --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c52d1834..c08ac2ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [1.45.0-beta.1](https://github.com/LerianStudio/midaz/compare/v1.44.1-beta.1...v1.45.0-beta.1) (2025-01-09) + + +### Features + +* added router find account by alias :sparkles: ([a2e8c99](https://github.com/LerianStudio/midaz/commit/a2e8c99ec816149c23beb5962a600ca7d7bb0328)) +* push choco :sparkles: ([4d19380](https://github.com/LerianStudio/midaz/commit/4d19380685d0bad020ed4f0b67e73dcac372876e)) + ## [1.44.1-beta.1](https://github.com/LerianStudio/midaz/compare/v1.44.0...v1.44.1-beta.1) (2025-01-08) ## [1.44.0](https://github.com/LerianStudio/midaz/compare/v1.43.0...v1.44.0) (2025-01-08) From 97448dc69d6bcfafe66bbd94d81cff8b4733da3e Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Fri, 10 Jan 2025 12:00:09 +0100 Subject: [PATCH 17/49] feature: new updates to avoid race condition --- .../internal/adapters/http/in/transaction.go | 188 +++++++----------- .../command/create-lock-race-condition.go | 46 ++++- .../command/update-accounts-ledger-grpc.go | 65 ++++++ .../query/get-accounts-ledger-grpc.go | 58 ++++++ 4 files changed, 235 insertions(+), 122 deletions(-) create mode 100644 components/transaction/internal/services/command/update-accounts-ledger-grpc.go create mode 100644 components/transaction/internal/services/query/get-accounts-ledger-grpc.go diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 53b9f4c7..7febcff4 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -466,6 +466,39 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L spanCreateTransaction.End() + ctxProcessAccounts, spanUpdateAccounts := tracer.Start(ctx, "handler.create_transaction.update_accounts") + + err = mopentelemetry.SetSpanAttributesFromStruct(&spanUpdateAccounts, "payload_handler_update_accounts", accounts) + if err != nil { + mopentelemetry.HandleSpanError(&spanUpdateAccounts, "Failed to convert accounts from struct to JSON string", err) + } + + err = handler.Command.UpdateAccounts(ctxProcessAccounts, logger, *validate, token, organizationID, ledgerID, accounts) + if err != nil { + mopentelemetry.HandleSpanError(&spanUpdateAccounts, "Failed to update accounts", err) + + _, spanReleaseLock := tracer.Start(ctx, "handler.update_accounts.delete_race_condition") + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) + + spanReleaseLock.End() + + ctxUpdateTransactionStatus, spanUpdateTransactionStatus := tracer.Start(ctx, "handler.update_accounts.update_transaction_status") + _, err = handler.Command.UpdateTransactionStatus(ctxUpdateTransactionStatus, organizationID, ledgerID, tran.IDtoUUID(), constant.DECLINED) + if err != nil { + mopentelemetry.HandleSpanError(&spanUpdateTransactionStatus, "Failed to update transaction status", err) + + logger.Errorf("Failed to update Transaction with ID: %s, Error: %s", tran.ID, err.Error()) + + return http.WithError(c, err) + } + + spanUpdateTransactionStatus.End() + + return http.WithError(c, err) + } + + spanUpdateAccounts.End() + e := make(chan error) result := make(chan []*operation.Operation) @@ -487,28 +520,6 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L spanCreateOperation.End() - ctxProcessAccounts, spanProcessAccounts := tracer.Start(ctx, "handler.create_transaction.process_accounts") - - err = mopentelemetry.SetSpanAttributesFromStruct(&spanProcessAccounts, "payload_handler_process_accounts", accounts) - if err != nil { - mopentelemetry.HandleSpanError(&spanProcessAccounts, "Failed to convert accounts from struct to JSON string", err) - } - - err = handler.processAccounts(ctxProcessAccounts, logger, *validate, token, organizationID, ledgerID, accounts) - if err != nil { - mopentelemetry.HandleSpanError(&spanProcessAccounts, "Failed to process accounts", err) - - return http.WithError(c, err) - } - - spanProcessAccounts.End() - - _, spanReleaseLock := tracer.Start(ctx, "handler.create_transaction.delete_race_condition") - - handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) - - spanReleaseLock.End() - ctxUpdateTransactionStatus, spanUpdateTransactionStatus := tracer.Start(ctx, "handler.create_transaction.update_transaction_status") _, err = handler.Command.UpdateTransactionStatus(ctxUpdateTransactionStatus, organizationID, ledgerID, tran.IDtoUUID(), constant.APPROVED) if err != nil { @@ -540,11 +551,45 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L logger.Infof("Successfully updated Transaction with Organization ID: %s, Ledger ID: %s and ID: %s", organizationID.String(), ledgerID.String(), tran.ID) + _, spanReleaseLock := tracer.Start(ctx, "handler.create_transaction.delete_race_condition") + + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) + + spanReleaseLock.End() + go handler.logTransaction(ctx, operations, organizationID, ledgerID, tran.IDtoUUID()) return http.Created(c, tran) } +// getAccounts is a function that split aliases and ids, call the properly function and return Accounts +func (handler *TransactionHandler) getAccounts(ctx context.Context, logger mlog.Logger, token string, organizationID, ledgerID uuid.UUID, input []string) ([]*account.Account, error) { + span := trace.SpanFromContext(ctx) + defer span.End() + + accounts, err := handler.Query.GetAccountsLedger(ctx, logger, token, organizationID, ledgerID, input) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get accounts", err) + + return nil, err + } + + searchAgain, err := handler.Command.LockBalanceVersion(ctx, organizationID, ledgerID, input, accounts) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get account by alias gRPC on Ledger", err) + + logger.Error("Failed to get account by alias gRPC on Ledger", err.Error()) + + return nil, err + } + + if searchAgain { + return handler.getAccounts(ctx, logger, token, organizationID, ledgerID, input) + } + + return accounts, nil +} + // logTransaction creates a message representing a transaction log and sends to auditing exchange func (handler *TransactionHandler) logTransaction(ctx context.Context, operations []*operation.Operation, organizationID uuid.UUID, ledgerID uuid.UUID, transactionID uuid.UUID) { logger := pkg.NewLoggerFromContext(ctx) @@ -591,105 +636,6 @@ func (handler *TransactionHandler) logTransaction(ctx context.Context, operation } } -// getAccounts is a function that split aliases and ids, call the properly function and return Accounts -func (handler *TransactionHandler) getAccounts(ctx context.Context, logger mlog.Logger, token string, organizationID, ledgerID uuid.UUID, input []string) ([]*account.Account, error) { - span := trace.SpanFromContext(ctx) - - var ids []string - - var aliases []string - - for _, item := range input { - if pkg.IsUUID(item) { - ids = append(ids, item) - } else { - aliases = append(aliases, item) - } - } - - var accounts []*account.Account - - if len(ids) > 0 { - gRPCAccounts, err := handler.Query.AccountGRPCRepo.GetAccountsByIds(ctx, token, organizationID, ledgerID, ids) - if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to get account by ids gRPC on Ledger", err) - - logger.Error("Failed to get account gRPC by ids on Ledger", err.Error()) - - return nil, err - } - - accounts = append(accounts, gRPCAccounts.GetAccounts()...) - } - - if len(aliases) > 0 { - gRPCAccounts, err := handler.Query.AccountGRPCRepo.GetAccountsByAlias(ctx, token, organizationID, ledgerID, aliases) - if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to get account by alias gRPC on Ledger", err) - - logger.Error("Failed to get account by alias gRPC on Ledger", err.Error()) - - return nil, err - } - - accounts = append(accounts, gRPCAccounts.GetAccounts()...) - } - - return accounts, nil -} - -// processAccounts is a function that adjust balance on Accounts -func (handler *TransactionHandler) processAccounts(ctx context.Context, logger mlog.Logger, validate goldModel.Responses, token string, organizationID, ledgerID uuid.UUID, accounts []*account.Account) error { - span := trace.SpanFromContext(ctx) - - e := make(chan error) - result := make(chan []*account.Account) - - var accountsToUpdate []*account.Account - - go goldModel.UpdateAccounts(constant.DEBIT, validate.From, accounts, result, e) - select { - case r := <-result: - accountsToUpdate = append(accountsToUpdate, r...) - case err := <-e: - mopentelemetry.HandleSpanError(&span, "Failed to update debit accounts", err) - - return err - } - - go goldModel.UpdateAccounts(constant.CREDIT, validate.To, accounts, result, e) - select { - case r := <-result: - accountsToUpdate = append(accountsToUpdate, r...) - case err := <-e: - mopentelemetry.HandleSpanError(&span, "Failed to update credit accounts", err) - - return err - } - - err := mopentelemetry.SetSpanAttributesFromStruct(&span, "payload_grpc_update_accounts", accountsToUpdate) - if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to convert accountsToUpdate from struct to JSON string", err) - - return err - } - - acc, err := handler.Command.AccountGRPCRepo.UpdateAccounts(ctx, token, organizationID, ledgerID, accountsToUpdate) - if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to update accounts gRPC on Ledger", err) - - logger.Error("Failed to update accounts gRPC on Ledger", err.Error()) - - return err - } - - for _, a := range acc.Accounts { - logger.Infof(a.UpdatedAt) - } - - return nil -} - func isAuditLogEnabled() bool { envValue := strings.ToLower(strings.TrimSpace(os.Getenv("AUDIT_LOG_ENABLED"))) return envValue != "false" diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index 9642aee1..0d367aea 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -4,14 +4,17 @@ import ( "context" "errors" "github.com/LerianStudio/midaz/pkg" + "github.com/LerianStudio/midaz/pkg/mgrpc/account" "github.com/LerianStudio/midaz/pkg/mopentelemetry" "github.com/google/uuid" "github.com/redis/go-redis/v9" + "strconv" "sync" "time" ) -const TimeSetLock = 3 +const TimeSetLock = 1 +const TimeSetLockBalance = 60 func (uc *UseCase) AllKeysUnlocked(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, hash string) { logger := pkg.NewLoggerFromContext(context.Background()) @@ -86,3 +89,44 @@ func (uc *UseCase) DeleteLocks(ctx context.Context, organizationID, ledgerID uui } } } + +func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, accounts []*account.Account) (bool, error) { + logger := pkg.NewLoggerFromContext(context.Background()) + tracer := pkg.NewTracerFromContext(context.Background()) + + ctx, span := tracer.Start(ctx, "redis.lock_balance_version") + defer span.End() + + accountsMap := make(map[string]*account.Account) + for _, acc := range accounts { + accountsMap[acc.Id] = acc + accountsMap[acc.Alias] = acc + } + + for _, key := range keys { + if acc, exists := accountsMap[key]; exists { + balanceAvailable := strconv.FormatFloat(acc.Balance.Available, 'f', -1, 64) + + internalKey := pkg.LockInternalKey(organizationID, ledgerID, key) + ":" + balanceAvailable + + logger.Infof("Account balance version releasing lock on redis: %v", internalKey) + + success, err := uc.RedisRepo.SetNX(context.Background(), internalKey, "lock balance version...", TimeSetLockBalance) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to lock Account balance version: ", err) + + logger.Errorf("Failed to lock Account balance version: %v", err) + + return false, err + } + + if !success { + logger.Infof("Lock already exists for key, get Accounts again: %v", internalKey) + + return true, nil + } + } + } + + return false, nil +} diff --git a/components/transaction/internal/services/command/update-accounts-ledger-grpc.go b/components/transaction/internal/services/command/update-accounts-ledger-grpc.go new file mode 100644 index 00000000..e9fae267 --- /dev/null +++ b/components/transaction/internal/services/command/update-accounts-ledger-grpc.go @@ -0,0 +1,65 @@ +package command + +import ( + "context" + + "github.com/LerianStudio/midaz/pkg/constant" + goldModel "github.com/LerianStudio/midaz/pkg/gold/transaction/model" + "github.com/LerianStudio/midaz/pkg/mgrpc/account" + "github.com/LerianStudio/midaz/pkg/mlog" + "github.com/LerianStudio/midaz/pkg/mopentelemetry" + "github.com/google/uuid" + "go.opentelemetry.io/otel/trace" +) + +// UpdateAccounts methods that is responsible to update accounts on ledger by gRpc. +func (uc *UseCase) UpdateAccounts(ctx context.Context, logger mlog.Logger, validate goldModel.Responses, token string, organizationID, ledgerID uuid.UUID, accounts []*account.Account) error { + span := trace.SpanFromContext(ctx) + + e := make(chan error) + result := make(chan []*account.Account) + + var accountsToUpdate []*account.Account + + go goldModel.UpdateAccounts(constant.DEBIT, validate.From, accounts, result, e) + select { + case r := <-result: + accountsToUpdate = append(accountsToUpdate, r...) + case err := <-e: + mopentelemetry.HandleSpanError(&span, "Failed to update debit accounts", err) + + return err + } + + go goldModel.UpdateAccounts(constant.CREDIT, validate.To, accounts, result, e) + select { + case r := <-result: + accountsToUpdate = append(accountsToUpdate, r...) + case err := <-e: + mopentelemetry.HandleSpanError(&span, "Failed to update credit accounts", err) + + return err + } + + err := mopentelemetry.SetSpanAttributesFromStruct(&span, "payload_grpc_update_accounts", accountsToUpdate) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to convert accountsToUpdate from struct to JSON string", err) + + return err + } + + acc, err := uc.AccountGRPCRepo.UpdateAccounts(ctx, token, organizationID, ledgerID, accountsToUpdate) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to update accounts gRPC on Ledger", err) + + logger.Error("Failed to update accounts gRPC on Ledger", err.Error()) + + return err + } + + for _, a := range acc.Accounts { + logger.Infof(a.UpdatedAt) + } + + return nil +} diff --git a/components/transaction/internal/services/query/get-accounts-ledger-grpc.go b/components/transaction/internal/services/query/get-accounts-ledger-grpc.go new file mode 100644 index 00000000..766de9f9 --- /dev/null +++ b/components/transaction/internal/services/query/get-accounts-ledger-grpc.go @@ -0,0 +1,58 @@ +package query + +import ( + "context" + "github.com/LerianStudio/midaz/pkg" + "github.com/LerianStudio/midaz/pkg/mgrpc/account" + "github.com/LerianStudio/midaz/pkg/mlog" + "github.com/LerianStudio/midaz/pkg/mopentelemetry" + "github.com/google/uuid" + "go.opentelemetry.io/otel/trace" +) + +// GetAccountsLedger methods responsible to get accounts on ledger by gRpc. +func (uc *UseCase) GetAccountsLedger(ctx context.Context, logger mlog.Logger, token string, organizationID, ledgerID uuid.UUID, input []string) ([]*account.Account, error) { + span := trace.SpanFromContext(ctx) + + var ids []string + + var aliases []string + + for _, item := range input { + if pkg.IsUUID(item) { + ids = append(ids, item) + } else { + aliases = append(aliases, item) + } + } + + var accounts []*account.Account + + if len(ids) > 0 { + gRPCAccounts, err := uc.AccountGRPCRepo.GetAccountsByIds(ctx, token, organizationID, ledgerID, ids) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get account by ids gRPC on Ledger", err) + + logger.Error("Failed to get account gRPC by ids on Ledger", err.Error()) + + return nil, err + } + + accounts = append(accounts, gRPCAccounts.GetAccounts()...) + } + + if len(aliases) > 0 { + gRPCAccounts, err := uc.AccountGRPCRepo.GetAccountsByAlias(ctx, token, organizationID, ledgerID, aliases) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get account by alias gRPC on Ledger", err) + + logger.Error("Failed to get account by alias gRPC on Ledger", err.Error()) + + return nil, err + } + + accounts = append(accounts, gRPCAccounts.GetAccounts()...) + } + + return accounts, nil +} From 1ddf09f939da41d4ebabfd339b00d7caf9dc29f6 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Fri, 10 Jan 2025 13:12:58 +0100 Subject: [PATCH 18/49] fix: adjust to remove lock of get accounts :bug: --- .../transaction/internal/adapters/http/in/transaction.go | 3 +++ .../services/command/create-lock-race-condition.go | 8 +++----- pkg/constant/account.go | 3 +++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 7febcff4..59c8361f 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -439,6 +439,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L err = goldModel.ValidateAccounts(*validate, accounts) if err != nil { + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, constant.ValueBalanceLock) mopentelemetry.HandleSpanError(&spanValidateAccounts, "Failed to validate accounts", err) return http.WithError(c, err) @@ -513,6 +514,8 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L case err := <-e: mopentelemetry.HandleSpanError(&spanCreateOperation, "Failed to create operations", err) + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, constant.ValueBalanceLock) + logger.Error("Failed to create operations: ", err.Error()) return http.WithError(c, err) diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index 0d367aea..017d210d 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -4,6 +4,7 @@ import ( "context" "errors" "github.com/LerianStudio/midaz/pkg" + "github.com/LerianStudio/midaz/pkg/constant" "github.com/LerianStudio/midaz/pkg/mgrpc/account" "github.com/LerianStudio/midaz/pkg/mopentelemetry" "github.com/google/uuid" @@ -13,9 +14,6 @@ import ( "time" ) -const TimeSetLock = 1 -const TimeSetLockBalance = 60 - func (uc *UseCase) AllKeysUnlocked(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, hash string) { logger := pkg.NewLoggerFromContext(context.Background()) tracer := pkg.NewTracerFromContext(context.Background()) @@ -49,7 +47,7 @@ func (uc *UseCase) checkAndReleaseLock(ctx context.Context, wg *sync.WaitGroup, defer wg.Done() for { - success, err := uc.RedisRepo.SetNX(context.Background(), internalKey, hash, TimeSetLock) + success, err := uc.RedisRepo.SetNX(context.Background(), internalKey, hash, constant.TimeSetLock) if err != nil { resultChan <- false return @@ -111,7 +109,7 @@ func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledge logger.Infof("Account balance version releasing lock on redis: %v", internalKey) - success, err := uc.RedisRepo.SetNX(context.Background(), internalKey, "lock balance version...", TimeSetLockBalance) + success, err := uc.RedisRepo.SetNX(context.Background(), internalKey, constant.ValueBalanceLock, constant.TimeSetLockBalance) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to lock Account balance version: ", err) diff --git a/pkg/constant/account.go b/pkg/constant/account.go index 24585c4e..77d75aa5 100644 --- a/pkg/constant/account.go +++ b/pkg/constant/account.go @@ -3,4 +3,7 @@ package constant const ( DefaultExternalAccountAliasPrefix = "@external/" ExternalAccountType = "external" + TimeSetLock = 1 + TimeSetLockBalance = 60 + ValueBalanceLock = "lock balance version..." ) From 3970a04dd1ac5157081815726766387434ad0b66 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Fri, 10 Jan 2025 13:13:30 +0100 Subject: [PATCH 19/49] fix: change place :bug: --- .../transaction/internal/adapters/http/in/transaction.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 59c8361f..60c73ef6 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -439,8 +439,9 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L err = goldModel.ValidateAccounts(*validate, accounts) if err != nil { - handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, constant.ValueBalanceLock) mopentelemetry.HandleSpanError(&spanValidateAccounts, "Failed to validate accounts", err) + + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, constant.ValueBalanceLock) return http.WithError(c, err) } From 26af4697aff95095a98681af98a3c0658a60c75b Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Fri, 10 Jan 2025 13:45:57 +0100 Subject: [PATCH 20/49] fix: unlock specify by get accounts :bug: --- .../internal/adapters/http/in/transaction.go | 6 +-- .../command/create-lock-race-condition.go | 38 ++++++++++++++++++- pkg/utils.go | 6 +++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 60c73ef6..92ee5022 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -440,8 +440,8 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L err = goldModel.ValidateAccounts(*validate, accounts) if err != nil { mopentelemetry.HandleSpanError(&spanValidateAccounts, "Failed to validate accounts", err) - - handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, constant.ValueBalanceLock) + + handler.Command.DeleteLocksBalanceVersion(ctx, organizationID, ledgerID, validate.Aliases, accounts) return http.WithError(c, err) } @@ -515,7 +515,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L case err := <-e: mopentelemetry.HandleSpanError(&spanCreateOperation, "Failed to create operations", err) - handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, constant.ValueBalanceLock) + handler.Command.DeleteLocksBalanceVersion(ctx, organizationID, ledgerID, validate.Aliases, accounts) logger.Error("Failed to create operations: ", err.Error()) diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index 017d210d..15386085 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -88,6 +88,41 @@ func (uc *UseCase) DeleteLocks(ctx context.Context, organizationID, ledgerID uui } } +func (uc *UseCase) DeleteLocksBalanceVersion(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, accounts []*account.Account) { + logger := pkg.NewLoggerFromContext(context.Background()) + tracer := pkg.NewTracerFromContext(context.Background()) + + ctx, span := tracer.Start(ctx, "redis.delete_locks") + defer span.End() + + accountsMap := make(map[string]*account.Account) + for _, acc := range accounts { + accountsMap[acc.Id] = acc + accountsMap[acc.Alias] = acc + } + + for _, key := range keys { + if acc, exists := accountsMap[key]; exists { + balanceAvailable := strconv.FormatFloat(acc.Balance.Available, 'f', -1, 64) + balanceScale := strconv.FormatFloat(acc.Balance.Scale, 'f', -1, 64) + + internalKey := pkg.LockBalanceInternalKey(organizationID, ledgerID, key, balanceAvailable, balanceScale) + + logger.Infof("Account balance version releasing lock on redis: %v", internalKey) + + _, err := uc.RedisRepo.Get(ctx, key) + if !errors.Is(err, redis.Nil) && err != nil { + err = uc.RedisRepo.Del(ctx, internalKey) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to release Accounts lock", err) + + logger.Errorf("Failed to release Accounts lock: %v", err) + } + } + } + } +} + func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, accounts []*account.Account) (bool, error) { logger := pkg.NewLoggerFromContext(context.Background()) tracer := pkg.NewTracerFromContext(context.Background()) @@ -104,8 +139,9 @@ func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledge for _, key := range keys { if acc, exists := accountsMap[key]; exists { balanceAvailable := strconv.FormatFloat(acc.Balance.Available, 'f', -1, 64) + balanceScale := strconv.FormatFloat(acc.Balance.Scale, 'f', -1, 64) - internalKey := pkg.LockInternalKey(organizationID, ledgerID, key) + ":" + balanceAvailable + internalKey := pkg.LockBalanceInternalKey(organizationID, ledgerID, key, balanceAvailable, balanceScale) logger.Infof("Account balance version releasing lock on redis: %v", internalKey) diff --git a/pkg/utils.go b/pkg/utils.go index de15311b..84e1baf2 100644 --- a/pkg/utils.go +++ b/pkg/utils.go @@ -290,3 +290,9 @@ func LockInternalKey(organizationID, ledgerID uuid.UUID, key string) string { return lockInternalKey } + +func LockBalanceInternalKey(organizationID, ledgerID uuid.UUID, key, balanceAvailable, balanceScale string) string { + lockBalanceInternalKey := LockInternalKey(organizationID, ledgerID, key) + ":" + balanceAvailable + ":" + balanceScale + + return lockBalanceInternalKey +} From 3ff4f8df98aaceedb15cbfcacc5615ea1544012b Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Fri, 10 Jan 2025 14:26:45 +0100 Subject: [PATCH 21/49] refactor: remove delete lock balance because is unnecessary; :hammer: --- .../internal/adapters/http/in/transaction.go | 32 +++++++---------- .../command/create-lock-race-condition.go | 35 ------------------- 2 files changed, 12 insertions(+), 55 deletions(-) diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 92ee5022..13fcaa12 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -426,7 +426,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L token := http.GetTokenHeader(c) - accounts, err := handler.getAccounts(ctxGetAccounts, logger, token, organizationID, ledgerID, validate.Aliases) + accounts, err := handler.getAccountsAndValidate(ctxGetAccounts, logger, token, organizationID, ledgerID, validate) if err != nil { mopentelemetry.HandleSpanError(&spanGetAccounts, "Failed to get accounts", err) @@ -435,19 +435,6 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L spanGetAccounts.End() - _, spanValidateAccounts := tracer.Start(ctx, "handler.create_transaction.validate_accounts") - - err = goldModel.ValidateAccounts(*validate, accounts) - if err != nil { - mopentelemetry.HandleSpanError(&spanValidateAccounts, "Failed to validate accounts", err) - - handler.Command.DeleteLocksBalanceVersion(ctx, organizationID, ledgerID, validate.Aliases, accounts) - - return http.WithError(c, err) - } - - spanValidateAccounts.End() - ctxCreateTransaction, spanCreateTransaction := tracer.Start(ctx, "handler.create_transaction.create_transaction") err = mopentelemetry.SetSpanAttributesFromStruct(&spanCreateTransaction, "payload_command_create_transaction", parserDSL) @@ -515,8 +502,6 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L case err := <-e: mopentelemetry.HandleSpanError(&spanCreateOperation, "Failed to create operations", err) - handler.Command.DeleteLocksBalanceVersion(ctx, organizationID, ledgerID, validate.Aliases, accounts) - logger.Error("Failed to create operations: ", err.Error()) return http.WithError(c, err) @@ -567,18 +552,25 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L } // getAccounts is a function that split aliases and ids, call the properly function and return Accounts -func (handler *TransactionHandler) getAccounts(ctx context.Context, logger mlog.Logger, token string, organizationID, ledgerID uuid.UUID, input []string) ([]*account.Account, error) { +func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, logger mlog.Logger, token string, organizationID, ledgerID uuid.UUID, validate *goldModel.Responses) ([]*account.Account, error) { span := trace.SpanFromContext(ctx) defer span.End() - accounts, err := handler.Query.GetAccountsLedger(ctx, logger, token, organizationID, ledgerID, input) + accounts, err := handler.Query.GetAccountsLedger(ctx, logger, token, organizationID, ledgerID, validate.Aliases) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to get accounts", err) return nil, err } - searchAgain, err := handler.Command.LockBalanceVersion(ctx, organizationID, ledgerID, input, accounts) + err = goldModel.ValidateAccounts(*validate, accounts) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to validate accounts", err) + + return nil, err + } + + searchAgain, err := handler.Command.LockBalanceVersion(ctx, organizationID, ledgerID, validate.Aliases, accounts) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to get account by alias gRPC on Ledger", err) @@ -588,7 +580,7 @@ func (handler *TransactionHandler) getAccounts(ctx context.Context, logger mlog. } if searchAgain { - return handler.getAccounts(ctx, logger, token, organizationID, ledgerID, input) + return handler.getAccountsAndValidate(ctx, logger, token, organizationID, ledgerID, validate) } return accounts, nil diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index 15386085..3a570505 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -88,41 +88,6 @@ func (uc *UseCase) DeleteLocks(ctx context.Context, organizationID, ledgerID uui } } -func (uc *UseCase) DeleteLocksBalanceVersion(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, accounts []*account.Account) { - logger := pkg.NewLoggerFromContext(context.Background()) - tracer := pkg.NewTracerFromContext(context.Background()) - - ctx, span := tracer.Start(ctx, "redis.delete_locks") - defer span.End() - - accountsMap := make(map[string]*account.Account) - for _, acc := range accounts { - accountsMap[acc.Id] = acc - accountsMap[acc.Alias] = acc - } - - for _, key := range keys { - if acc, exists := accountsMap[key]; exists { - balanceAvailable := strconv.FormatFloat(acc.Balance.Available, 'f', -1, 64) - balanceScale := strconv.FormatFloat(acc.Balance.Scale, 'f', -1, 64) - - internalKey := pkg.LockBalanceInternalKey(organizationID, ledgerID, key, balanceAvailable, balanceScale) - - logger.Infof("Account balance version releasing lock on redis: %v", internalKey) - - _, err := uc.RedisRepo.Get(ctx, key) - if !errors.Is(err, redis.Nil) && err != nil { - err = uc.RedisRepo.Del(ctx, internalKey) - if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to release Accounts lock", err) - - logger.Errorf("Failed to release Accounts lock: %v", err) - } - } - } - } -} - func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, accounts []*account.Account) (bool, error) { logger := pkg.NewLoggerFromContext(context.Background()) tracer := pkg.NewTracerFromContext(context.Background()) From ac48054b9df26fb20af19b21da38c9d9da885de4 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Fri, 10 Jan 2025 15:53:09 +0100 Subject: [PATCH 22/49] refactor: change string to constant :hammer: --- pkg/gold/transaction/model/validations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gold/transaction/model/validations.go b/pkg/gold/transaction/model/validations.go index ef118273..7a62be8d 100644 --- a/pkg/gold/transaction/model/validations.go +++ b/pkg/gold/transaction/model/validations.go @@ -78,7 +78,7 @@ func ValidateFromToOperation(ft FromTo, validate Responses, acc *a.Account) (Amo if ft.IsFrom { ba := OperateAmounts(validate.From[ft.Account], acc.Balance, constant.DEBIT) - if ba.Available < 0 && acc.Type != "external" { + if ba.Available < 0 && acc.Type != constant.ExternalAccountType { return amount, balanceAfter, pkg.ValidateBusinessError(constant.ErrInsufficientFunds, "ValidateFromToOperation", acc.Alias) } From 3e4c7c0601a2c525a917dec8882b1e499ba6db35 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Fri, 10 Jan 2025 19:09:28 +0100 Subject: [PATCH 23/49] refactor: to use count and not waiting ttl :hammer: --- .../internal/adapters/http/in/transaction.go | 71 ++++++++++++------- .../internal/adapters/redis/consumer.redis.go | 21 +++++- .../internal/adapters/redis/redis.mock.go | 14 ++++ .../command/create-lock-race-condition.go | 36 +++++++++- pkg/constant/account.go | 1 - pkg/constant/errors.go | 1 + pkg/errors.go | 6 ++ 7 files changed, 119 insertions(+), 31 deletions(-) diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 13fcaa12..a165745a 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -426,10 +426,15 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L token := http.GetTokenHeader(c) - accounts, err := handler.getAccountsAndValidate(ctxGetAccounts, logger, token, organizationID, ledgerID, validate) + accounts, err := handler.getAccountsAndValidate(ctxGetAccounts, logger, token, organizationID, ledgerID, validate, parserDSL) if err != nil { mopentelemetry.HandleSpanError(&spanGetAccounts, "Failed to get accounts", err) + _, spanReleaseLock := tracer.Start(ctx, "handler.update_accounts.delete_locks_race_condition") + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) + + spanReleaseLock.End() + return http.WithError(c, err) } @@ -450,11 +455,42 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L logger.Error("Failed to create transaction", err.Error()) + _, spanReleaseLock := tracer.Start(ctx, "handler.update_accounts.delete_locks_race_condition") + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) + + spanReleaseLock.End() + return http.WithError(c, err) } spanCreateTransaction.End() + e := make(chan error) + result := make(chan []*operation.Operation) + + var operations []*operation.Operation + + ctxCreateOperation, spanCreateOperation := tracer.Start(ctx, "handler.create_transaction.create_operation") + + go handler.Command.CreateOperation(ctxCreateOperation, accounts, tran.ID, &parserDSL, *validate, result, e) + select { + case ops := <-result: + operations = append(operations, ops...) + case err := <-e: + mopentelemetry.HandleSpanError(&spanCreateOperation, "Failed to create operations", err) + + logger.Error("Failed to create operations: ", err.Error()) + + _, spanReleaseLock := tracer.Start(ctx, "handler.update_accounts.delete_locks_race_condition") + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) + + spanReleaseLock.End() + + return http.WithError(c, err) + } + + spanCreateOperation.End() + ctxProcessAccounts, spanUpdateAccounts := tracer.Start(ctx, "handler.create_transaction.update_accounts") err = mopentelemetry.SetSpanAttributesFromStruct(&spanUpdateAccounts, "payload_handler_update_accounts", accounts) @@ -466,7 +502,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L if err != nil { mopentelemetry.HandleSpanError(&spanUpdateAccounts, "Failed to update accounts", err) - _, spanReleaseLock := tracer.Start(ctx, "handler.update_accounts.delete_race_condition") + _, spanReleaseLock := tracer.Start(ctx, "handler.update_accounts.delete_locks_race_condition") handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) spanReleaseLock.End() @@ -488,27 +524,6 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L spanUpdateAccounts.End() - e := make(chan error) - result := make(chan []*operation.Operation) - - var operations []*operation.Operation - - ctxCreateOperation, spanCreateOperation := tracer.Start(ctx, "handler.create_transaction.create_operation") - - go handler.Command.CreateOperation(ctxCreateOperation, accounts, tran.ID, &parserDSL, *validate, result, e) - select { - case ops := <-result: - operations = append(operations, ops...) - case err := <-e: - mopentelemetry.HandleSpanError(&spanCreateOperation, "Failed to create operations", err) - - logger.Error("Failed to create operations: ", err.Error()) - - return http.WithError(c, err) - } - - spanCreateOperation.End() - ctxUpdateTransactionStatus, spanUpdateTransactionStatus := tracer.Start(ctx, "handler.create_transaction.update_transaction_status") _, err = handler.Command.UpdateTransactionStatus(ctxUpdateTransactionStatus, organizationID, ledgerID, tran.IDtoUUID(), constant.APPROVED) if err != nil { @@ -516,6 +531,11 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L logger.Errorf("Failed to update Transaction with ID: %s, Error: %s", tran.ID, err.Error()) + _, spanReleaseLock := tracer.Start(ctx, "handler.update_accounts.delete_locks_race_condition") + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) + + spanReleaseLock.End() + return http.WithError(c, err) } @@ -541,7 +561,6 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L logger.Infof("Successfully updated Transaction with Organization ID: %s, Ledger ID: %s and ID: %s", organizationID.String(), ledgerID.String(), tran.ID) _, spanReleaseLock := tracer.Start(ctx, "handler.create_transaction.delete_race_condition") - handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) spanReleaseLock.End() @@ -552,7 +571,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L } // getAccounts is a function that split aliases and ids, call the properly function and return Accounts -func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, logger mlog.Logger, token string, organizationID, ledgerID uuid.UUID, validate *goldModel.Responses) ([]*account.Account, error) { +func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, logger mlog.Logger, token string, organizationID, ledgerID uuid.UUID, validate *goldModel.Responses, transaction goldModel.Transaction) ([]*account.Account, error) { span := trace.SpanFromContext(ctx) defer span.End() @@ -580,7 +599,7 @@ func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, l } if searchAgain { - return handler.getAccountsAndValidate(ctx, logger, token, organizationID, ledgerID, validate) + return handler.getAccountsAndValidate(ctx, logger, token, organizationID, ledgerID, validate, transaction) } return accounts, nil diff --git a/components/transaction/internal/adapters/redis/consumer.redis.go b/components/transaction/internal/adapters/redis/consumer.redis.go index 29911af5..d6036951 100644 --- a/components/transaction/internal/adapters/redis/consumer.redis.go +++ b/components/transaction/internal/adapters/redis/consumer.redis.go @@ -17,6 +17,7 @@ type RedisRepository interface { SetNX(ctx context.Context, key, value string, ttl time.Duration) (bool, error) Get(ctx context.Context, key string) (string, error) Del(ctx context.Context, key string) error + Incr(ctx context.Context, key string) int64 } // RedisConsumerRepository is a Redis implementation of the Redis consumer. @@ -66,7 +67,7 @@ func (rr *RedisConsumerRepository) SetNX(ctx context.Context, key, value string, logger := pkg.NewLoggerFromContext(ctx) tracer := pkg.NewTracerFromContext(ctx) - ctx, span := tracer.Start(ctx, "redis.set") + ctx, span := tracer.Start(ctx, "redis.set_nx") defer span.End() rds, err := rr.conn.GetClient(ctx) @@ -80,7 +81,7 @@ func (rr *RedisConsumerRepository) SetNX(ctx context.Context, key, value string, isLocked, err := rds.SetNX(ctx, key, value, ttl*time.Second).Result() if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to set on redis", err) + mopentelemetry.HandleSpanError(&span, "Failed to set nx on redis", err) return false, err } @@ -139,3 +140,19 @@ func (rr *RedisConsumerRepository) Del(ctx context.Context, key string) error { return nil } + +func (rr *RedisConsumerRepository) Incr(ctx context.Context, key string) int64 { + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "redis.incr") + defer span.End() + + rds, err := rr.conn.GetClient(ctx) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get redis", err) + + return 0 + } + + return rds.Incr(ctx, key).Val() +} diff --git a/components/transaction/internal/adapters/redis/redis.mock.go b/components/transaction/internal/adapters/redis/redis.mock.go index 3e610dc1..b3ea9588 100644 --- a/components/transaction/internal/adapters/redis/redis.mock.go +++ b/components/transaction/internal/adapters/redis/redis.mock.go @@ -69,6 +69,20 @@ func (mr *MockRedisRepositoryMockRecorder) Get(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRedisRepository)(nil).Get), arg0, arg1) } +// Incr mocks base method. +func (m *MockRedisRepository) Incr(arg0 context.Context, arg1 string) int64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Incr", arg0, arg1) + ret0, _ := ret[0].(int64) + return ret0 +} + +// Incr indicates an expected call of Incr. +func (mr *MockRedisRepositoryMockRecorder) Incr(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Incr", reflect.TypeOf((*MockRedisRepository)(nil).Incr), arg0, arg1) +} + // Set mocks base method. func (m *MockRedisRepository) Set(arg0 context.Context, arg1, arg2 string, arg3 time.Duration) error { m.ctrl.T.Helper() diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index 3a570505..2e27ec59 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -88,6 +88,22 @@ func (uc *UseCase) DeleteLocks(ctx context.Context, organizationID, ledgerID uui } } +func (uc *UseCase) getCounter(ctx context.Context, organizationID, ledgerID uuid.UUID, key string) (int, error) { + val, err := uc.RedisRepo.Get(ctx, key) + if err != nil { + if !errors.Is(err, redis.Nil) && err != nil { + return 1, nil + } + return 1, err + } + integer, err := strconv.Atoi(val) + if err != nil { + return 1, err + } + + return integer, nil +} + func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, accounts []*account.Account) (bool, error) { logger := pkg.NewLoggerFromContext(context.Background()) tracer := pkg.NewTracerFromContext(context.Background()) @@ -110,7 +126,7 @@ func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledge logger.Infof("Account balance version releasing lock on redis: %v", internalKey) - success, err := uc.RedisRepo.SetNX(context.Background(), internalKey, constant.ValueBalanceLock, constant.TimeSetLockBalance) + isSuccess, err := uc.RedisRepo.SetNX(ctx, internalKey, "0", constant.TimeSetLockBalance) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to lock Account balance version: ", err) @@ -119,7 +135,23 @@ func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledge return false, err } - if !success { + total := uc.RedisRepo.Incr(ctx, internalKey) + if total > 3 { + err = uc.RedisRepo.Del(ctx, internalKey) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to release Account balance version lock", err) + + logger.Errorf("Failed to release Account balance version lock: %v", err) + } + + logger.Infof("Account balance version releasing lock on redis: %v", internalKey) + + return false, pkg.ValidateBusinessError(constant.ErrLockVersionAccountBalance, "LockBalanceVersion") + } + + if !isSuccess { + time.Sleep(200 * time.Millisecond) + logger.Infof("Lock already exists for key, get Accounts again: %v", internalKey) return true, nil diff --git a/pkg/constant/account.go b/pkg/constant/account.go index 77d75aa5..6de2c2cf 100644 --- a/pkg/constant/account.go +++ b/pkg/constant/account.go @@ -5,5 +5,4 @@ const ( ExternalAccountType = "external" TimeSetLock = 1 TimeSetLockBalance = 60 - ValueBalanceLock = "lock balance version..." ) diff --git a/pkg/constant/errors.go b/pkg/constant/errors.go index 8dc2d27a..cc1fc086 100644 --- a/pkg/constant/errors.go +++ b/pkg/constant/errors.go @@ -92,4 +92,5 @@ var ( ErrInvalidQueryParameter = errors.New("0082") ErrInvalidDateRange = errors.New("0083") ErrIdempotencyKey = errors.New("0084") + ErrLockVersionAccountBalance = errors.New("0085") ) diff --git a/pkg/errors.go b/pkg/errors.go index dc418467..f954865b 100644 --- a/pkg/errors.go +++ b/pkg/errors.go @@ -783,6 +783,12 @@ func ValidateBusinessError(err error, entityType string, args ...any) error { Title: "Duplicate Idempotency Key", Message: fmt.Sprintf("The idempotency key %v is already in use. Please provide a unique key and try again.", args), }, + constant.ErrLockVersionAccountBalance: ValidationError{ + EntityType: entityType, + Code: constant.ErrLockVersionAccountBalance.Error(), + Title: "Race conditioning detected", + Message: "A race condition was detected while processing your request. Please try again", + }, } if mappedError, found := errorMap[err]; found { From 6ff156d268d5647f19c9bcae394a5e788fddac4b Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Fri, 10 Jan 2025 19:59:44 +0100 Subject: [PATCH 24/49] feat: add logger :sparkles: --- .../internal/services/command/create-lock-race-condition.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index 2e27ec59..fbed4640 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -136,6 +136,8 @@ func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledge } total := uc.RedisRepo.Incr(ctx, internalKey) + logger.Infof("%v attempt(s) to get Account balance", internalKey) + if total > 3 { err = uc.RedisRepo.Del(ctx, internalKey) if err != nil { From 5a8259fc7116628fb00a232ef6e582cd5c4b3810 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Fri, 10 Jan 2025 20:00:25 +0100 Subject: [PATCH 25/49] add: validation to balance before second lock --- .../internal/adapters/http/in/transaction.go | 2 +- pkg/gold/transaction/model/validations.go | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index a165745a..b2edc770 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -582,7 +582,7 @@ func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, l return nil, err } - err = goldModel.ValidateAccounts(*validate, accounts) + err = goldModel.ValidateAccounts(transaction, *validate, accounts) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to validate accounts", err) diff --git a/pkg/gold/transaction/model/validations.go b/pkg/gold/transaction/model/validations.go index 7a62be8d..88600a6a 100644 --- a/pkg/gold/transaction/model/validations.go +++ b/pkg/gold/transaction/model/validations.go @@ -11,7 +11,7 @@ import ( ) // ValidateAccounts function with some validates in accounts and DSL operations -func ValidateAccounts(validate Responses, accounts []*a.Account) error { +func ValidateAccounts(transaction Transaction, validate Responses, accounts []*a.Account) error { if len(accounts) != (len(validate.From) + len(validate.To)) { return pkg.ValidateBusinessError(constant.ErrAccountIneligibility, "ValidateAccounts") } @@ -24,6 +24,27 @@ func ValidateAccounts(validate Responses, accounts []*a.Account) error { if err := validateToAccounts(acc, validate.To, validate.Asset); err != nil { return err } + + if err := validateBalance(transaction, validate.From, acc); err != nil { + return err + } + + } + + return nil +} + +func validateBalance(dsl Transaction, from map[string]Amount, acc *a.Account) error { + for key := range from { + for _, f := range dsl.Send.Source.From { + if acc.Id == key || acc.Alias == key { + ba := OperateAmounts(from[f.Account], acc.Balance, constant.DEBIT) + + if ba.Available < 0 && acc.Type != constant.ExternalAccountType { + return pkg.ValidateBusinessError(constant.ErrInsufficientFunds, "ValidateFromToOperation", acc.Alias) + } + } + } } return nil From 0c62a314e4ad86918b6955ba3792ce2017102c8e Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Fri, 10 Jan 2025 20:15:09 +0100 Subject: [PATCH 26/49] fix: add unlock :bug: --- .../transaction/internal/adapters/http/in/transaction.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index b2edc770..872f80c2 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -426,7 +426,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L token := http.GetTokenHeader(c) - accounts, err := handler.getAccountsAndValidate(ctxGetAccounts, logger, token, organizationID, ledgerID, validate, parserDSL) + accounts, err := handler.getAccountsAndValidate(ctxGetAccounts, logger, token, hash, organizationID, ledgerID, validate, parserDSL) if err != nil { mopentelemetry.HandleSpanError(&spanGetAccounts, "Failed to get accounts", err) @@ -571,7 +571,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L } // getAccounts is a function that split aliases and ids, call the properly function and return Accounts -func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, logger mlog.Logger, token string, organizationID, ledgerID uuid.UUID, validate *goldModel.Responses, transaction goldModel.Transaction) ([]*account.Account, error) { +func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, logger mlog.Logger, token, hash string, organizationID, ledgerID uuid.UUID, validate *goldModel.Responses, transaction goldModel.Transaction) ([]*account.Account, error) { span := trace.SpanFromContext(ctx) defer span.End() @@ -586,6 +586,8 @@ func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, l if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to validate accounts", err) + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) + return nil, err } @@ -599,7 +601,7 @@ func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, l } if searchAgain { - return handler.getAccountsAndValidate(ctx, logger, token, organizationID, ledgerID, validate, transaction) + return handler.getAccountsAndValidate(ctx, logger, token, hash, organizationID, ledgerID, validate, transaction) } return accounts, nil From 83f01302838cb2438dbd6d0a5316ffe27bd4d174 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Mon, 13 Jan 2025 11:12:39 +0100 Subject: [PATCH 27/49] refactor: change some names to correct reference :hammer: --- pkg/gold/transaction/model/validations.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/gold/transaction/model/validations.go b/pkg/gold/transaction/model/validations.go index 88600a6a..76d4e957 100644 --- a/pkg/gold/transaction/model/validations.go +++ b/pkg/gold/transaction/model/validations.go @@ -41,7 +41,7 @@ func validateBalance(dsl Transaction, from map[string]Amount, acc *a.Account) er ba := OperateAmounts(from[f.Account], acc.Balance, constant.DEBIT) if ba.Available < 0 && acc.Type != constant.ExternalAccountType { - return pkg.ValidateBusinessError(constant.ErrInsufficientFunds, "ValidateFromToOperation", acc.Alias) + return pkg.ValidateBusinessError(constant.ErrInsufficientFunds, "validateBalance", acc.Alias) } } } @@ -54,15 +54,15 @@ func validateFromAccounts(acc *a.Account, from map[string]Amount, asset string) for key := range from { if acc.Id == key || acc.Alias == key { if acc.AssetCode != asset { - return pkg.ValidateBusinessError(constant.ErrAssetCodeNotFound, "ValidateAccounts") + return pkg.ValidateBusinessError(constant.ErrAssetCodeNotFound, "validateFromAccounts") } if !acc.AllowSending { - return pkg.ValidateBusinessError(constant.ErrAccountStatusTransactionRestriction, "ValidateAccounts") + return pkg.ValidateBusinessError(constant.ErrAccountStatusTransactionRestriction, "validateFromAccounts") } if acc.Balance.Available <= 0 && acc.Type != constant.ExternalAccountType { - return pkg.ValidateBusinessError(constant.ErrInsufficientFunds, "ValidateAccounts", acc.Alias) + return pkg.ValidateBusinessError(constant.ErrInsufficientFunds, "validateFromAccounts", acc.Alias) } } } @@ -74,15 +74,15 @@ func validateToAccounts(acc *a.Account, to map[string]Amount, asset string) erro for key := range to { if acc.Id == key || acc.Alias == key { if acc.AssetCode != asset { - return pkg.ValidateBusinessError(constant.ErrAssetCodeNotFound, "ValidateAccounts") + return pkg.ValidateBusinessError(constant.ErrAssetCodeNotFound, "validateToAccounts") } if !acc.AllowReceiving { - return pkg.ValidateBusinessError(constant.ErrAccountStatusTransactionRestriction, "ValidateAccounts") + return pkg.ValidateBusinessError(constant.ErrAccountStatusTransactionRestriction, "validateToAccounts") } if acc.Balance.Available > 0 && acc.Type == constant.ExternalAccountType { - return pkg.ValidateBusinessError(constant.ErrInsufficientFunds, "ValidateAccounts", acc.Alias) + return pkg.ValidateBusinessError(constant.ErrInsufficientFunds, "validateToAccounts", acc.Alias) } } } From 429215c43932501ea5cd9962b8bd087f1d9b7b65 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Mon, 13 Jan 2025 11:13:49 +0100 Subject: [PATCH 28/49] refactor: create constants; delete non used method; change magic numbers; :hammer: --- .../command/create-lock-race-condition.go | 20 ++----------------- pkg/constant/account.go | 4 +++- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index fbed4640..e9bcceb9 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -88,22 +88,6 @@ func (uc *UseCase) DeleteLocks(ctx context.Context, organizationID, ledgerID uui } } -func (uc *UseCase) getCounter(ctx context.Context, organizationID, ledgerID uuid.UUID, key string) (int, error) { - val, err := uc.RedisRepo.Get(ctx, key) - if err != nil { - if !errors.Is(err, redis.Nil) && err != nil { - return 1, nil - } - return 1, err - } - integer, err := strconv.Atoi(val) - if err != nil { - return 1, err - } - - return integer, nil -} - func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledgerID uuid.UUID, keys []string, accounts []*account.Account) (bool, error) { logger := pkg.NewLoggerFromContext(context.Background()) tracer := pkg.NewTracerFromContext(context.Background()) @@ -138,7 +122,7 @@ func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledge total := uc.RedisRepo.Incr(ctx, internalKey) logger.Infof("%v attempt(s) to get Account balance", internalKey) - if total > 3 { + if total > constant.RedisTimesRetry { err = uc.RedisRepo.Del(ctx, internalKey) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to release Account balance version lock", err) @@ -152,7 +136,7 @@ func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledge } if !isSuccess { - time.Sleep(200 * time.Millisecond) + time.Sleep(constant.LockRetry * time.Millisecond) logger.Infof("Lock already exists for key, get Accounts again: %v", internalKey) diff --git a/pkg/constant/account.go b/pkg/constant/account.go index 6de2c2cf..413681d3 100644 --- a/pkg/constant/account.go +++ b/pkg/constant/account.go @@ -4,5 +4,7 @@ const ( DefaultExternalAccountAliasPrefix = "@external/" ExternalAccountType = "external" TimeSetLock = 1 - TimeSetLockBalance = 60 + TimeSetLockBalance = 10 + LockRetry = 200 + RedisTimesRetry = 3 ) From 3f37adeb3592531aa64612915066998defa00c06 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Mon, 13 Jan 2025 13:00:36 +0100 Subject: [PATCH 29/49] feature: add optimistic lock on database using version to control race condition; --- components/ledger/Makefile | 2 +- .../adapters/postgres/account/account.go | 3 + .../postgres/account/account.postgresql.go | 36 ++++- .../000005_create_account_table.up.sql | 1 + .../command/create-lock-race-condition.go | 5 +- pkg/gold/transaction/model/validations.go | 1 + pkg/mgrpc/account/account.pb.go | 142 ++++++++++-------- pkg/mgrpc/account/account.proto | 9 +- pkg/mgrpc/account/account_grpc.pb.go | 48 ++++-- pkg/mmodel/account.go | 2 + pkg/utils.go | 6 +- 11 files changed, 159 insertions(+), 96 deletions(-) diff --git a/components/ledger/Makefile b/components/ledger/Makefile index 757e7169..faebdd87 100644 --- a/components/ledger/Makefile +++ b/components/ledger/Makefile @@ -95,7 +95,7 @@ ps: # App Commands .PHONY: grpc-ledger-gen grpc-ledger-gen: - @protoc --proto_path=../../common/mgrpc --go-grpc_out=../../common/mgrpc --go_out=../../common/mgrpc ../../common/mgrpc/account/account.proto + @protoc --proto_path=../../pkg/mgrpc --go-grpc_out=../../pkg/mgrpc --go_out=../../pkg/mgrpc ../../pkg/mgrpc/account/account.proto .PHONY: run run: diff --git a/components/ledger/internal/adapters/postgres/account/account.go b/components/ledger/internal/adapters/postgres/account/account.go index 0928104b..a23920a5 100644 --- a/components/ledger/internal/adapters/postgres/account/account.go +++ b/components/ledger/internal/adapters/postgres/account/account.go @@ -28,6 +28,7 @@ type AccountPostgreSQLModel struct { AllowReceiving bool Alias *string Type string + Version int64 CreatedAt time.Time UpdatedAt time.Time DeletedAt sql.NullTime @@ -63,6 +64,7 @@ func (t *AccountPostgreSQLModel) ToEntity() *mmodel.Account { AllowReceiving: &t.AllowReceiving, Alias: t.Alias, Type: t.Type, + Version: t.Version, CreatedAt: t.CreatedAt, UpdatedAt: t.UpdatedAt, DeletedAt: nil, @@ -94,6 +96,7 @@ func (t *AccountPostgreSQLModel) FromEntity(account *mmodel.Account) { StatusDescription: account.Status.Description, Alias: account.Alias, Type: account.Type, + Version: account.Version, CreatedAt: account.CreatedAt, UpdatedAt: account.UpdatedAt, } diff --git a/components/ledger/internal/adapters/postgres/account/account.postgresql.go b/components/ledger/internal/adapters/postgres/account/account.postgresql.go index 284dfe4e..e3dc9287 100644 --- a/components/ledger/internal/adapters/postgres/account/account.postgresql.go +++ b/components/ledger/internal/adapters/postgres/account/account.postgresql.go @@ -94,7 +94,7 @@ func (r *AccountPostgreSQLRepository) Create(ctx context.Context, acc *mmodel.Ac result, err := db.ExecContext(ctx, `INSERT INTO account VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22 ) RETURNING *`, record.ID, @@ -115,6 +115,7 @@ func (r *AccountPostgreSQLRepository) Create(ctx context.Context, acc *mmodel.Ac record.AllowReceiving, record.Alias, record.Type, + record.Version, record.CreatedAt, record.UpdatedAt, record.DeletedAt, @@ -767,6 +768,7 @@ func (r *AccountPostgreSQLRepository) ListAccountsByIDs(ctx context.Context, org &acc.AllowReceiving, &acc.Alias, &acc.Type, + &acc.Version, &acc.CreatedAt, &acc.UpdatedAt, &acc.DeletedAt, @@ -837,6 +839,7 @@ func (r *AccountPostgreSQLRepository) ListAccountsByAlias(ctx context.Context, o &acc.AllowReceiving, &acc.Alias, &acc.Type, + &acc.Version, &acc.CreatedAt, &acc.UpdatedAt, &acc.DeletedAt, @@ -976,21 +979,42 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi updates = append(updates, "balance_scale = $"+strconv.Itoa(len(args)+1)) args = append(args, acc.Balance.Scale) + updates = append(updates, "version = $"+strconv.Itoa(len(args)+1)) + version := acc.Version + 1 + args = append(args, version) + updates = append(updates, "updated_at = $"+strconv.Itoa(len(args)+1)) - args = append(args, time.Now(), organizationID, ledgerID, acc.Id) + args = append(args, time.Now(), organizationID, ledgerID, acc.Id, acc.Version) query := `UPDATE account 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)) + + ` WHERE organization_id = $` + strconv.Itoa(len(args)-3) + + ` AND ledger_id = $` + strconv.Itoa(len(args)-2) + + ` AND id = $` + strconv.Itoa(len(args)-1) + + ` AND version = $` + strconv.Itoa(len(args)) + ` AND deleted_at IS NULL` - _, err := tx.ExecContext(ctx, query, args...) + result, err := tx.ExecContext(ctx, query, args...) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to update account", err) return err } + + rowsAffected, err := result.RowsAffected() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get rows affected", err) + + return err + } + + if rowsAffected == 0 { + err = pkg.ValidateBusinessError(constant.ErrLockVersionAccountBalance, reflect.TypeOf(mmodel.Account{}).Name()) + + mopentelemetry.HandleSpanError(&span, "Failed to update account", err) + + return err + } + } if err := tx.Commit(); err != nil { diff --git a/components/ledger/migrations/000005_create_account_table.up.sql b/components/ledger/migrations/000005_create_account_table.up.sql index 672c4eb1..4ab65f56 100644 --- a/components/ledger/migrations/000005_create_account_table.up.sql +++ b/components/ledger/migrations/000005_create_account_table.up.sql @@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS account allow_receiving BOOLEAN NOT NULL, alias TEXT NULL, type TEXT NOT NULL, + version NUMERIC DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE, diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index e9bcceb9..6a3c2a55 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -103,10 +103,7 @@ func (uc *UseCase) LockBalanceVersion(ctx context.Context, organizationID, ledge for _, key := range keys { if acc, exists := accountsMap[key]; exists { - balanceAvailable := strconv.FormatFloat(acc.Balance.Available, 'f', -1, 64) - balanceScale := strconv.FormatFloat(acc.Balance.Scale, 'f', -1, 64) - - internalKey := pkg.LockBalanceInternalKey(organizationID, ledgerID, key, balanceAvailable, balanceScale) + internalKey := pkg.LockVersionInternalKey(organizationID, ledgerID, key, strconv.FormatInt(acc.Version, 10)) logger.Infof("Account balance version releasing lock on redis: %v", internalKey) diff --git a/pkg/gold/transaction/model/validations.go b/pkg/gold/transaction/model/validations.go index 76d4e957..85b14c3f 100644 --- a/pkg/gold/transaction/model/validations.go +++ b/pkg/gold/transaction/model/validations.go @@ -158,6 +158,7 @@ func UpdateAccounts(operation string, fromTo map[string]Amount, accounts []*a.Ac AllowSending: acc.AllowSending, AllowReceiving: acc.AllowReceiving, Type: acc.Type, + Version: acc.Version, CreatedAt: acc.CreatedAt, UpdatedAt: acc.UpdatedAt, } diff --git a/pkg/mgrpc/account/account.pb.go b/pkg/mgrpc/account/account.pb.go index 70a29d1e..6d1f5000 100644 --- a/pkg/mgrpc/account/account.pb.go +++ b/pkg/mgrpc/account/account.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0-devel -// protoc v3.14.0 +// protoc-gen-go v1.34.2 +// protoc v5.27.0 // source: account/account.proto package account @@ -205,10 +205,11 @@ type Account struct { AllowReceiving bool `protobuf:"varint,13,opt,name=allow_receiving,json=allowReceiving,proto3" json:"allow_receiving,omitempty"` Alias string `protobuf:"bytes,14,opt,name=alias,proto3" json:"alias,omitempty"` Type string `protobuf:"bytes,15,opt,name=type,proto3" json:"type,omitempty"` - CreatedAt string `protobuf:"bytes,16,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - UpdatedAt string `protobuf:"bytes,17,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - DeletedAt string `protobuf:"bytes,18,opt,name=deleted_at,json=deletedAt,proto3" json:"deleted_at,omitempty"` - Metadata *Metadata `protobuf:"bytes,19,opt,name=metadata,proto3" json:"metadata,omitempty"` + Version int64 `protobuf:"varint,16,opt,name=version,proto3" json:"version,omitempty"` + CreatedAt string `protobuf:"bytes,17,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt string `protobuf:"bytes,18,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + DeletedAt string `protobuf:"bytes,19,opt,name=deleted_at,json=deletedAt,proto3" json:"deleted_at,omitempty"` + Metadata *Metadata `protobuf:"bytes,20,opt,name=metadata,proto3" json:"metadata,omitempty"` } func (x *Account) Reset() { @@ -348,6 +349,13 @@ func (x *Account) GetType() string { return "" } +func (x *Account) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + func (x *Account) GetCreatedAt() string { if x != nil { return x.CreatedAt @@ -634,7 +642,7 @@ var file_account_account_proto_rawDesc = []byte{ 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0xf6, 0x04, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, + 0x6f, 0x6e, 0x22, 0x90, 0x05, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x63, 0x63, @@ -665,57 +673,59 @@ var file_account_account_proto_rawDesc = []byte{ 0x69, 0x76, 0x69, 0x6e, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, - 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x10, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, - 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x11, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x2d, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, - 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x40, 0x0a, 0x10, 0x41, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x2c, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x52, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0x85, 0x01, - 0x0a, 0x0f, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x72, 0x67, 0x61, - 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x65, - 0x64, 0x67, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, - 0x65, 0x64, 0x67, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x08, 0x61, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0x64, 0x0a, 0x0a, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x73, 0x49, 0x44, 0x12, 0x27, 0x0a, 0x0f, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x72, - 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, - 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, 0x6f, 0x0a, 0x0d, 0x41, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x27, 0x0a, 0x0f, - 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x5f, - 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, - 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x32, 0xea, 0x01, 0x0a, - 0x0c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x44, 0x0a, - 0x10, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x42, 0x79, 0x49, 0x64, - 0x73, 0x12, 0x13, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x73, 0x49, 0x44, 0x1a, 0x19, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x73, 0x42, 0x79, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x61, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x41, 0x6c, - 0x69, 0x61, 0x73, 0x1a, 0x19, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x47, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x73, 0x12, 0x18, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x10, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x2d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x40, 0x0a, 0x10, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x08, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x08, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0x85, 0x01, 0x0a, 0x0f, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x6f, + 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x49, + 0x64, 0x12, 0x2c, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, + 0x64, 0x0a, 0x0a, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x49, 0x44, 0x12, 0x27, 0x0a, + 0x0f, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x65, 0x64, 0x67, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, 0x6f, 0x0a, 0x0d, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x73, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x1b, 0x0a, 0x09, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, + 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x32, 0xea, 0x01, 0x0a, 0x0c, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x44, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x42, 0x79, 0x49, 0x64, 0x73, 0x12, 0x13, 0x2e, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x49, 0x44, + 0x1a, 0x19, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, + 0x14, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x42, 0x79, 0x41, 0x6c, + 0x69, 0x61, 0x73, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x1a, 0x19, 0x2e, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x47, 0x0a, 0x0e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x0b, 0x5a, 0x09, 0x2e, 0x2f, 0x61, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x42, 0x0b, 0x5a, 0x09, 0x2e, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -731,7 +741,7 @@ func file_account_account_proto_rawDescGZIP() []byte { } var file_account_account_proto_msgTypes = make([]protoimpl.MessageInfo, 9) -var file_account_account_proto_goTypes = []interface{}{ +var file_account_account_proto_goTypes = []any{ (*Balance)(nil), // 0: account.Balance (*Metadata)(nil), // 1: account.Metadata (*Status)(nil), // 2: account.Status @@ -768,7 +778,7 @@ func file_account_account_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_account_account_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_account_account_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*Balance); i { case 0: return &v.state @@ -780,7 +790,7 @@ func file_account_account_proto_init() { return nil } } - file_account_account_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_account_account_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*Metadata); i { case 0: return &v.state @@ -792,7 +802,7 @@ func file_account_account_proto_init() { return nil } } - file_account_account_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_account_account_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*Status); i { case 0: return &v.state @@ -804,7 +814,7 @@ func file_account_account_proto_init() { return nil } } - file_account_account_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_account_account_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*Account); i { case 0: return &v.state @@ -816,7 +826,7 @@ func file_account_account_proto_init() { return nil } } - file_account_account_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_account_account_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*AccountsResponse); i { case 0: return &v.state @@ -828,7 +838,7 @@ func file_account_account_proto_init() { return nil } } - file_account_account_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_account_account_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*AccountsRequest); i { case 0: return &v.state @@ -840,7 +850,7 @@ func file_account_account_proto_init() { return nil } } - file_account_account_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_account_account_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*AccountsID); i { case 0: return &v.state @@ -852,7 +862,7 @@ func file_account_account_proto_init() { return nil } } - file_account_account_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_account_account_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*AccountsAlias); i { case 0: return &v.state diff --git a/pkg/mgrpc/account/account.proto b/pkg/mgrpc/account/account.proto index 47ca487b..f3a3cca5 100644 --- a/pkg/mgrpc/account/account.proto +++ b/pkg/mgrpc/account/account.proto @@ -34,10 +34,11 @@ message Account { bool allow_receiving = 13; string alias = 14; string type = 15; - string created_at = 16; - string updated_at = 17; - string deleted_at = 18; - Metadata metadata = 19; + int64 version = 16; + string created_at = 17; + string updated_at = 18; + string deleted_at = 19; + Metadata metadata = 20; } message AccountsResponse { diff --git a/pkg/mgrpc/account/account_grpc.pb.go b/pkg/mgrpc/account/account_grpc.pb.go index bbee49d0..7d24d0a2 100644 --- a/pkg/mgrpc/account/account_grpc.pb.go +++ b/pkg/mgrpc/account/account_grpc.pb.go @@ -1,4 +1,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.27.0 +// source: account/account.proto package account @@ -11,8 +15,14 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + AccountProto_GetAccountsByIds_FullMethodName = "/account.AccountProto/GetAccountsByIds" + AccountProto_GetAccountsByAliases_FullMethodName = "/account.AccountProto/GetAccountsByAliases" + AccountProto_UpdateAccounts_FullMethodName = "/account.AccountProto/UpdateAccounts" +) // AccountProtoClient is the client API for AccountProto service. // @@ -32,8 +42,9 @@ func NewAccountProtoClient(cc grpc.ClientConnInterface) AccountProtoClient { } func (c *accountProtoClient) GetAccountsByIds(ctx context.Context, in *AccountsID, opts ...grpc.CallOption) (*AccountsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(AccountsResponse) - err := c.cc.Invoke(ctx, "/account.AccountProto/GetAccountsByIds", in, out, opts...) + err := c.cc.Invoke(ctx, AccountProto_GetAccountsByIds_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -41,8 +52,9 @@ func (c *accountProtoClient) GetAccountsByIds(ctx context.Context, in *AccountsI } func (c *accountProtoClient) GetAccountsByAliases(ctx context.Context, in *AccountsAlias, opts ...grpc.CallOption) (*AccountsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(AccountsResponse) - err := c.cc.Invoke(ctx, "/account.AccountProto/GetAccountsByAliases", in, out, opts...) + err := c.cc.Invoke(ctx, AccountProto_GetAccountsByAliases_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -50,8 +62,9 @@ func (c *accountProtoClient) GetAccountsByAliases(ctx context.Context, in *Accou } func (c *accountProtoClient) UpdateAccounts(ctx context.Context, in *AccountsRequest, opts ...grpc.CallOption) (*AccountsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(AccountsResponse) - err := c.cc.Invoke(ctx, "/account.AccountProto/UpdateAccounts", in, out, opts...) + err := c.cc.Invoke(ctx, AccountProto_UpdateAccounts_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -60,7 +73,7 @@ func (c *accountProtoClient) UpdateAccounts(ctx context.Context, in *AccountsReq // AccountProtoServer is the server API for AccountProto service. // All implementations must embed UnimplementedAccountProtoServer -// for forward compatibility +// for forward compatibility. type AccountProtoServer interface { GetAccountsByIds(context.Context, *AccountsID) (*AccountsResponse, error) GetAccountsByAliases(context.Context, *AccountsAlias) (*AccountsResponse, error) @@ -68,9 +81,12 @@ type AccountProtoServer interface { mustEmbedUnimplementedAccountProtoServer() } -// UnimplementedAccountProtoServer must be embedded to have forward compatible implementations. -type UnimplementedAccountProtoServer struct { -} +// UnimplementedAccountProtoServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAccountProtoServer struct{} func (UnimplementedAccountProtoServer) GetAccountsByIds(context.Context, *AccountsID) (*AccountsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetAccountsByIds not implemented") @@ -82,6 +98,7 @@ func (UnimplementedAccountProtoServer) UpdateAccounts(context.Context, *Accounts return nil, status.Errorf(codes.Unimplemented, "method UpdateAccounts not implemented") } func (UnimplementedAccountProtoServer) mustEmbedUnimplementedAccountProtoServer() {} +func (UnimplementedAccountProtoServer) testEmbeddedByValue() {} // UnsafeAccountProtoServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to AccountProtoServer will @@ -91,6 +108,13 @@ type UnsafeAccountProtoServer interface { } func RegisterAccountProtoServer(s grpc.ServiceRegistrar, srv AccountProtoServer) { + // If the following call pancis, it indicates UnimplementedAccountProtoServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&AccountProto_ServiceDesc, srv) } @@ -104,7 +128,7 @@ func _AccountProto_GetAccountsByIds_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/account.AccountProto/GetAccountsByIds", + FullMethod: AccountProto_GetAccountsByIds_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(AccountProtoServer).GetAccountsByIds(ctx, req.(*AccountsID)) @@ -122,7 +146,7 @@ func _AccountProto_GetAccountsByAliases_Handler(srv interface{}, ctx context.Con } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/account.AccountProto/GetAccountsByAliases", + FullMethod: AccountProto_GetAccountsByAliases_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(AccountProtoServer).GetAccountsByAliases(ctx, req.(*AccountsAlias)) @@ -140,7 +164,7 @@ func _AccountProto_UpdateAccounts_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/account.AccountProto/UpdateAccounts", + FullMethod: AccountProto_UpdateAccounts_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(AccountProtoServer).UpdateAccounts(ctx, req.(*AccountsRequest)) diff --git a/pkg/mmodel/account.go b/pkg/mmodel/account.go index 127a42bd..d45214c4 100644 --- a/pkg/mmodel/account.go +++ b/pkg/mmodel/account.go @@ -59,6 +59,7 @@ type Account struct { AllowReceiving *bool `json:"allowReceiving" example:"true"` Alias *string `json:"alias" example:"@person1"` Type string `json:"type" example:"creditCard"` + Version int64 `json:"-"` CreatedAt time.Time `json:"createdAt" example:"2021-01-01T00:00:00Z"` UpdatedAt time.Time `json:"updatedAt" example:"2021-01-01T00:00:00Z"` DeletedAt *time.Time `json:"deletedAt" example:"2021-01-01T00:00:00Z"` @@ -117,6 +118,7 @@ func (e *Account) ToProto() *proto.Account { AllowSending: *e.AllowSending, AllowReceiving: *e.AllowReceiving, Type: e.Type, + Version: e.Version, } if e.ParentAccountID != nil { diff --git a/pkg/utils.go b/pkg/utils.go index 84e1baf2..195f14bc 100644 --- a/pkg/utils.go +++ b/pkg/utils.go @@ -291,8 +291,8 @@ func LockInternalKey(organizationID, ledgerID uuid.UUID, key string) string { return lockInternalKey } -func LockBalanceInternalKey(organizationID, ledgerID uuid.UUID, key, balanceAvailable, balanceScale string) string { - lockBalanceInternalKey := LockInternalKey(organizationID, ledgerID, key) + ":" + balanceAvailable + ":" + balanceScale +func LockVersionInternalKey(organizationID, ledgerID uuid.UUID, key, version string) string { + lockVersionInternalKey := LockInternalKey(organizationID, ledgerID, key) + ":" + version - return lockBalanceInternalKey + return lockVersionInternalKey } From a04a8eebeda62ff6f1812606c778aa5bcbf15041 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Mon, 13 Jan 2025 13:05:43 +0100 Subject: [PATCH 30/49] fix: merge with develop :bug: --- pkg/constant/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/constant/errors.go b/pkg/constant/errors.go index 3ed91db3..73a06898 100644 --- a/pkg/constant/errors.go +++ b/pkg/constant/errors.go @@ -93,5 +93,5 @@ var ( ErrInvalidDateRange = errors.New("0083") ErrIdempotencyKey = errors.New("0084") ErrAccountAliasNotFound = errors.New("0085") - ErrLockVersionAccountBalance = errors.New("0085") + ErrLockVersionAccountBalance = errors.New("0086") ) From 6c54b034f152a58077a8f74b1b1fe956a0124dbd Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Mon, 13 Jan 2025 16:07:29 +0100 Subject: [PATCH 31/49] refacotr: back get accounts to query cqrs; --- .../internal/adapters/grpc/in/account.go | 158 +++++++++--------- .../adapters/postgres/account/account.mock.go | 124 ++++++++------ .../{command => query}/get-alias-accounts.go | 2 +- .../get-alias-accounts_test.go | 7 +- .../{command => query}/get-ids-accounts.go | 2 +- .../get-ids-accounts_test.go | 5 +- .../internal/adapters/http/in/transaction.go | 8 +- 7 files changed, 159 insertions(+), 147 deletions(-) rename components/ledger/internal/services/{command => query}/get-alias-accounts.go (98%) rename components/ledger/internal/services/{command => query}/get-alias-accounts_test.go (96%) rename components/ledger/internal/services/{command => query}/get-ids-accounts.go (98%) rename components/ledger/internal/services/{command => query}/get-ids-accounts_test.go (96%) diff --git a/components/ledger/internal/adapters/grpc/in/account.go b/components/ledger/internal/adapters/grpc/in/account.go index 656639ce..9215a163 100644 --- a/components/ledger/internal/adapters/grpc/in/account.go +++ b/components/ledger/internal/adapters/grpc/in/account.go @@ -60,7 +60,7 @@ func (ap *AccountProto) GetAccountsByIds(ctx context.Context, ids *account.Accou return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), strings.Join(invalidUUIDs, ", ")) } - acc, err := ap.Command.ListAccountsByIDs(ctx, organizationUUID, ledgerUUID, uuids) + acc, err := ap.Query.ListAccountsByIDs(ctx, organizationUUID, ledgerUUID, uuids) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to retrieve Accounts by ids for grpc", err) @@ -99,7 +99,7 @@ func (ap *AccountProto) GetAccountsByAliases(ctx context.Context, aliases *accou return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), ledgerUUID) } - acc, err := ap.Command.ListAccountsByAlias(ctx, organizationUUID, ledgerUUID, aliases.GetAliases()) + acc, err := ap.Query.ListAccountsByAlias(ctx, organizationUUID, ledgerUUID, aliases.GetAliases()) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to retrieve Accounts by aliases for grpc", err) @@ -120,82 +120,82 @@ func (ap *AccountProto) GetAccountsByAliases(ctx context.Context, aliases *accou return &response, nil } -//// UpdateAccounts is a method that update Account balances by a given ids. -//func (ap *AccountProto) UpdateAccounts(ctx context.Context, update *account.AccountsRequest) (*account.AccountsResponse, error) { -// logger := pkg.NewLoggerFromContext(ctx) -// tracer := pkg.NewTracerFromContext(ctx) -// -// ctx, span := tracer.Start(ctx, "handler.UpdateAccounts") -// defer span.End() -// -// accounts := make([]*account.Account, 0) -// -// uuids := make([]uuid.UUID, 0) -// -// for _, getacc := range update.GetAccounts() { -// if pkg.IsNilOrEmpty(&getacc.Id) { -// mopentelemetry.HandleSpanError(&span, "Failed to update Accounts because id is empty", nil) -// -// logger.Errorf("Failed to update Accounts because id is empty") -// -// return nil, pkg.ValidateBusinessError(constant.ErrNoAccountIDsProvided, reflect.TypeOf(mmodel.Account{}).Name()) -// } -// -// balance := mmodel.Balance{ -// Available: &getacc.Balance.Available, -// OnHold: &getacc.Balance.OnHold, -// Scale: &getacc.Balance.Scale, -// } -// -// organizationUUID, err := uuid.Parse(getacc.GetOrganizationId()) -// if err != nil { -// return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), organizationUUID) -// } -// -// ledgerUUID, err := uuid.Parse(getacc.GetLedgerId()) -// if err != nil { -// return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), ledgerUUID) -// } -// -// accountUUID, err := uuid.Parse(getacc.GetId()) -// if err != nil { -// return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), accountUUID) -// } -// -// _, err = ap.Command.UpdateAccountByID(ctx, organizationUUID, ledgerUUID, accountUUID, &balance) -// if err != nil { -// mopentelemetry.HandleSpanError(&span, "Failed to update balance in Account by id", err) -// -// logger.Errorf("Failed to update balance in Account by id for organizationId %s and ledgerId %s in grpc, Error: %s", getacc.OrganizationId, getacc.LedgerId, err.Error()) -// -// return nil, pkg.ValidateBusinessError(constant.ErrBalanceUpdateFailed, reflect.TypeOf(mmodel.Account{}).Name()) -// } -// -// uuids = append(uuids, uuid.MustParse(getacc.Id)) -// } -// -// organizationID := update.GetOrganizationId() -// ledgerID := update.GetLedgerId() -// -// acc, err := ap.Command.ListAccountsByIDs(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuids) -// if err != nil { -// mopentelemetry.HandleSpanError(&span, "Failed to retrieve Accounts by ids for grpc", err) -// -// logger.Errorf("Failed to retrieve Accounts by ids for organizationId %s and ledgerId %s in grpc, Error: %s", organizationID, ledgerID, err.Error()) -// -// return nil, pkg.ValidateBusinessError(constant.ErrNoAccountsFound, reflect.TypeOf(mmodel.Account{}).Name()) -// } -// -// for _, ac := range acc { -// accounts = append(accounts, ac.ToProto()) -// } -// -// response := account.AccountsResponse{ -// Accounts: accounts, -// } -// -// return &response, nil -//} +// UpdateAccountsByIDS is a method that update Account balances by a given ids. +func (ap *AccountProto) UpdateAccountsByIDS(ctx context.Context, update *account.AccountsRequest) (*account.AccountsResponse, error) { + logger := pkg.NewLoggerFromContext(ctx) + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "handler.UpdateAccounts") + defer span.End() + + accounts := make([]*account.Account, 0) + + uuids := make([]uuid.UUID, 0) + + for _, getacc := range update.GetAccounts() { + if pkg.IsNilOrEmpty(&getacc.Id) { + mopentelemetry.HandleSpanError(&span, "Failed to update Accounts because id is empty", nil) + + logger.Errorf("Failed to update Accounts because id is empty") + + return nil, pkg.ValidateBusinessError(constant.ErrNoAccountIDsProvided, reflect.TypeOf(mmodel.Account{}).Name()) + } + + balance := mmodel.Balance{ + Available: &getacc.Balance.Available, + OnHold: &getacc.Balance.OnHold, + Scale: &getacc.Balance.Scale, + } + + organizationUUID, err := uuid.Parse(getacc.GetOrganizationId()) + if err != nil { + return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), organizationUUID) + } + + ledgerUUID, err := uuid.Parse(getacc.GetLedgerId()) + if err != nil { + return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), ledgerUUID) + } + + accountUUID, err := uuid.Parse(getacc.GetId()) + if err != nil { + return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), accountUUID) + } + + _, err = ap.Command.UpdateAccountByID(ctx, organizationUUID, ledgerUUID, accountUUID, &balance) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to update balance in Account by id", err) + + logger.Errorf("Failed to update balance in Account by id for organizationId %s and ledgerId %s in grpc, Error: %s", getacc.OrganizationId, getacc.LedgerId, err.Error()) + + return nil, pkg.ValidateBusinessError(constant.ErrBalanceUpdateFailed, reflect.TypeOf(mmodel.Account{}).Name()) + } + + uuids = append(uuids, uuid.MustParse(getacc.Id)) + } + + organizationID := update.GetOrganizationId() + ledgerID := update.GetLedgerId() + + acc, err := ap.Query.ListAccountsByIDs(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuids) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to retrieve Accounts by ids for grpc", err) + + logger.Errorf("Failed to retrieve Accounts by ids for organizationId %s and ledgerId %s in grpc, Error: %s", organizationID, ledgerID, err.Error()) + + return nil, pkg.ValidateBusinessError(constant.ErrNoAccountsFound, reflect.TypeOf(mmodel.Account{}).Name()) + } + + for _, ac := range acc { + accounts = append(accounts, ac.ToProto()) + } + + response := account.AccountsResponse{ + Accounts: accounts, + } + + return &response, nil +} // UpdateAccounts is a method that update Account balances by a given ids. func (ap *AccountProto) UpdateAccounts(ctx context.Context, update *account.AccountsRequest) (*account.AccountsResponse, error) { @@ -228,7 +228,7 @@ func (ap *AccountProto) UpdateAccounts(ctx context.Context, update *account.Acco logger.Errorf("Failed to update balance in Account by id for organizationId %v and ledgerId %v in grpc, Error: %v", organizationID, ledgerID, err.Error()) - return nil, pkg.ValidateBusinessError(constant.ErrBalanceUpdateFailed, reflect.TypeOf(mmodel.Account{}).Name()) + return nil, err } return nil, nil diff --git a/components/ledger/internal/adapters/postgres/account/account.mock.go b/components/ledger/internal/adapters/postgres/account/account.mock.go index 743eb60f..b3a8c3e4 100644 --- a/components/ledger/internal/adapters/postgres/account/account.mock.go +++ b/components/ledger/internal/adapters/postgres/account/account.mock.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: /home/max/Workspace/midaz/components/ledger/internal/adapters/postgres/account/account.postgresql.go +// Source: github.com/LerianStudio/midaz/components/ledger/internal/adapters/postgres/account (interfaces: Repository) // // Generated by this command: // -// mockgen -source=/home/max/Workspace/midaz/components/ledger/internal/adapters/postgres/account/account.postgresql.go -destination=/home/max/Workspace/midaz/components/ledger/internal/adapters/postgres/account/account.postgresql_mock.go -package account +// mockgen --destination=account.mock.go --package=account . Repository // // Package account is a generated GoMock package. @@ -13,6 +13,7 @@ import ( context "context" reflect "reflect" + account "github.com/LerianStudio/midaz/pkg/mgrpc/account" mmodel "github.com/LerianStudio/midaz/pkg/mmodel" http "github.com/LerianStudio/midaz/pkg/net/http" uuid "github.com/google/uuid" @@ -23,7 +24,6 @@ import ( type MockRepository struct { ctrl *gomock.Controller recorder *MockRepositoryMockRecorder - isgomock struct{} } // MockRepositoryMockRecorder is the mock recorder for MockRepository. @@ -44,195 +44,209 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { } // Create mocks base method. -func (m *MockRepository) Create(ctx context.Context, acc *mmodel.Account) (*mmodel.Account, error) { +func (m *MockRepository) Create(arg0 context.Context, arg1 *mmodel.Account) (*mmodel.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", ctx, acc) + ret := m.ctrl.Call(m, "Create", arg0, arg1) ret0, _ := ret[0].(*mmodel.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. -func (mr *MockRepositoryMockRecorder) Create(ctx, acc any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Create(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), ctx, acc) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), arg0, arg1) } // Delete mocks base method. -func (m *MockRepository) Delete(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, id uuid.UUID) error { +func (m *MockRepository) Delete(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 *uuid.UUID, arg4 uuid.UUID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", ctx, organizationID, ledgerID, portfolioID, id) + ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. -func (mr *MockRepositoryMockRecorder) Delete(ctx, organizationID, ledgerID, portfolioID, id any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Delete(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), ctx, organizationID, ledgerID, portfolioID, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), arg0, arg1, arg2, arg3, arg4) } // Find mocks base method. -func (m *MockRepository) Find(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, id uuid.UUID) (*mmodel.Account, error) { +func (m *MockRepository) Find(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 *uuid.UUID, arg4 uuid.UUID) (*mmodel.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Find", ctx, organizationID, ledgerID, portfolioID, id) + ret := m.ctrl.Call(m, "Find", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*mmodel.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // Find indicates an expected call of Find. -func (mr *MockRepositoryMockRecorder) Find(ctx, organizationID, ledgerID, portfolioID, id any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Find(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockRepository)(nil).Find), ctx, organizationID, ledgerID, portfolioID, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockRepository)(nil).Find), arg0, arg1, arg2, arg3, arg4) } // FindAlias mocks base method. -func (m *MockRepository) FindAlias(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, alias string) (*mmodel.Account, error) { +func (m *MockRepository) FindAlias(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 *uuid.UUID, arg4 string) (*mmodel.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindAlias", ctx, organizationID, ledgerID, portfolioID, alias) + ret := m.ctrl.Call(m, "FindAlias", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*mmodel.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // FindAlias indicates an expected call of FindAlias. -func (mr *MockRepositoryMockRecorder) FindAlias(ctx, organizationID, ledgerID, portfolioID, alias any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) FindAlias(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAlias", reflect.TypeOf((*MockRepository)(nil).FindAlias), ctx, organizationID, ledgerID, portfolioID, alias) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAlias", reflect.TypeOf((*MockRepository)(nil).FindAlias), arg0, arg1, arg2, arg3, arg4) } // FindAll mocks base method. -func (m *MockRepository) FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, filter http.Pagination) ([]*mmodel.Account, error) { +func (m *MockRepository) FindAll(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 *uuid.UUID, arg4 http.Pagination) ([]*mmodel.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindAll", ctx, organizationID, ledgerID, portfolioID, filter) + ret := m.ctrl.Call(m, "FindAll", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].([]*mmodel.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // FindAll indicates an expected call of FindAll. -func (mr *MockRepositoryMockRecorder) FindAll(ctx, organizationID, ledgerID, portfolioID, filter any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) FindAll(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAll", reflect.TypeOf((*MockRepository)(nil).FindAll), ctx, organizationID, ledgerID, portfolioID, filter) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAll", reflect.TypeOf((*MockRepository)(nil).FindAll), arg0, arg1, arg2, arg3, arg4) } // FindByAlias mocks base method. -func (m *MockRepository) FindByAlias(ctx context.Context, organizationID, ledgerID uuid.UUID, alias string) (bool, error) { +func (m *MockRepository) FindByAlias(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindByAlias", ctx, organizationID, ledgerID, alias) + ret := m.ctrl.Call(m, "FindByAlias", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // FindByAlias indicates an expected call of FindByAlias. -func (mr *MockRepositoryMockRecorder) FindByAlias(ctx, organizationID, ledgerID, alias any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) FindByAlias(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByAlias", reflect.TypeOf((*MockRepository)(nil).FindByAlias), ctx, organizationID, ledgerID, alias) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByAlias", reflect.TypeOf((*MockRepository)(nil).FindByAlias), arg0, arg1, arg2, arg3) } // FindWithDeleted mocks base method. -func (m *MockRepository) FindWithDeleted(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, id uuid.UUID) (*mmodel.Account, error) { +func (m *MockRepository) FindWithDeleted(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 *uuid.UUID, arg4 uuid.UUID) (*mmodel.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindWithDeleted", ctx, organizationID, ledgerID, portfolioID, id) + ret := m.ctrl.Call(m, "FindWithDeleted", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*mmodel.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // FindWithDeleted indicates an expected call of FindWithDeleted. -func (mr *MockRepositoryMockRecorder) FindWithDeleted(ctx, organizationID, ledgerID, portfolioID, id any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) FindWithDeleted(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindWithDeleted", reflect.TypeOf((*MockRepository)(nil).FindWithDeleted), ctx, organizationID, ledgerID, portfolioID, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindWithDeleted", reflect.TypeOf((*MockRepository)(nil).FindWithDeleted), arg0, arg1, arg2, arg3, arg4) } // ListAccountsByAlias mocks base method. -func (m *MockRepository) ListAccountsByAlias(ctx context.Context, organizationID, ledgerID uuid.UUID, aliases []string) ([]*mmodel.Account, error) { +func (m *MockRepository) ListAccountsByAlias(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 []string) ([]*mmodel.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAccountsByAlias", ctx, organizationID, ledgerID, aliases) + ret := m.ctrl.Call(m, "ListAccountsByAlias", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]*mmodel.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // ListAccountsByAlias indicates an expected call of ListAccountsByAlias. -func (mr *MockRepositoryMockRecorder) ListAccountsByAlias(ctx, organizationID, ledgerID, aliases any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) ListAccountsByAlias(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccountsByAlias", reflect.TypeOf((*MockRepository)(nil).ListAccountsByAlias), ctx, organizationID, ledgerID, aliases) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccountsByAlias", reflect.TypeOf((*MockRepository)(nil).ListAccountsByAlias), arg0, arg1, arg2, arg3) } // ListAccountsByIDs mocks base method. -func (m *MockRepository) ListAccountsByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*mmodel.Account, error) { +func (m *MockRepository) ListAccountsByIDs(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 []uuid.UUID) ([]*mmodel.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAccountsByIDs", ctx, organizationID, ledgerID, ids) + ret := m.ctrl.Call(m, "ListAccountsByIDs", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]*mmodel.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // ListAccountsByIDs indicates an expected call of ListAccountsByIDs. -func (mr *MockRepositoryMockRecorder) ListAccountsByIDs(ctx, organizationID, ledgerID, ids any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) ListAccountsByIDs(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccountsByIDs", reflect.TypeOf((*MockRepository)(nil).ListAccountsByIDs), ctx, organizationID, ledgerID, ids) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccountsByIDs", reflect.TypeOf((*MockRepository)(nil).ListAccountsByIDs), arg0, arg1, arg2, arg3) } // ListByAlias mocks base method. -func (m *MockRepository) ListByAlias(ctx context.Context, organizationID, ledgerID, portfolioID uuid.UUID, alias []string) ([]*mmodel.Account, error) { +func (m *MockRepository) ListByAlias(arg0 context.Context, arg1, arg2, arg3 uuid.UUID, arg4 []string) ([]*mmodel.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListByAlias", ctx, organizationID, ledgerID, portfolioID, alias) + ret := m.ctrl.Call(m, "ListByAlias", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].([]*mmodel.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // ListByAlias indicates an expected call of ListByAlias. -func (mr *MockRepositoryMockRecorder) ListByAlias(ctx, organizationID, ledgerID, portfolioID, alias any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) ListByAlias(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByAlias", reflect.TypeOf((*MockRepository)(nil).ListByAlias), ctx, organizationID, ledgerID, portfolioID, alias) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByAlias", reflect.TypeOf((*MockRepository)(nil).ListByAlias), arg0, arg1, arg2, arg3, arg4) } // ListByIDs mocks base method. -func (m *MockRepository) ListByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, ids []uuid.UUID) ([]*mmodel.Account, error) { +func (m *MockRepository) ListByIDs(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 *uuid.UUID, arg4 []uuid.UUID) ([]*mmodel.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListByIDs", ctx, organizationID, ledgerID, portfolioID, ids) + ret := m.ctrl.Call(m, "ListByIDs", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].([]*mmodel.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // ListByIDs indicates an expected call of ListByIDs. -func (mr *MockRepositoryMockRecorder) ListByIDs(ctx, organizationID, ledgerID, portfolioID, ids any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) ListByIDs(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByIDs", reflect.TypeOf((*MockRepository)(nil).ListByIDs), ctx, organizationID, ledgerID, portfolioID, ids) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByIDs", reflect.TypeOf((*MockRepository)(nil).ListByIDs), arg0, arg1, arg2, arg3, arg4) } // Update mocks base method. -func (m *MockRepository) Update(ctx context.Context, organizationID, ledgerID uuid.UUID, portfolioID *uuid.UUID, id uuid.UUID, acc *mmodel.Account) (*mmodel.Account, error) { +func (m *MockRepository) Update(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 *uuid.UUID, arg4 uuid.UUID, arg5 *mmodel.Account) (*mmodel.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", ctx, organizationID, ledgerID, portfolioID, id, acc) + ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3, arg4, arg5) ret0, _ := ret[0].(*mmodel.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // Update indicates an expected call of Update. -func (mr *MockRepositoryMockRecorder) Update(ctx, organizationID, ledgerID, portfolioID, id, acc any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Update(arg0, arg1, arg2, arg3, arg4, arg5 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), ctx, organizationID, ledgerID, portfolioID, id, acc) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), arg0, arg1, arg2, arg3, arg4, arg5) } // UpdateAccountByID mocks base method. -func (m *MockRepository) UpdateAccountByID(ctx context.Context, organizationID, ledgerID, id uuid.UUID, acc *mmodel.Account) (*mmodel.Account, error) { +func (m *MockRepository) UpdateAccountByID(arg0 context.Context, arg1, arg2, arg3 uuid.UUID, arg4 *mmodel.Account) (*mmodel.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateAccountByID", ctx, organizationID, ledgerID, id, acc) + ret := m.ctrl.Call(m, "UpdateAccountByID", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*mmodel.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateAccountByID indicates an expected call of UpdateAccountByID. -func (mr *MockRepositoryMockRecorder) UpdateAccountByID(ctx, organizationID, ledgerID, id, acc any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) UpdateAccountByID(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountByID", reflect.TypeOf((*MockRepository)(nil).UpdateAccountByID), ctx, organizationID, ledgerID, id, acc) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountByID", reflect.TypeOf((*MockRepository)(nil).UpdateAccountByID), arg0, arg1, arg2, arg3, arg4) +} + +// UpdateAccounts mocks base method. +func (m *MockRepository) UpdateAccounts(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 []*account.Account) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateAccounts", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateAccounts indicates an expected call of UpdateAccounts. +func (mr *MockRepositoryMockRecorder) UpdateAccounts(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccounts", reflect.TypeOf((*MockRepository)(nil).UpdateAccounts), arg0, arg1, arg2, arg3) } diff --git a/components/ledger/internal/services/command/get-alias-accounts.go b/components/ledger/internal/services/query/get-alias-accounts.go similarity index 98% rename from components/ledger/internal/services/command/get-alias-accounts.go rename to components/ledger/internal/services/query/get-alias-accounts.go index ee43040e..4b5ed64c 100644 --- a/components/ledger/internal/services/command/get-alias-accounts.go +++ b/components/ledger/internal/services/query/get-alias-accounts.go @@ -1,4 +1,4 @@ -package command +package query import ( "context" diff --git a/components/ledger/internal/services/command/get-alias-accounts_test.go b/components/ledger/internal/services/query/get-alias-accounts_test.go similarity index 96% rename from components/ledger/internal/services/command/get-alias-accounts_test.go rename to components/ledger/internal/services/query/get-alias-accounts_test.go index 2b86d9f5..5440797b 100644 --- a/components/ledger/internal/services/command/get-alias-accounts_test.go +++ b/components/ledger/internal/services/query/get-alias-accounts_test.go @@ -1,15 +1,14 @@ -package command +package query import ( "context" "errors" - "github.com/LerianStudio/midaz/components/ledger/internal/services/query" + "github.com/LerianStudio/midaz/pkg/mpointers" "testing" "github.com/LerianStudio/midaz/components/ledger/internal/adapters/postgres/account" "github.com/LerianStudio/midaz/components/ledger/internal/services" "github.com/LerianStudio/midaz/pkg/mmodel" - "github.com/LerianStudio/midaz/pkg/mpointers" "github.com/google/uuid" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -21,7 +20,7 @@ func TestListAccountsByAlias(t *testing.T) { mockAccountRepo := account.NewMockRepository(ctrl) - uc := &query.UseCase{ + uc := &UseCase{ AccountRepo: mockAccountRepo, } diff --git a/components/ledger/internal/services/command/get-ids-accounts.go b/components/ledger/internal/services/query/get-ids-accounts.go similarity index 98% rename from components/ledger/internal/services/command/get-ids-accounts.go rename to components/ledger/internal/services/query/get-ids-accounts.go index bcc55fd0..7074f0de 100644 --- a/components/ledger/internal/services/command/get-ids-accounts.go +++ b/components/ledger/internal/services/query/get-ids-accounts.go @@ -1,4 +1,4 @@ -package command +package query import ( "context" diff --git a/components/ledger/internal/services/command/get-ids-accounts_test.go b/components/ledger/internal/services/query/get-ids-accounts_test.go similarity index 96% rename from components/ledger/internal/services/command/get-ids-accounts_test.go rename to components/ledger/internal/services/query/get-ids-accounts_test.go index 6044d939..e690770c 100644 --- a/components/ledger/internal/services/command/get-ids-accounts_test.go +++ b/components/ledger/internal/services/query/get-ids-accounts_test.go @@ -1,9 +1,8 @@ -package command +package query import ( "context" "errors" - "github.com/LerianStudio/midaz/components/ledger/internal/services/query" "testing" "github.com/LerianStudio/midaz/components/ledger/internal/adapters/postgres/account" @@ -20,7 +19,7 @@ func TestListAccountsByIDs(t *testing.T) { mockAccountRepo := account.NewMockRepository(ctrl) - uc := &query.UseCase{ + uc := &UseCase{ AccountRepo: mockAccountRepo, } diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 872f80c2..e8b80115 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -446,7 +446,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L if err != nil { mopentelemetry.HandleSpanError(&spanCreateTransaction, "Failed to convert parserDSL from struct to JSON string", err) - return err + return http.WithError(c, err) } tran, err := handler.Command.CreateTransaction(ctxCreateTransaction, organizationID, ledgerID, &parserDSL) @@ -508,13 +508,13 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L spanReleaseLock.End() ctxUpdateTransactionStatus, spanUpdateTransactionStatus := tracer.Start(ctx, "handler.update_accounts.update_transaction_status") - _, err = handler.Command.UpdateTransactionStatus(ctxUpdateTransactionStatus, organizationID, ledgerID, tran.IDtoUUID(), constant.DECLINED) - if err != nil { + _, er := handler.Command.UpdateTransactionStatus(ctxUpdateTransactionStatus, organizationID, ledgerID, tran.IDtoUUID(), constant.DECLINED) + if er != nil { mopentelemetry.HandleSpanError(&spanUpdateTransactionStatus, "Failed to update transaction status", err) logger.Errorf("Failed to update Transaction with ID: %s, Error: %s", tran.ID, err.Error()) - return http.WithError(c, err) + return http.WithError(c, er) } spanUpdateTransactionStatus.End() From 60da1cf7dce25469c87bf5786aa6b603fbe79638 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Mon, 13 Jan 2025 18:32:37 +0100 Subject: [PATCH 32/49] feat: adjust time :sparkles: --- pkg/constant/account.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/constant/account.go b/pkg/constant/account.go index 413681d3..81ea9144 100644 --- a/pkg/constant/account.go +++ b/pkg/constant/account.go @@ -4,7 +4,7 @@ const ( DefaultExternalAccountAliasPrefix = "@external/" ExternalAccountType = "external" TimeSetLock = 1 - TimeSetLockBalance = 10 - LockRetry = 200 + TimeSetLockBalance = 5 + LockRetry = 100 RedisTimesRetry = 3 ) From ff3a8ad792b1b592fb3faa93bcd86dc4f45a1572 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Mon, 13 Jan 2025 18:49:41 +0100 Subject: [PATCH 33/49] fix: make lint :bug: --- .../postgres/account/account.postgresql.go | 140 +++++++++--------- .../internal/adapters/http/in/transaction.go | 14 +- .../command/create-idempotency-key.go | 2 +- .../command/create-lock-race-condition.go | 4 +- pkg/gold/transaction/model/validations.go | 1 - .../transaction/model/validations_test.go | 4 +- 6 files changed, 85 insertions(+), 80 deletions(-) diff --git a/components/ledger/internal/adapters/postgres/account/account.postgresql.go b/components/ledger/internal/adapters/postgres/account/account.postgresql.go index 26b4b886..1910de40 100644 --- a/components/ledger/internal/adapters/postgres/account/account.postgresql.go +++ b/components/ledger/internal/adapters/postgres/account/account.postgresql.go @@ -271,7 +271,7 @@ func (r *AccountPostgreSQLRepository) Find(ctx context.Context, organizationID, query += " ORDER BY created_at DESC" - account := &AccountPostgreSQLModel{} + acc := &AccountPostgreSQLModel{} ctx, spanQuery := tracer.Start(ctx, "postgres.find.query") @@ -280,27 +280,27 @@ func (r *AccountPostgreSQLRepository) Find(ctx context.Context, organizationID, spanQuery.End() if err := row.Scan( - &account.ID, - &account.Name, - &account.ParentAccountID, - &account.EntityID, - &account.AssetCode, - &account.OrganizationID, - &account.LedgerID, - &account.PortfolioID, - &account.ProductID, - &account.AvailableBalance, - &account.OnHoldBalance, - &account.BalanceScale, - &account.Status, - &account.StatusDescription, - &account.AllowSending, - &account.AllowReceiving, - &account.Alias, - &account.Type, - &account.CreatedAt, - &account.UpdatedAt, - &account.DeletedAt, + &acc.ID, + &acc.Name, + &acc.ParentAccountID, + &acc.EntityID, + &acc.AssetCode, + &acc.OrganizationID, + &acc.LedgerID, + &acc.PortfolioID, + &acc.ProductID, + &acc.AvailableBalance, + &acc.OnHoldBalance, + &acc.BalanceScale, + &acc.Status, + &acc.StatusDescription, + &acc.AllowSending, + &acc.AllowReceiving, + &acc.Alias, + &acc.Type, + &acc.CreatedAt, + &acc.UpdatedAt, + &acc.DeletedAt, ); err != nil { mopentelemetry.HandleSpanError(&span, "Failed to scan row", err) @@ -311,7 +311,7 @@ func (r *AccountPostgreSQLRepository) Find(ctx context.Context, organizationID, return nil, err } - return account.ToEntity(), nil + return acc.ToEntity(), nil } // FindWithDeleted retrieves an Account entity from the database using the provided ID (including soft-deleted ones). @@ -339,7 +339,7 @@ func (r *AccountPostgreSQLRepository) FindWithDeleted(ctx context.Context, organ query += " ORDER BY created_at DESC" - account := &AccountPostgreSQLModel{} + acc := &AccountPostgreSQLModel{} ctx, spanQuery := tracer.Start(ctx, "postgres.find_with_deleted.query") @@ -348,27 +348,27 @@ func (r *AccountPostgreSQLRepository) FindWithDeleted(ctx context.Context, organ spanQuery.End() if err := row.Scan( - &account.ID, - &account.Name, - &account.ParentAccountID, - &account.EntityID, - &account.AssetCode, - &account.OrganizationID, - &account.LedgerID, - &account.PortfolioID, - &account.ProductID, - &account.AvailableBalance, - &account.OnHoldBalance, - &account.BalanceScale, - &account.Status, - &account.StatusDescription, - &account.AllowSending, - &account.AllowReceiving, - &account.Alias, - &account.Type, - &account.CreatedAt, - &account.UpdatedAt, - &account.DeletedAt, + &acc.ID, + &acc.Name, + &acc.ParentAccountID, + &acc.EntityID, + &acc.AssetCode, + &acc.OrganizationID, + &acc.LedgerID, + &acc.PortfolioID, + &acc.ProductID, + &acc.AvailableBalance, + &acc.OnHoldBalance, + &acc.BalanceScale, + &acc.Status, + &acc.StatusDescription, + &acc.AllowSending, + &acc.AllowReceiving, + &acc.Alias, + &acc.Type, + &acc.CreatedAt, + &acc.UpdatedAt, + &acc.DeletedAt, ); err != nil { mopentelemetry.HandleSpanError(&span, "Failed to scan row", err) @@ -379,7 +379,7 @@ func (r *AccountPostgreSQLRepository) FindWithDeleted(ctx context.Context, organ return nil, err } - return account.ToEntity(), nil + return acc.ToEntity(), nil } // FindAlias retrieves an Account entity from the database using the provided Alias (including soft-deleted ones). @@ -407,7 +407,7 @@ func (r *AccountPostgreSQLRepository) FindAlias(ctx context.Context, organizatio query += " ORDER BY created_at DESC" - account := &AccountPostgreSQLModel{} + acc := &AccountPostgreSQLModel{} ctx, spanQuery := tracer.Start(ctx, "postgres.find_with_deleted.query") @@ -416,27 +416,27 @@ func (r *AccountPostgreSQLRepository) FindAlias(ctx context.Context, organizatio spanQuery.End() if err := row.Scan( - &account.ID, - &account.Name, - &account.ParentAccountID, - &account.EntityID, - &account.AssetCode, - &account.OrganizationID, - &account.LedgerID, - &account.PortfolioID, - &account.ProductID, - &account.AvailableBalance, - &account.OnHoldBalance, - &account.BalanceScale, - &account.Status, - &account.StatusDescription, - &account.AllowSending, - &account.AllowReceiving, - &account.Alias, - &account.Type, - &account.CreatedAt, - &account.UpdatedAt, - &account.DeletedAt, + &acc.ID, + &acc.Name, + &acc.ParentAccountID, + &acc.EntityID, + &acc.AssetCode, + &acc.OrganizationID, + &acc.LedgerID, + &acc.PortfolioID, + &acc.ProductID, + &acc.AvailableBalance, + &acc.OnHoldBalance, + &acc.BalanceScale, + &acc.Status, + &acc.StatusDescription, + &acc.AllowSending, + &acc.AllowReceiving, + &acc.Alias, + &acc.Type, + &acc.CreatedAt, + &acc.UpdatedAt, + &acc.DeletedAt, ); err != nil { mopentelemetry.HandleSpanError(&span, "Failed to scan row", err) @@ -447,7 +447,7 @@ func (r *AccountPostgreSQLRepository) FindAlias(ctx context.Context, organizatio return nil, err } - return account.ToEntity(), nil + return acc.ToEntity(), nil } // FindByAlias find account from the database using Organization and Ledger id and Alias. Returns true and ErrAliasUnavailability error if the alias is already taken. @@ -1033,6 +1033,7 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi for _, acc := range accounts { var updates []string + var args []any updates = append(updates, "available_balance = $"+strconv.Itoa(len(args)+1)) @@ -1079,7 +1080,6 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi return err } - } if err := tx.Commit(); err != nil { diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index e8b80115..2bdc2d9e 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -62,7 +62,7 @@ func (handler *TransactionHandler) CreateTransactionJSON(p any, c *fiber.Ctx) er parserDSL := input.FromDSl() logger.Infof("Request to create an transaction with details: %#v", parserDSL) - response := handler.createTransaction(c, logger, *parserDSL, false) + response := handler.createTransaction(c, logger, *parserDSL) return response } @@ -130,7 +130,7 @@ func (handler *TransactionHandler) CreateTransactionDSL(c *fiber.Ctx) error { return http.WithError(c, err) } - response := handler.createTransaction(c, logger, parserDSL, false) + response := handler.createTransaction(c, logger, parserDSL) return response } @@ -379,7 +379,7 @@ func (handler *TransactionHandler) GetAllTransactions(c *fiber.Ctx) error { } // createTransaction func that received struct from DSL parsed and create Transaction -func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.Logger, parserDSL goldModel.Transaction, isLocked bool) error { +func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.Logger, parserDSL goldModel.Transaction) error { ctx := c.UserContext() tracer := pkg.NewTracerFromContext(ctx) @@ -509,6 +509,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L ctxUpdateTransactionStatus, spanUpdateTransactionStatus := tracer.Start(ctx, "handler.update_accounts.update_transaction_status") _, er := handler.Command.UpdateTransactionStatus(ctxUpdateTransactionStatus, organizationID, ledgerID, tran.IDtoUUID(), constant.DECLINED) + if er != nil { mopentelemetry.HandleSpanError(&spanUpdateTransactionStatus, "Failed to update transaction status", err) @@ -526,6 +527,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L ctxUpdateTransactionStatus, spanUpdateTransactionStatus := tracer.Start(ctx, "handler.create_transaction.update_transaction_status") _, err = handler.Command.UpdateTransactionStatus(ctxUpdateTransactionStatus, organizationID, ledgerID, tran.IDtoUUID(), constant.APPROVED) + if err != nil { mopentelemetry.HandleSpanError(&spanUpdateTransactionStatus, "Failed to update transaction status", err) @@ -571,7 +573,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L } // getAccounts is a function that split aliases and ids, call the properly function and return Accounts -func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, logger mlog.Logger, token, hash string, organizationID, ledgerID uuid.UUID, validate *goldModel.Responses, transaction goldModel.Transaction) ([]*account.Account, error) { +func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, logger mlog.Logger, token, hash string, organizationID, ledgerID uuid.UUID, validate *goldModel.Responses, body goldModel.Transaction) ([]*account.Account, error) { span := trace.SpanFromContext(ctx) defer span.End() @@ -582,7 +584,7 @@ func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, l return nil, err } - err = goldModel.ValidateAccounts(transaction, *validate, accounts) + err = goldModel.ValidateAccounts(body, *validate, accounts) if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to validate accounts", err) @@ -601,7 +603,7 @@ func (handler *TransactionHandler) getAccountsAndValidate(ctx context.Context, l } if searchAgain { - return handler.getAccountsAndValidate(ctx, logger, token, hash, organizationID, ledgerID, validate, transaction) + return handler.getAccountsAndValidate(ctx, logger, token, hash, organizationID, ledgerID, validate, body) } return accounts, nil diff --git a/components/transaction/internal/services/command/create-idempotency-key.go b/components/transaction/internal/services/command/create-idempotency-key.go index bc09f625..5b994c14 100644 --- a/components/transaction/internal/services/command/create-idempotency-key.go +++ b/components/transaction/internal/services/command/create-idempotency-key.go @@ -13,7 +13,7 @@ func (uc *UseCase) CreateOrCheckIdempotencyKey(ctx context.Context, organization logger := pkg.NewLoggerFromContext(ctx) tracer := pkg.NewTracerFromContext(ctx) - ctx, span := tracer.Start(ctx, "command.create-idempotency-key") + _, span := tracer.Start(ctx, "command.create-idempotency-key") defer span.End() logger.Infof("Trying to create or check idempotency key in redis") diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index 6a3c2a55..1d40689a 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -22,6 +22,7 @@ func (uc *UseCase) AllKeysUnlocked(ctx context.Context, organizationID, ledgerID defer span.End() var wg sync.WaitGroup + resultChan := make(chan bool, len(keys)) for _, key := range keys { @@ -30,6 +31,7 @@ func (uc *UseCase) AllKeysUnlocked(ctx context.Context, organizationID, ledgerID logger.Infof("Account try to lock on redis: %v", internalKey) wg.Add(1) + go uc.checkAndReleaseLock(ctx, &wg, internalKey, hash, resultChan) } @@ -41,7 +43,7 @@ func (uc *UseCase) checkAndReleaseLock(ctx context.Context, wg *sync.WaitGroup, logger := pkg.NewLoggerFromContext(context.Background()) tracer := pkg.NewTracerFromContext(context.Background()) - ctx, span := tracer.Start(ctx, "redis.check_and_release_lock") + _, span := tracer.Start(ctx, "redis.check_and_release_lock") defer span.End() defer wg.Done() diff --git a/pkg/gold/transaction/model/validations.go b/pkg/gold/transaction/model/validations.go index 85b14c3f..be98aa8c 100644 --- a/pkg/gold/transaction/model/validations.go +++ b/pkg/gold/transaction/model/validations.go @@ -28,7 +28,6 @@ func ValidateAccounts(transaction Transaction, validate Responses, accounts []*a if err := validateBalance(transaction, validate.From, acc); err != nil { return err } - } return nil diff --git a/pkg/gold/transaction/model/validations_test.go b/pkg/gold/transaction/model/validations_test.go index 2095ebad..5bc9c69d 100644 --- a/pkg/gold/transaction/model/validations_test.go +++ b/pkg/gold/transaction/model/validations_test.go @@ -14,6 +14,7 @@ import ( func TestValidateAccounts(t *testing.T) { tests := []struct { name string + tran Transaction validate Responses accounts []*a.Account expectedError error @@ -65,7 +66,7 @@ func TestValidateAccounts(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateAccounts(tt.validate, tt.accounts) + err := ValidateAccounts(tt.tran, tt.validate, tt.accounts) if (err != nil && tt.expectedError == nil) || (err == nil && tt.expectedError != nil) { t.Fatalf("Expected error: %v, got: %v", tt.expectedError, err) @@ -82,6 +83,7 @@ func TestValidateFromToOperation(t *testing.T) { tests := []struct { name string ft FromTo + tran Transaction validate Responses acc *a.Account expectedError error From 40a294ee420d0c275b358dde10b2d1765433ed2f Mon Sep 17 00:00:00 2001 From: lerian-studio Date: Mon, 13 Jan 2025 17:49:41 +0000 Subject: [PATCH 34/49] chore(release): update version in .env.example to v1.45.0 --- chocolatey/tools/chocolateyinstall.ps1 | 2 +- components/audit/.env.example | 2 +- components/ledger/.env.example | 2 +- components/mdz/.env.example | 2 +- components/transaction/.env.example | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chocolatey/tools/chocolateyinstall.ps1 b/chocolatey/tools/chocolateyinstall.ps1 index b817fc46..d4875150 100644 --- a/chocolatey/tools/chocolateyinstall.ps1 +++ b/chocolatey/tools/chocolateyinstall.ps1 @@ -1,4 +1,4 @@ -$version = 'v1.44.1' +$version = 'v1.45.0' $ErrorActionPreference = 'Stop'; diff --git a/components/audit/.env.example b/components/audit/.env.example index 59735e70..c74246e8 100644 --- a/components/audit/.env.example +++ b/components/audit/.env.example @@ -2,7 +2,7 @@ # ENV_NAME=production # APP -VERSION=v1.44.1 +VERSION=v1.45.0 SERVER_PORT=3005 SERVER_ADDRESS=:${SERVER_PORT} diff --git a/components/ledger/.env.example b/components/ledger/.env.example index 48bb6090..bb0b58c2 100644 --- a/components/ledger/.env.example +++ b/components/ledger/.env.example @@ -4,7 +4,7 @@ # ENV_NAME=production # APP -VERSION=v1.44.1 +VERSION=v1.45.0 SERVER_PORT=3000 SERVER_ADDRESS=:${SERVER_PORT} diff --git a/components/mdz/.env.example b/components/mdz/.env.example index cfc5d36e..d88cea36 100644 --- a/components/mdz/.env.example +++ b/components/mdz/.env.example @@ -2,4 +2,4 @@ CLIENT_ID=9670e0ca55a29a466d31 CLIENT_SECRET=dd03f916cacf4a98c6a413d9c38ba102dce436a9 URL_API_AUTH=http://127.0.0.1:8080 URL_API_LEDGER=http://127.0.0.1:3000 -VERSION=v1.44.1 +VERSION=v1.45.0 diff --git a/components/transaction/.env.example b/components/transaction/.env.example index 2025762d..2640ac7e 100644 --- a/components/transaction/.env.example +++ b/components/transaction/.env.example @@ -4,7 +4,7 @@ # ENV_NAME=production # APP -VERSION=v1.44.1 +VERSION=v1.45.0 APP_CONTEXT=/transaction/v1 SERVER_PORT=3002 SERVER_ADDRESS=:${SERVER_PORT} From a1e82dace5122444d15300350f64cab4ffa59224 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Mon, 13 Jan 2025 18:55:05 +0100 Subject: [PATCH 35/49] Merge branch 'feature/MIDAZ-420-b' of ssh://github.com/LerianStudio/midaz into feature/MIDAZ-420-b --- chocolatey/tools/chocolateyinstall.ps1 | 2 +- components/audit/.env.example | 2 +- components/ledger/.env.example | 2 +- components/mdz/.env.example | 2 +- components/transaction/.env.example | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chocolatey/tools/chocolateyinstall.ps1 b/chocolatey/tools/chocolateyinstall.ps1 index b817fc46..d4875150 100644 --- a/chocolatey/tools/chocolateyinstall.ps1 +++ b/chocolatey/tools/chocolateyinstall.ps1 @@ -1,4 +1,4 @@ -$version = 'v1.44.1' +$version = 'v1.45.0' $ErrorActionPreference = 'Stop'; diff --git a/components/audit/.env.example b/components/audit/.env.example index 59735e70..c74246e8 100644 --- a/components/audit/.env.example +++ b/components/audit/.env.example @@ -2,7 +2,7 @@ # ENV_NAME=production # APP -VERSION=v1.44.1 +VERSION=v1.45.0 SERVER_PORT=3005 SERVER_ADDRESS=:${SERVER_PORT} diff --git a/components/ledger/.env.example b/components/ledger/.env.example index 48bb6090..bb0b58c2 100644 --- a/components/ledger/.env.example +++ b/components/ledger/.env.example @@ -4,7 +4,7 @@ # ENV_NAME=production # APP -VERSION=v1.44.1 +VERSION=v1.45.0 SERVER_PORT=3000 SERVER_ADDRESS=:${SERVER_PORT} diff --git a/components/mdz/.env.example b/components/mdz/.env.example index cfc5d36e..d88cea36 100644 --- a/components/mdz/.env.example +++ b/components/mdz/.env.example @@ -2,4 +2,4 @@ CLIENT_ID=9670e0ca55a29a466d31 CLIENT_SECRET=dd03f916cacf4a98c6a413d9c38ba102dce436a9 URL_API_AUTH=http://127.0.0.1:8080 URL_API_LEDGER=http://127.0.0.1:3000 -VERSION=v1.44.1 +VERSION=v1.45.0 diff --git a/components/transaction/.env.example b/components/transaction/.env.example index 2025762d..2640ac7e 100644 --- a/components/transaction/.env.example +++ b/components/transaction/.env.example @@ -4,7 +4,7 @@ # ENV_NAME=production # APP -VERSION=v1.44.1 +VERSION=v1.45.0 APP_CONTEXT=/transaction/v1 SERVER_PORT=3002 SERVER_ADDRESS=:${SERVER_PORT} From da168e176c58d2d093f336640cfab808226c7fc1 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Mon, 13 Jan 2025 18:55:17 +0100 Subject: [PATCH 36/49] Update account.go --- components/ledger/internal/adapters/grpc/in/account.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/components/ledger/internal/adapters/grpc/in/account.go b/components/ledger/internal/adapters/grpc/in/account.go index 9215a163..90502791 100644 --- a/components/ledger/internal/adapters/grpc/in/account.go +++ b/components/ledger/internal/adapters/grpc/in/account.go @@ -204,14 +204,7 @@ func (ap *AccountProto) UpdateAccounts(ctx context.Context, update *account.Acco ctx, span := tracer.Start(ctx, "handler.UpdateAccounts") defer span.End() - - uuids := make([]uuid.UUID, 0) - aliases := make([]string, 0) - for _, getAcc := range update.GetAccounts() { - uuids = append(uuids, uuid.MustParse(getAcc.Id)) - aliases = append(aliases, getAcc.Alias) - } - + organizationID, err := uuid.Parse(update.OrganizationId) if err != nil { return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), organizationID) From 0cdabd13e58d91d6f86170c70baf4d602690bd16 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Mon, 13 Jan 2025 19:17:13 +0100 Subject: [PATCH 37/49] fix: add defer rollback :bug: --- .../postgres/account/account.postgresql.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/components/ledger/internal/adapters/postgres/account/account.postgresql.go b/components/ledger/internal/adapters/postgres/account/account.postgresql.go index 1910de40..26c24e1a 100644 --- a/components/ledger/internal/adapters/postgres/account/account.postgresql.go +++ b/components/ledger/internal/adapters/postgres/account/account.postgresql.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "github.com/LerianStudio/midaz/pkg/mgrpc/account" + "github.com/bxcodec/dbresolver/v2" "reflect" "strconv" "strings" @@ -1031,6 +1032,13 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi return err } + defer func(tx dbresolver.Tx) { + rollbackErr := tx.Rollback() + if rollbackErr != nil { + mopentelemetry.HandleSpanError(&span, "Failed to rollback transaction", rollbackErr) + } + }(tx) + for _, acc := range accounts { var updates []string @@ -1074,7 +1082,7 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi } if rowsAffected == 0 { - err = pkg.ValidateBusinessError(constant.ErrLockVersionAccountBalance, reflect.TypeOf(mmodel.Account{}).Name()) + err := pkg.ValidateBusinessError(constant.ErrLockVersionAccountBalance, reflect.TypeOf(mmodel.Account{}).Name()) mopentelemetry.HandleSpanError(&span, "Failed to update account", err) @@ -1082,12 +1090,12 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi } } - if err := tx.Commit(); err != nil { + if commitErr := tx.Commit(); commitErr != nil { err := pkg.ValidateBusinessError(constant.ErrEntityNotFound, reflect.TypeOf(mmodel.Account{}).Name()) mopentelemetry.HandleSpanError(&span, "Failed to commit accounts", err) - return err + return commitErr } return nil From 66d74168b2da61b5fc74662ff7150403fb624b36 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Mon, 13 Jan 2025 20:27:47 +0100 Subject: [PATCH 38/49] fix: add rollback in case of error to unlock database; :bug: --- .../postgres/account/account.postgresql.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/components/ledger/internal/adapters/postgres/account/account.postgresql.go b/components/ledger/internal/adapters/postgres/account/account.postgresql.go index 26c24e1a..42b04e7b 100644 --- a/components/ledger/internal/adapters/postgres/account/account.postgresql.go +++ b/components/ledger/internal/adapters/postgres/account/account.postgresql.go @@ -1071,6 +1071,11 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to update account", err) + rollbackErr := tx.Rollback() + if rollbackErr != nil { + mopentelemetry.HandleSpanError(&span, "Failed to rollback transaction", rollbackErr) + } + return err } @@ -1078,6 +1083,11 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi if err != nil { mopentelemetry.HandleSpanError(&span, "Failed to get rows affected", err) + rollbackErr := tx.Rollback() + if rollbackErr != nil { + mopentelemetry.HandleSpanError(&span, "Failed to rollback transaction", rollbackErr) + } + return err } @@ -1086,6 +1096,11 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi mopentelemetry.HandleSpanError(&span, "Failed to update account", err) + rollbackErr := tx.Rollback() + if rollbackErr != nil { + mopentelemetry.HandleSpanError(&span, "Failed to rollback transaction", rollbackErr) + } + return err } } From f776fcc678cbbb652dfb01ff6ab7890b6ac85777 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Tue, 14 Jan 2025 10:57:21 +0100 Subject: [PATCH 39/49] fix: update go mod dependabot :bug: --- go.mod | 8 ++++---- go.sum | 18 ++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 150f9848..70ca8b54 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/go-playground/locales v0.14.1 github.com/go-playground/universal-translator v0.18.1 github.com/gofiber/fiber/v2 v2.52.6 - github.com/google/trillian v1.7.0 + github.com/google/trillian v1.7.1 github.com/icrowley/fake v0.0.0-20240710202011-f797eb4a99c0 github.com/jackc/pgx/v5 v5.7.2 github.com/jarcoal/httpmock v1.3.1 @@ -29,7 +29,7 @@ require ( github.com/swaggo/fiber-swagger v1.3.0 github.com/swaggo/swag v1.16.4 github.com/transparency-dev/merkle v0.0.2 - go.mongodb.org/mongo-driver v1.17.1 + go.mongodb.org/mongo-driver v1.17.2 go.opentelemetry.io/contrib/bridges/otelzap v0.8.0 go.opentelemetry.io/otel v1.33.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0 @@ -43,7 +43,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.33.0 go.opentelemetry.io/otel/trace v1.33.0 go.uber.org/mock v0.5.0 - google.golang.org/grpc v1.69.2 + google.golang.org/grpc v1.69.4 google.golang.org/protobuf v1.36.2 gotest.tools v2.2.0+incompatible ) @@ -132,7 +132,7 @@ require ( github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.17.11 // indirect github.com/lestrrat-go/jwx v1.2.30 - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible diff --git a/go.sum b/go.sum index 1c34b968..4d339505 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/trillian v1.7.0 h1:Oib7mKRvZ0Z3GjvNcn2C4clRmFouEOkBcbzw7q8JlFI= -github.com/google/trillian v1.7.0/go.mod h1:JMp1zzzHe7j2m9m8P/eTWOaoon3R/SwgqUnFMhm4vfw= +github.com/google/trillian v1.7.1 h1:+zX8jLM3524bAMPS+VxaDIDgsMv3/ty6DuLWerHXcek= +github.com/google/trillian v1.7.1/go.mod h1:E1UMAHqpZCA8AQdrKdWmHmtUfSeiD0sDWD1cv00Xa+c= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -192,9 +192,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= @@ -301,8 +300,8 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zU github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= -go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= +go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/bridges/otelzap v0.8.0 h1:4jqXEd0FGULFBy1bF1ledBePc0Ssu8YVddTgr8BXDTc= @@ -385,7 +384,6 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= @@ -415,8 +413,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1: google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw= google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= -google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= -google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 25fdb706ef65ec550172bb7f6d47652eb8f944f5 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Tue, 14 Jan 2025 13:15:04 +0100 Subject: [PATCH 40/49] feat: add go routines to update; some postgres configs :sparkles: --- .../postgres/account/account.postgresql.go | 94 +++++++++---------- pkg/mpostgres/postgres.go | 10 ++ 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/components/ledger/internal/adapters/postgres/account/account.postgresql.go b/components/ledger/internal/adapters/postgres/account/account.postgresql.go index 42b04e7b..540d7c6b 100644 --- a/components/ledger/internal/adapters/postgres/account/account.postgresql.go +++ b/components/ledger/internal/adapters/postgres/account/account.postgresql.go @@ -5,10 +5,10 @@ import ( "database/sql" "errors" "github.com/LerianStudio/midaz/pkg/mgrpc/account" - "github.com/bxcodec/dbresolver/v2" "reflect" "strconv" "strings" + "sync" "time" "github.com/LerianStudio/midaz/pkg/mpointers" @@ -1032,75 +1032,65 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi return err } - defer func(tx dbresolver.Tx) { - rollbackErr := tx.Rollback() - if rollbackErr != nil { - mopentelemetry.HandleSpanError(&span, "Failed to rollback transaction", rollbackErr) - } - }(tx) + var wg sync.WaitGroup + errChan := make(chan error, len(accounts)) for _, acc := range accounts { - var updates []string - - var args []any + wg.Add(1) - updates = append(updates, "available_balance = $"+strconv.Itoa(len(args)+1)) - args = append(args, acc.Balance.Available) + go func(acc *account.Account) { + defer wg.Done() + var updates []string + var args []interface{} - updates = append(updates, "on_hold_balance = $"+strconv.Itoa(len(args)+1)) - args = append(args, acc.Balance.OnHold) + updates = append(updates, "available_balance = $"+strconv.Itoa(len(args)+1)) + args = append(args, acc.Balance.Available) - updates = append(updates, "balance_scale = $"+strconv.Itoa(len(args)+1)) - args = append(args, acc.Balance.Scale) + updates = append(updates, "on_hold_balance = $"+strconv.Itoa(len(args)+1)) + args = append(args, acc.Balance.OnHold) - updates = append(updates, "version = $"+strconv.Itoa(len(args)+1)) - version := acc.Version + 1 - args = append(args, version) + updates = append(updates, "balance_scale = $"+strconv.Itoa(len(args)+1)) + args = append(args, acc.Balance.Scale) - updates = append(updates, "updated_at = $"+strconv.Itoa(len(args)+1)) - args = append(args, time.Now(), organizationID, ledgerID, acc.Id, acc.Version) + updates = append(updates, "version = $"+strconv.Itoa(len(args)+1)) + version := acc.Version + 1 + args = append(args, version) - query := `UPDATE account SET ` + strings.Join(updates, ", ") + - ` WHERE organization_id = $` + strconv.Itoa(len(args)-3) + - ` AND ledger_id = $` + strconv.Itoa(len(args)-2) + - ` AND id = $` + strconv.Itoa(len(args)-1) + - ` AND version = $` + strconv.Itoa(len(args)) + - ` AND deleted_at IS NULL` + updates = append(updates, "updated_at = $"+strconv.Itoa(len(args)+1)) + args = append(args, time.Now(), organizationID, ledgerID, acc.Id, acc.Version) - result, err := tx.ExecContext(ctx, query, args...) - if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to update account", err) + query := `UPDATE account SET ` + strings.Join(updates, ", ") + + ` WHERE organization_id = $` + strconv.Itoa(len(args)-3) + + ` AND ledger_id = $` + strconv.Itoa(len(args)-2) + + ` AND id = $` + strconv.Itoa(len(args)-1) + + ` AND version = $` + strconv.Itoa(len(args)) + + ` AND deleted_at IS NULL` - rollbackErr := tx.Rollback() - if rollbackErr != nil { - mopentelemetry.HandleSpanError(&span, "Failed to rollback transaction", rollbackErr) + result, err := tx.ExecContext(ctx, query, args...) + if err != nil { + errChan <- err + return } - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to get rows affected", err) - - rollbackErr := tx.Rollback() - if rollbackErr != nil { - mopentelemetry.HandleSpanError(&span, "Failed to rollback transaction", rollbackErr) + rowsAffected, err := result.RowsAffected() + if err != nil || rowsAffected == 0 { + if err == nil { + err = sql.ErrNoRows + } + errChan <- err } + }(acc) + } - return err - } - - if rowsAffected == 0 { - err := pkg.ValidateBusinessError(constant.ErrLockVersionAccountBalance, reflect.TypeOf(mmodel.Account{}).Name()) - - mopentelemetry.HandleSpanError(&span, "Failed to update account", err) + wg.Wait() + close(errChan) + for err := range errChan { + if err != nil { rollbackErr := tx.Rollback() if rollbackErr != nil { - mopentelemetry.HandleSpanError(&span, "Failed to rollback transaction", rollbackErr) + return rollbackErr } - return err } } diff --git a/pkg/mpostgres/postgres.go b/pkg/mpostgres/postgres.go index 6527fb2e..e81f06ff 100644 --- a/pkg/mpostgres/postgres.go +++ b/pkg/mpostgres/postgres.go @@ -6,6 +6,8 @@ import ( "go.uber.org/zap" "net/url" "path/filepath" + "time" + // File system migration source. We need to import it to be able to use it as source in migrate.NewWithSourceInstance "github.com/LerianStudio/midaz/pkg/mlog" @@ -39,12 +41,20 @@ func (pc *PostgresConnection) Connect() error { return nil } + dbPrimary.SetMaxOpenConns(100) + dbPrimary.SetMaxIdleConns(100) + dbPrimary.SetConnMaxLifetime(time.Minute * 5) + dbReadOnlyReplica, err := sql.Open("pgx", pc.ConnectionStringReplica) if err != nil { pc.Logger.Fatal("failed to open connect to replica database", zap.Error(err)) return nil } + dbReadOnlyReplica.SetMaxOpenConns(100) + dbReadOnlyReplica.SetMaxIdleConns(100) + dbReadOnlyReplica.SetConnMaxLifetime(time.Minute * 5) + connectionDB := dbresolver.New( dbresolver.WithPrimaryDBs(dbPrimary), dbresolver.WithReplicaDBs(dbReadOnlyReplica), From a7df566659892e15eddc0486087d93a74cf707d4 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Tue, 14 Jan 2025 14:26:59 +0100 Subject: [PATCH 41/49] fix: lint; add version; :bug: --- .../adapters/postgres/account/account.postgresql.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/components/ledger/internal/adapters/postgres/account/account.postgresql.go b/components/ledger/internal/adapters/postgres/account/account.postgresql.go index 540d7c6b..3488a1b0 100644 --- a/components/ledger/internal/adapters/postgres/account/account.postgresql.go +++ b/components/ledger/internal/adapters/postgres/account/account.postgresql.go @@ -226,6 +226,7 @@ func (r *AccountPostgreSQLRepository) FindAll(ctx context.Context, organizationI &acc.AllowReceiving, &acc.Alias, &acc.Type, + &acc.Version, &acc.CreatedAt, &acc.UpdatedAt, &acc.DeletedAt, @@ -299,6 +300,7 @@ func (r *AccountPostgreSQLRepository) Find(ctx context.Context, organizationID, &acc.AllowReceiving, &acc.Alias, &acc.Type, + &acc.Version, &acc.CreatedAt, &acc.UpdatedAt, &acc.DeletedAt, @@ -367,6 +369,7 @@ func (r *AccountPostgreSQLRepository) FindWithDeleted(ctx context.Context, organ &acc.AllowReceiving, &acc.Alias, &acc.Type, + &acc.Version, &acc.CreatedAt, &acc.UpdatedAt, &acc.DeletedAt, @@ -435,6 +438,7 @@ func (r *AccountPostgreSQLRepository) FindAlias(ctx context.Context, organizatio &acc.AllowReceiving, &acc.Alias, &acc.Type, + &acc.Version, &acc.CreatedAt, &acc.UpdatedAt, &acc.DeletedAt, @@ -549,6 +553,7 @@ func (r *AccountPostgreSQLRepository) ListByIDs(ctx context.Context, organizatio &acc.AllowReceiving, &acc.Alias, &acc.Type, + &acc.Version, &acc.CreatedAt, &acc.UpdatedAt, &acc.DeletedAt, @@ -620,6 +625,7 @@ func (r *AccountPostgreSQLRepository) ListByAlias(ctx context.Context, organizat &acc.AllowReceiving, &acc.Alias, &acc.Type, + &acc.Version, &acc.CreatedAt, &acc.UpdatedAt, &acc.DeletedAt, @@ -1033,6 +1039,7 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi } var wg sync.WaitGroup + errChan := make(chan error, len(accounts)) for _, acc := range accounts { @@ -1040,8 +1047,10 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi go func(acc *account.Account) { defer wg.Done() + var updates []string - var args []interface{} + + var args []any updates = append(updates, "available_balance = $"+strconv.Itoa(len(args)+1)) args = append(args, acc.Balance.Available) @@ -1091,6 +1100,7 @@ func (r *AccountPostgreSQLRepository) UpdateAccounts(ctx context.Context, organi if rollbackErr != nil { return rollbackErr } + return err } } From d5b619788782563d2c336fccaa01f4584ac54e57 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Tue, 14 Jan 2025 20:01:22 +0100 Subject: [PATCH 42/49] feat: add transaction body on database; :sparkles: --- .../adapters/postgres/transaction/transaction.go | 4 ++++ .../transaction/transaction.postgresql.go | 16 +++++++++++++++- .../services/command/create-transaction.go | 1 + .../000000_create_transaction_table.up.sql | 1 + 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/components/transaction/internal/adapters/postgres/transaction/transaction.go b/components/transaction/internal/adapters/postgres/transaction/transaction.go index eab9e2fb..545dfab1 100644 --- a/components/transaction/internal/adapters/postgres/transaction/transaction.go +++ b/components/transaction/internal/adapters/postgres/transaction/transaction.go @@ -25,6 +25,7 @@ type TransactionPostgreSQLModel struct { ChartOfAccountsGroupName string LedgerID string OrganizationID string + Body goldModel.Transaction CreatedAt time.Time UpdatedAt time.Time DeletedAt sql.NullTime @@ -92,6 +93,7 @@ type Transaction struct { Destination []string `json:"destination" example:"@person2"` LedgerID string `json:"ledgerId" example:"00000000-0000-0000-0000-000000000000"` OrganizationID string `json:"organizationId" example:"00000000-0000-0000-0000-000000000000"` + Body goldModel.Transaction `json:"-"` CreatedAt time.Time `json:"createdAt" example:"2021-01-01T00:00:00Z"` UpdatedAt time.Time `json:"updatedAt" example:"2021-01-01T00:00:00Z"` DeletedAt *time.Time `json:"deletedAt" example:"2021-01-01T00:00:00Z"` @@ -123,6 +125,7 @@ func (t *TransactionPostgreSQLModel) ToEntity() *Transaction { ChartOfAccountsGroupName: t.ChartOfAccountsGroupName, LedgerID: t.LedgerID, OrganizationID: t.OrganizationID, + Body: t.Body, CreatedAt: t.CreatedAt, UpdatedAt: t.UpdatedAt, } @@ -150,6 +153,7 @@ func (t *TransactionPostgreSQLModel) FromEntity(transaction *Transaction) { ChartOfAccountsGroupName: transaction.ChartOfAccountsGroupName, LedgerID: transaction.LedgerID, OrganizationID: transaction.OrganizationID, + Body: transaction.Body, CreatedAt: transaction.CreatedAt, UpdatedAt: transaction.UpdatedAt, } diff --git a/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go b/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go index ff98293d..717c5c93 100644 --- a/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go +++ b/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go @@ -3,6 +3,7 @@ package transaction import ( "context" "database/sql" + "encoding/json" "errors" "github.com/LerianStudio/midaz/pkg/mpointers" "github.com/LerianStudio/midaz/pkg/net/http" @@ -80,7 +81,7 @@ func (r *TransactionPostgreSQLRepository) Create(ctx context.Context, transactio return nil, err } - result, err := db.ExecContext(ctx, `INSERT INTO transaction VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *`, + result, err := db.ExecContext(ctx, `INSERT INTO transaction VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING *`, record.ID, record.ParentTransactionID, record.Description, @@ -93,6 +94,7 @@ func (r *TransactionPostgreSQLRepository) Create(ctx context.Context, transactio record.ChartOfAccountsGroupName, record.LedgerID, record.OrganizationID, + record.Body, record.CreatedAt, record.UpdatedAt, record.DeletedAt, @@ -197,6 +199,7 @@ func (r *TransactionPostgreSQLRepository) FindAll(ctx context.Context, organizat &transaction.ChartOfAccountsGroupName, &transaction.LedgerID, &transaction.OrganizationID, + &transaction.Body, &transaction.CreatedAt, &transaction.UpdatedAt, &transaction.DeletedAt, @@ -273,6 +276,7 @@ func (r *TransactionPostgreSQLRepository) ListByIDs(ctx context.Context, organiz &transaction.ChartOfAccountsGroupName, &transaction.LedgerID, &transaction.OrganizationID, + &transaction.Body, &transaction.CreatedAt, &transaction.UpdatedAt, &transaction.DeletedAt, @@ -310,6 +314,8 @@ func (r *TransactionPostgreSQLRepository) Find(ctx context.Context, organization transaction := &TransactionPostgreSQLModel{} + var body string + ctx, spanQuery := tracer.Start(ctx, "postgres.find.query") row := db.QueryRowContext(ctx, "SELECT * FROM transaction WHERE organization_id = $1 AND ledger_id = $2 AND id = $3 AND deleted_at IS NULL", @@ -330,6 +336,7 @@ func (r *TransactionPostgreSQLRepository) Find(ctx context.Context, organization &transaction.ChartOfAccountsGroupName, &transaction.LedgerID, &transaction.OrganizationID, + &body, &transaction.CreatedAt, &transaction.UpdatedAt, &transaction.DeletedAt, @@ -343,6 +350,13 @@ func (r *TransactionPostgreSQLRepository) Find(ctx context.Context, organization return nil, err } + err = json.Unmarshal([]byte(body), &transaction.Body) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to unmarshal address", err) + + return nil, err + } + return transaction.ToEntity(), nil } diff --git a/components/transaction/internal/services/command/create-transaction.go b/components/transaction/internal/services/command/create-transaction.go index 9df4f4a5..ad42ccf4 100644 --- a/components/transaction/internal/services/command/create-transaction.go +++ b/components/transaction/internal/services/command/create-transaction.go @@ -46,6 +46,7 @@ func (uc *UseCase) CreateTransaction(ctx context.Context, organizationID, ledger AmountScale: &scale, AssetCode: t.Send.Asset, ChartOfAccountsGroupName: t.ChartOfAccountsGroupName, + Body: *t, CreatedAt: time.Now(), UpdatedAt: time.Now(), } diff --git a/components/transaction/migrations/000000_create_transaction_table.up.sql b/components/transaction/migrations/000000_create_transaction_table.up.sql index ac32ab00..0cf2e046 100644 --- a/components/transaction/migrations/000000_create_transaction_table.up.sql +++ b/components/transaction/migrations/000000_create_transaction_table.up.sql @@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS "transaction" ( chart_of_accounts_group_name TEXT NOT NULL, ledger_id UUID NOT NULL, organization_id UUID NOT NULL, + body JSONB NOT NULL, created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE, From a8f5a6deb065858eb90a3a9b74c641ecc304e4f5 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Wed, 15 Jan 2025 20:11:13 +0100 Subject: [PATCH 43/49] feat: new implementatios; :sparkles: --- .../internal/adapters/http/in/transaction.go | 68 ++++++++++++++- .../postgres/transaction/transaction.mock.go | 64 ++++++++------ .../transaction/transaction.postgresql.go | 85 ++++++++++++++++++- .../query/get-parent-id-transaction.go | 49 +++++++++++ .../query/get-parent-id-transaction_test.go | 62 ++++++++++++++ 5 files changed, 299 insertions(+), 29 deletions(-) create mode 100644 components/transaction/internal/services/query/get-parent-id-transaction.go create mode 100644 components/transaction/internal/services/query/get-parent-id-transaction_test.go diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 2bdc2d9e..3795aa2e 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -169,7 +169,18 @@ func (handler *TransactionHandler) CommitTransaction(c *fiber.Ctx) error { // RevertTransaction method that revert transaction created before // -// TODO: Implement this method and the swagger documentation related to it +// @Summary Revert a Transaction +// @Description Revert a Transaction with Transaction ID only +// @Tags Transactions +// @Accept json +// @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 transaction_id path string true "Transaction ID" +// @Success 200 {object} transaction.Transaction +// @Router /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/{transaction_id}/revert [post] func (handler *TransactionHandler) RevertTransaction(c *fiber.Ctx) error { ctx := c.UserContext() @@ -179,7 +190,60 @@ func (handler *TransactionHandler) RevertTransaction(c *fiber.Ctx) error { _, span := tracer.Start(ctx, "handler.revert_transaction") defer span.End() - return http.Created(c, logger) + organizationID := c.Locals("organization_id").(uuid.UUID) + ledgerID := c.Locals("ledger_id").(uuid.UUID) + transactionID := c.Locals("transaction_id").(uuid.UUID) + + parent, err := handler.Query.GetParentByTransactionID(ctx, organizationID, ledgerID, transactionID) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to retrieve transaction on query", err) + + logger.Errorf("Failed to retrieve Transaction with ID: %s, Error: %s", transactionID.String(), err.Error()) + + return http.WithError(c, err) + } + + tran, err := handler.Query.GetTransactionByID(ctx, organizationID, ledgerID, transactionID) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to retrieve transaction on query", err) + + logger.Errorf("Failed to retrieve Transaction with ID: %s, Error: %s", transactionID.String(), err.Error()) + + return http.WithError(c, err) + } + + logger.Infof("Successfully retrieved Transaction with ID: %s", transactionID.String()) + + newSource := goldModel.Source{ + From: tran.Body.Send.Distribute.To, + Remaining: tran.Body.Send.Distribute.Remaining, + } + + newDistribute := goldModel.Distribute{ + To: tran.Body.Send.Source.From, + Remaining: tran.Body.Send.Source.Remaining, + } + + send := goldModel.Send{ + Asset: tran.Body.Send.Asset, + Value: tran.Body.Send.Value, + Scale: tran.Body.Send.Scale, + Source: newSource, + Distribute: newDistribute, + } + + parserDSL := goldModel.Transaction{ + ChartOfAccountsGroupName: tran.Body.ChartOfAccountsGroupName, + Description: tran.Body.Description, + Code: tran.Body.Code, + Pending: tran.Body.Pending, + Metadata: tran.Body.Metadata, + Send: send, + } + + response := handler.createTransaction(c, logger, parserDSL) + + return response } // UpdateTransaction method that patch transaction created before diff --git a/components/transaction/internal/adapters/postgres/transaction/transaction.mock.go b/components/transaction/internal/adapters/postgres/transaction/transaction.mock.go index c6c62931..38dec921 100644 --- a/components/transaction/internal/adapters/postgres/transaction/transaction.mock.go +++ b/components/transaction/internal/adapters/postgres/transaction/transaction.mock.go @@ -22,7 +22,6 @@ import ( type MockRepository struct { ctrl *gomock.Controller recorder *MockRepositoryMockRecorder - isgomock struct{} } // MockRepositoryMockRecorder is the mock recorder for MockRepository. @@ -43,53 +42,53 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { } // Create mocks base method. -func (m *MockRepository) Create(ctx context.Context, transaction *Transaction) (*Transaction, error) { +func (m *MockRepository) Create(arg0 context.Context, arg1 *Transaction) (*Transaction, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", ctx, transaction) + ret := m.ctrl.Call(m, "Create", arg0, arg1) ret0, _ := ret[0].(*Transaction) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. -func (mr *MockRepositoryMockRecorder) Create(ctx, transaction any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Create(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), ctx, transaction) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), arg0, arg1) } // Delete mocks base method. -func (m *MockRepository) Delete(ctx context.Context, organizationID, ledgerID, id uuid.UUID) error { +func (m *MockRepository) Delete(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", ctx, organizationID, ledgerID, id) + ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. -func (mr *MockRepositoryMockRecorder) Delete(ctx, organizationID, ledgerID, id any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Delete(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), ctx, organizationID, ledgerID, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), arg0, arg1, arg2, arg3) } // Find mocks base method. -func (m *MockRepository) Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*Transaction, error) { +func (m *MockRepository) Find(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) (*Transaction, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Find", ctx, organizationID, ledgerID, id) + ret := m.ctrl.Call(m, "Find", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(*Transaction) 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 { +func (mr *MockRepositoryMockRecorder) Find(arg0, arg1, arg2, arg3 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, "Find", reflect.TypeOf((*MockRepository)(nil).Find), arg0, arg1, arg2, arg3) } // FindAll mocks base method. -func (m *MockRepository) FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID, filter http.Pagination) ([]*Transaction, http.CursorPagination, error) { +func (m *MockRepository) FindAll(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 http.Pagination) ([]*Transaction, http.CursorPagination, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindAll", ctx, organizationID, ledgerID, filter) + ret := m.ctrl.Call(m, "FindAll", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]*Transaction) ret1, _ := ret[1].(http.CursorPagination) ret2, _ := ret[2].(error) @@ -97,37 +96,52 @@ func (m *MockRepository) FindAll(ctx context.Context, organizationID, ledgerID u } // FindAll indicates an expected call of FindAll. -func (mr *MockRepositoryMockRecorder) FindAll(ctx, organizationID, ledgerID, filter any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) FindAll(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAll", reflect.TypeOf((*MockRepository)(nil).FindAll), ctx, organizationID, ledgerID, filter) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAll", reflect.TypeOf((*MockRepository)(nil).FindAll), arg0, arg1, arg2, arg3) +} + +// FindByParentID mocks base method. +func (m *MockRepository) FindByParentID(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) (*Transaction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByParentID", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*Transaction) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByParentID indicates an expected call of FindByParentID. +func (mr *MockRepositoryMockRecorder) FindByParentID(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByParentID", reflect.TypeOf((*MockRepository)(nil).FindByParentID), arg0, arg1, arg2, arg3) } // ListByIDs mocks base method. -func (m *MockRepository) ListByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*Transaction, error) { +func (m *MockRepository) ListByIDs(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 []uuid.UUID) ([]*Transaction, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListByIDs", ctx, organizationID, ledgerID, ids) + ret := m.ctrl.Call(m, "ListByIDs", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]*Transaction) ret1, _ := ret[1].(error) return ret0, ret1 } // ListByIDs indicates an expected call of ListByIDs. -func (mr *MockRepositoryMockRecorder) ListByIDs(ctx, organizationID, ledgerID, ids any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) ListByIDs(arg0, arg1, arg2, arg3 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByIDs", reflect.TypeOf((*MockRepository)(nil).ListByIDs), ctx, organizationID, ledgerID, ids) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByIDs", reflect.TypeOf((*MockRepository)(nil).ListByIDs), arg0, arg1, arg2, arg3) } // Update mocks base method. -func (m *MockRepository) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, transaction *Transaction) (*Transaction, error) { +func (m *MockRepository) Update(arg0 context.Context, arg1, arg2, arg3 uuid.UUID, arg4 *Transaction) (*Transaction, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", ctx, organizationID, ledgerID, id, transaction) + ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*Transaction) ret1, _ := ret[1].(error) return ret0, ret1 } // Update indicates an expected call of Update. -func (mr *MockRepositoryMockRecorder) Update(ctx, organizationID, ledgerID, id, transaction any) *gomock.Call { +func (mr *MockRepositoryMockRecorder) Update(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), ctx, organizationID, ledgerID, id, transaction) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), arg0, arg1, arg2, arg3, arg4) } diff --git a/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go b/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go index 717c5c93..aafde444 100644 --- a/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go +++ b/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go @@ -29,6 +29,7 @@ type Repository interface { Create(ctx context.Context, transaction *Transaction) (*Transaction, error) FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID, filter http.Pagination) ([]*Transaction, http.CursorPagination, error) Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*Transaction, error) + FindByParentID(ctx context.Context, organizationID, ledgerID, parentID uuid.UUID) (*Transaction, error) ListByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*Transaction, error) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, transaction *Transaction) (*Transaction, error) Delete(ctx context.Context, organizationID, ledgerID, id uuid.UUID) error @@ -186,6 +187,8 @@ func (r *TransactionPostgreSQLRepository) FindAll(ctx context.Context, organizat for rows.Next() { var transaction TransactionPostgreSQLModel + var body string + if err := rows.Scan( &transaction.ID, &transaction.ParentTransactionID, @@ -199,7 +202,7 @@ func (r *TransactionPostgreSQLRepository) FindAll(ctx context.Context, organizat &transaction.ChartOfAccountsGroupName, &transaction.LedgerID, &transaction.OrganizationID, - &transaction.Body, + &body, &transaction.CreatedAt, &transaction.UpdatedAt, &transaction.DeletedAt, @@ -209,6 +212,13 @@ func (r *TransactionPostgreSQLRepository) FindAll(ctx context.Context, organizat return nil, http.CursorPagination{}, err } + err = json.Unmarshal([]byte(body), &transaction.Body) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to unmarshal address", err) + + return nil, http.CursorPagination{}, err + } + transactions = append(transactions, transaction.ToEntity()) } @@ -263,6 +273,8 @@ func (r *TransactionPostgreSQLRepository) ListByIDs(ctx context.Context, organiz for rows.Next() { var transaction TransactionPostgreSQLModel + var body string + if err := rows.Scan( &transaction.ID, &transaction.ParentTransactionID, @@ -276,7 +288,7 @@ func (r *TransactionPostgreSQLRepository) ListByIDs(ctx context.Context, organiz &transaction.ChartOfAccountsGroupName, &transaction.LedgerID, &transaction.OrganizationID, - &transaction.Body, + &body, &transaction.CreatedAt, &transaction.UpdatedAt, &transaction.DeletedAt, @@ -286,6 +298,13 @@ func (r *TransactionPostgreSQLRepository) ListByIDs(ctx context.Context, organiz return nil, err } + err = json.Unmarshal([]byte(body), &transaction.Body) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to unmarshal address", err) + + return nil, err + } + transactions = append(transactions, transaction.ToEntity()) } @@ -360,6 +379,68 @@ func (r *TransactionPostgreSQLRepository) Find(ctx context.Context, organization return transaction.ToEntity(), nil } +// FindByParentID retrieves a Transaction entity from the database using the provided parent ID. +func (r *TransactionPostgreSQLRepository) FindByParentID(ctx context.Context, organizationID, ledgerID, parentID uuid.UUID) (*Transaction, error) { + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "postgres.find_transaction") + defer span.End() + + db, err := r.connection.GetDB() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get database connection", err) + + return nil, err + } + + transaction := &TransactionPostgreSQLModel{} + + var body string + + ctx, spanQuery := tracer.Start(ctx, "postgres.find.query") + + row := db.QueryRowContext(ctx, "SELECT * FROM transaction WHERE organization_id = $1 AND ledger_id = $2 AND parent_transaction_id = $3 AND deleted_at IS NULL", + organizationID, ledgerID, parentID) + + spanQuery.End() + + if err := row.Scan( + &transaction.ID, + &transaction.ParentTransactionID, + &transaction.Description, + &transaction.Template, + &transaction.Status, + &transaction.StatusDescription, + &transaction.Amount, + &transaction.AmountScale, + &transaction.AssetCode, + &transaction.ChartOfAccountsGroupName, + &transaction.LedgerID, + &transaction.OrganizationID, + &body, + &transaction.CreatedAt, + &transaction.UpdatedAt, + &transaction.DeletedAt, + ); err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to scan row", err) + + if errors.Is(err, sql.ErrNoRows) { + return nil, pkg.ValidateBusinessError(constant.ErrEntityNotFound, reflect.TypeOf(Transaction{}).Name()) + } + + return nil, err + } + + err = json.Unmarshal([]byte(body), &transaction.Body) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to unmarshal address", err) + + return nil, err + } + + return transaction.ToEntity(), nil +} + // Update a Transaction entity into Postgresql and returns the Transaction updated. func (r *TransactionPostgreSQLRepository) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, transaction *Transaction) (*Transaction, error) { tracer := pkg.NewTracerFromContext(ctx) diff --git a/components/transaction/internal/services/query/get-parent-id-transaction.go b/components/transaction/internal/services/query/get-parent-id-transaction.go new file mode 100644 index 00000000..d653a236 --- /dev/null +++ b/components/transaction/internal/services/query/get-parent-id-transaction.go @@ -0,0 +1,49 @@ +package query + +import ( + "context" + "reflect" + + "github.com/LerianStudio/midaz/components/transaction/internal/adapters/postgres/transaction" + "github.com/LerianStudio/midaz/pkg" + "github.com/LerianStudio/midaz/pkg/mopentelemetry" + + "github.com/google/uuid" +) + +// GetParentByTransactionID gets data in the repository. +func (uc *UseCase) GetParentByTransactionID(ctx context.Context, organizationID, ledgerID, parentID uuid.UUID) (*transaction.Transaction, error) { + logger := pkg.NewLoggerFromContext(ctx) + tracer := pkg.NewTracerFromContext(ctx) + + ctx, span := tracer.Start(ctx, "query.get_parent_by_transaction_id") + defer span.End() + + logger.Infof("Trying to get transaction") + + tran, err := uc.TransactionRepo.FindByParentID(ctx, organizationID, ledgerID, parentID) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get parent transaction on repo by id", err) + + logger.Errorf("Error getting parent transaction: %v", err) + + return nil, err + } + + if tran != nil { + metadata, err := uc.MetadataRepo.FindByEntity(ctx, reflect.TypeOf(transaction.Transaction{}).Name(), tran.ID) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get metadata on mongodb account", err) + + logger.Errorf("Error get metadata on mongodb account: %v", err) + + return nil, err + } + + if metadata != nil { + tran.Metadata = metadata.Data + } + } + + return tran, nil +} diff --git a/components/transaction/internal/services/query/get-parent-id-transaction_test.go b/components/transaction/internal/services/query/get-parent-id-transaction_test.go new file mode 100644 index 00000000..eb07c1bd --- /dev/null +++ b/components/transaction/internal/services/query/get-parent-id-transaction_test.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "go.uber.org/mock/gomock" + "testing" + + "github.com/LerianStudio/midaz/components/transaction/internal/adapters/postgres/transaction" + "github.com/LerianStudio/midaz/pkg" + + "github.com/stretchr/testify/assert" +) + +func TestGetParentByTransactionID(t *testing.T) { + ID := pkg.GenerateUUIDv7() + organizationID := pkg.GenerateUUIDv7() + ledgerID := pkg.GenerateUUIDv7() + parentID := ID.String() + + tran := &transaction.Transaction{ + ParentTransactionID: &parentID, + OrganizationID: organizationID.String(), + LedgerID: ledgerID.String(), + } + + uc := UseCase{ + TransactionRepo: transaction.NewMockRepository(gomock.NewController(t)), + } + + uc.TransactionRepo.(*transaction.MockRepository). + EXPECT(). + FindByParentID(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(tran, nil). + Times(1) + res, err := uc.TransactionRepo.FindByParentID(context.TODO(), organizationID, ledgerID, ID) + + assert.Equal(t, tran, res) + assert.Nil(t, err) +} + +func TestGetParentByTransactionIDError(t *testing.T) { + errMSG := "err to create account on database" + ID := pkg.GenerateUUIDv7() + organizationID := pkg.GenerateUUIDv7() + ledgerID := pkg.GenerateUUIDv7() + + uc := UseCase{ + TransactionRepo: transaction.NewMockRepository(gomock.NewController(t)), + } + + uc.TransactionRepo.(*transaction.MockRepository). + EXPECT(). + FindByParentID(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.TransactionRepo.FindByParentID(context.TODO(), organizationID, ledgerID, ID) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} From acc57a3fc91ebbe4d4a7492bbd4bd49d23945e78 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Wed, 15 Jan 2025 20:21:50 +0100 Subject: [PATCH 44/49] feat: add magic numbers to constant :sparkles: --- .../internal/services/command/create-lock-race-condition.go | 2 +- pkg/constant/account.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/components/transaction/internal/services/command/create-lock-race-condition.go b/components/transaction/internal/services/command/create-lock-race-condition.go index 1d40689a..66789fc1 100644 --- a/components/transaction/internal/services/command/create-lock-race-condition.go +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -62,7 +62,7 @@ func (uc *UseCase) checkAndReleaseLock(ctx context.Context, wg *sync.WaitGroup, return } - time.Sleep(200 * time.Millisecond) + time.Sleep(constant.CheckAndReleaseLock * time.Millisecond) } } diff --git a/pkg/constant/account.go b/pkg/constant/account.go index 81ea9144..187b25ac 100644 --- a/pkg/constant/account.go +++ b/pkg/constant/account.go @@ -7,4 +7,5 @@ const ( TimeSetLockBalance = 5 LockRetry = 100 RedisTimesRetry = 3 + CheckAndReleaseLock = 200 ) From 24b5e6bf30be7603b884b3872725d5498f95a3a9 Mon Sep 17 00:00:00 2001 From: lerian-studio Date: Wed, 15 Jan 2025 19:30:46 +0000 Subject: [PATCH 45/49] chore(release): 1.45.0-beta.2 ## [1.45.0-beta.2](https://github.com/LerianStudio/midaz/compare/v1.45.0-beta.1...v1.45.0-beta.2) (2025-01-15) ### Features * add go routines to update; some postgres configs :sparkles: ([25fdb70](https://github.com/LerianStudio/midaz/commit/25fdb706ef65ec550172bb7f6d47652eb8f944f5)) * add logger :sparkles: ([6ff156d](https://github.com/LerianStudio/midaz/commit/6ff156d268d5647f19c9bcae394a5e788fddac4b)) * add magic numbers to constant :sparkles: ([acc57a3](https://github.com/LerianStudio/midaz/commit/acc57a3fc91ebbe4d4a7492bbd4bd49d23945e78)) * add optimistic lock on database using version to control race condition; ([3f37ade](https://github.com/LerianStudio/midaz/commit/3f37adeb3592531aa64612915066998defa00c06)) * adjust time :sparkles: ([60da1cf](https://github.com/LerianStudio/midaz/commit/60da1cf7dce25469c87bf5786aa6b603fbe79638)) * create race condition using gorotine and chanel ([3248ee7](https://github.com/LerianStudio/midaz/commit/3248ee7e8fca50dc8882327e94f4c9fcbfd3529e)) * new race condition implementation ([6bb89dd](https://github.com/LerianStudio/midaz/commit/6bb89dd46e163b057cb4c7c32cdd8e3a8c418147)) * new updates to avoid race condition ([97448dc](https://github.com/LerianStudio/midaz/commit/97448dc69d6bcfafe66bbd94d81cff8b4733da3e)) * update time lock :sparkles: ([beb4921](https://github.com/LerianStudio/midaz/commit/beb49216d8e7c7ddcc76a841c9c454304abd0e62)) ### Bug Fixes * add defer rollback :bug: ([0cdabd1](https://github.com/LerianStudio/midaz/commit/0cdabd13e58d91d6f86170c70baf4d602690bd16)) * add rollback in case of error to unlock database; :bug: ([66d7416](https://github.com/LerianStudio/midaz/commit/66d74168b2da61b5fc74662ff7150403fb624b36)) * add unlock :bug: ([0c62a31](https://github.com/LerianStudio/midaz/commit/0c62a314e4ad86918b6955ba3792ce2017102c8e)) * adjust to remove lock of get accounts :bug: ([1ddf09f](https://github.com/LerianStudio/midaz/commit/1ddf09f939da41d4ebabfd339b00d7caf9dc29f6)) * change place :bug: ([3970a04](https://github.com/LerianStudio/midaz/commit/3970a04dd1ac5157081815726766387434ad0b66)) * improve idempotency using setnx; :bug: ([5a7988e](https://github.com/LerianStudio/midaz/commit/5a7988e161e0bb64a64149c1871ba2f0c9f2dbd5)) * lint; add version; :bug: ([a7df566](https://github.com/LerianStudio/midaz/commit/a7df566659892e15eddc0486087d93a74cf707d4)) * make lint :bug: ([ff3a8ad](https://github.com/LerianStudio/midaz/commit/ff3a8ad792b1b592fb3faa93bcd86dc4f45a1572)) * merge with develop :bug: ([a04a8ee](https://github.com/LerianStudio/midaz/commit/a04a8eebeda62ff6f1812606c778aa5bcbf15041)) * reduce lock time :bug: ([ddb6a60](https://github.com/LerianStudio/midaz/commit/ddb6a60ca16b3560d6b8c0802e12fb9a246894bf)) * unlock specify by get accounts :bug: ([26af469](https://github.com/LerianStudio/midaz/commit/26af4697aff95095a98681af98a3c0658a60c75b)) * update go mod dependabot :bug: ([f776fcc](https://github.com/LerianStudio/midaz/commit/f776fcc678cbbb652dfb01ff6ab7890b6ac85777)) * updates to improve race condition :bug: ([022c3c9](https://github.com/LerianStudio/midaz/commit/022c3c90f6827149bc8ba4e78b2acb314895bbc8)) --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c08ac2ee..2d6eb8fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +## [1.45.0-beta.2](https://github.com/LerianStudio/midaz/compare/v1.45.0-beta.1...v1.45.0-beta.2) (2025-01-15) + + +### Features + +* add go routines to update; some postgres configs :sparkles: ([25fdb70](https://github.com/LerianStudio/midaz/commit/25fdb706ef65ec550172bb7f6d47652eb8f944f5)) +* add logger :sparkles: ([6ff156d](https://github.com/LerianStudio/midaz/commit/6ff156d268d5647f19c9bcae394a5e788fddac4b)) +* add magic numbers to constant :sparkles: ([acc57a3](https://github.com/LerianStudio/midaz/commit/acc57a3fc91ebbe4d4a7492bbd4bd49d23945e78)) +* add optimistic lock on database using version to control race condition; ([3f37ade](https://github.com/LerianStudio/midaz/commit/3f37adeb3592531aa64612915066998defa00c06)) +* adjust time :sparkles: ([60da1cf](https://github.com/LerianStudio/midaz/commit/60da1cf7dce25469c87bf5786aa6b603fbe79638)) +* create race condition using gorotine and chanel ([3248ee7](https://github.com/LerianStudio/midaz/commit/3248ee7e8fca50dc8882327e94f4c9fcbfd3529e)) +* new race condition implementation ([6bb89dd](https://github.com/LerianStudio/midaz/commit/6bb89dd46e163b057cb4c7c32cdd8e3a8c418147)) +* new updates to avoid race condition ([97448dc](https://github.com/LerianStudio/midaz/commit/97448dc69d6bcfafe66bbd94d81cff8b4733da3e)) +* update time lock :sparkles: ([beb4921](https://github.com/LerianStudio/midaz/commit/beb49216d8e7c7ddcc76a841c9c454304abd0e62)) + + +### Bug Fixes + +* add defer rollback :bug: ([0cdabd1](https://github.com/LerianStudio/midaz/commit/0cdabd13e58d91d6f86170c70baf4d602690bd16)) +* add rollback in case of error to unlock database; :bug: ([66d7416](https://github.com/LerianStudio/midaz/commit/66d74168b2da61b5fc74662ff7150403fb624b36)) +* add unlock :bug: ([0c62a31](https://github.com/LerianStudio/midaz/commit/0c62a314e4ad86918b6955ba3792ce2017102c8e)) +* adjust to remove lock of get accounts :bug: ([1ddf09f](https://github.com/LerianStudio/midaz/commit/1ddf09f939da41d4ebabfd339b00d7caf9dc29f6)) +* change place :bug: ([3970a04](https://github.com/LerianStudio/midaz/commit/3970a04dd1ac5157081815726766387434ad0b66)) +* improve idempotency using setnx; :bug: ([5a7988e](https://github.com/LerianStudio/midaz/commit/5a7988e161e0bb64a64149c1871ba2f0c9f2dbd5)) +* lint; add version; :bug: ([a7df566](https://github.com/LerianStudio/midaz/commit/a7df566659892e15eddc0486087d93a74cf707d4)) +* make lint :bug: ([ff3a8ad](https://github.com/LerianStudio/midaz/commit/ff3a8ad792b1b592fb3faa93bcd86dc4f45a1572)) +* merge with develop :bug: ([a04a8ee](https://github.com/LerianStudio/midaz/commit/a04a8eebeda62ff6f1812606c778aa5bcbf15041)) +* reduce lock time :bug: ([ddb6a60](https://github.com/LerianStudio/midaz/commit/ddb6a60ca16b3560d6b8c0802e12fb9a246894bf)) +* unlock specify by get accounts :bug: ([26af469](https://github.com/LerianStudio/midaz/commit/26af4697aff95095a98681af98a3c0658a60c75b)) +* update go mod dependabot :bug: ([f776fcc](https://github.com/LerianStudio/midaz/commit/f776fcc678cbbb652dfb01ff6ab7890b6ac85777)) +* updates to improve race condition :bug: ([022c3c9](https://github.com/LerianStudio/midaz/commit/022c3c90f6827149bc8ba4e78b2acb314895bbc8)) + ## [1.45.0-beta.1](https://github.com/LerianStudio/midaz/compare/v1.44.1-beta.1...v1.45.0-beta.1) (2025-01-09) From 44b650cb66cf14773119b7b94c790fa61a7e6231 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Thu, 16 Jan 2025 18:03:25 +0100 Subject: [PATCH 46/49] feat: final adjusts to rever transaction :sparkles: --- .../internal/adapters/http/in/transaction.go | 43 ++++- .../transaction/transaction.postgresql.go | 2 +- .../services/command/create-transaction.go | 10 +- pkg/constant/errors.go | 174 +++++++++--------- pkg/errors.go | 12 ++ 5 files changed, 146 insertions(+), 95 deletions(-) diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 3795aa2e..11663946 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -196,9 +196,19 @@ func (handler *TransactionHandler) RevertTransaction(c *fiber.Ctx) error { parent, err := handler.Query.GetParentByTransactionID(ctx, organizationID, ledgerID, transactionID) if err != nil { - mopentelemetry.HandleSpanError(&span, "Failed to retrieve transaction on query", err) + mopentelemetry.HandleSpanError(&span, "Failed to retrieve Parent Transaction on query", err) - logger.Errorf("Failed to retrieve Transaction with ID: %s, Error: %s", transactionID.String(), err.Error()) + logger.Errorf("Failed to retrieve Parent Transaction with ID: %s, Error: %s", transactionID.String(), err.Error()) + + return http.WithError(c, err) + } + + if parent != nil { + err = pkg.ValidateBusinessError(constant.ErrTransactionIDHasAlreadyParentTransaction, "RevertTransaction") + + mopentelemetry.HandleSpanError(&span, "Transaction Has Already Parent Transaction", err) + + logger.Errorf("Transaction Has Already Parent Transaction with ID: %s, Error: %s", transactionID.String(), err) return http.WithError(c, err) } @@ -212,15 +222,35 @@ func (handler *TransactionHandler) RevertTransaction(c *fiber.Ctx) error { return http.WithError(c, err) } - logger.Infof("Successfully retrieved Transaction with ID: %s", transactionID.String()) + if tran.ParentTransactionID != nil { + err = pkg.ValidateBusinessError(constant.ErrTransactionIDIsAlreadyARevert, "RevertTransaction") + + mopentelemetry.HandleSpanError(&span, "Transaction Has Already Parent Transaction", err) + + logger.Errorf("Transaction Has Already Parent Transaction with ID: %s, Error: %s", transactionID.String(), err) + + return http.WithError(c, err) + } + + froms := make([]goldModel.FromTo, 0) + for _, to := range tran.Body.Send.Distribute.To { + to.IsFrom = true + froms = append(froms, to) + } newSource := goldModel.Source{ - From: tran.Body.Send.Distribute.To, + From: froms, Remaining: tran.Body.Send.Distribute.Remaining, } + tos := make([]goldModel.FromTo, 0) + for _, from := range tran.Body.Send.Source.From { + from.IsFrom = false + tos = append(tos, from) + } + newDistribute := goldModel.Distribute{ - To: tran.Body.Send.Source.From, + To: tos, Remaining: tran.Body.Send.Source.Remaining, } @@ -449,6 +479,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L organizationID := c.Locals("organization_id").(uuid.UUID) ledgerID := c.Locals("ledger_id").(uuid.UUID) + transactionID, _ := c.Locals("transaction_id").(uuid.UUID) _, spanIdempotency := tracer.Start(ctx, "handler.create_transaction_idempotency") @@ -513,7 +544,7 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L return http.WithError(c, err) } - tran, err := handler.Command.CreateTransaction(ctxCreateTransaction, organizationID, ledgerID, &parserDSL) + tran, err := handler.Command.CreateTransaction(ctxCreateTransaction, organizationID, ledgerID, transactionID, &parserDSL) if err != nil { mopentelemetry.HandleSpanError(&spanCreateTransaction, "Failed to create transaction", err) diff --git a/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go b/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go index aafde444..65b460aa 100644 --- a/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go +++ b/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go @@ -425,7 +425,7 @@ func (r *TransactionPostgreSQLRepository) FindByParentID(ctx context.Context, or mopentelemetry.HandleSpanError(&span, "Failed to scan row", err) if errors.Is(err, sql.ErrNoRows) { - return nil, pkg.ValidateBusinessError(constant.ErrEntityNotFound, reflect.TypeOf(Transaction{}).Name()) + return nil, nil } return nil, err diff --git a/components/transaction/internal/services/command/create-transaction.go b/components/transaction/internal/services/command/create-transaction.go index ad42ccf4..10f2ae5c 100644 --- a/components/transaction/internal/services/command/create-transaction.go +++ b/components/transaction/internal/services/command/create-transaction.go @@ -16,7 +16,7 @@ import ( ) // CreateTransaction creates a new transaction persisting data in the repository. -func (uc *UseCase) CreateTransaction(ctx context.Context, organizationID, ledgerID uuid.UUID, t *goldModel.Transaction) (*transaction.Transaction, error) { +func (uc *UseCase) CreateTransaction(ctx context.Context, organizationID, ledgerID, transactionID uuid.UUID, t *goldModel.Transaction) (*transaction.Transaction, error) { logger := pkg.NewLoggerFromContext(ctx) tracer := pkg.NewTracerFromContext(ctx) @@ -34,9 +34,15 @@ func (uc *UseCase) CreateTransaction(ctx context.Context, organizationID, ledger amount := float64(t.Send.Value) scale := float64(t.Send.Scale) + var parentTransactionID *string + if transactionID != uuid.Nil { + value := transactionID.String() + parentTransactionID = &value + } + save := &transaction.Transaction{ ID: pkg.GenerateUUIDv7().String(), - ParentTransactionID: nil, + ParentTransactionID: parentTransactionID, OrganizationID: organizationID.String(), LedgerID: ledgerID.String(), Description: t.Description, diff --git a/pkg/constant/errors.go b/pkg/constant/errors.go index 73a06898..9aa50034 100644 --- a/pkg/constant/errors.go +++ b/pkg/constant/errors.go @@ -8,90 +8,92 @@ import ( * For more details, refer to the API documentation: https://docs.midaz.io/midaz/api-reference/resources/errors-list */ var ( - ErrDuplicateLedger = errors.New("0001") - ErrLedgerNameConflict = errors.New("0002") - ErrAssetNameOrCodeDuplicate = errors.New("0003") - ErrCodeUppercaseRequirement = errors.New("0004") - ErrCurrencyCodeStandardCompliance = errors.New("0005") - ErrUnmodifiableField = errors.New("0006") - ErrEntityNotFound = errors.New("0007") - ErrActionNotPermitted = errors.New("0008") - ErrMissingFieldsInRequest = errors.New("0009") - ErrAccountTypeImmutable = errors.New("0010") - ErrInactiveAccountType = errors.New("0011") - ErrAccountBalanceDeletion = errors.New("0012") - ErrResourceAlreadyDeleted = errors.New("0013") - ErrProductIDInactive = errors.New("0014") - ErrDuplicateProductName = errors.New("0015") - ErrBalanceRemainingDeletion = errors.New("0016") - ErrInvalidScriptFormat = errors.New("0017") - ErrInsufficientFunds = errors.New("0018") - ErrAccountIneligibility = errors.New("0019") - ErrAliasUnavailability = errors.New("0020") - ErrParentTransactionIDNotFound = errors.New("0021") - ErrImmutableField = errors.New("0022") - ErrTransactionTimingRestriction = errors.New("0023") - ErrAccountStatusTransactionRestriction = errors.New("0024") - ErrInsufficientAccountBalance = errors.New("0025") - ErrTransactionMethodRestriction = errors.New("0026") - ErrDuplicateTransactionTemplateCode = errors.New("0027") - ErrDuplicateAssetPair = errors.New("0028") - ErrInvalidParentAccountID = errors.New("0029") - ErrMismatchedAssetCode = errors.New("0030") - ErrChartTypeNotFound = errors.New("0031") - ErrInvalidCountryCode = errors.New("0032") - ErrInvalidCodeFormat = errors.New("0033") - ErrAssetCodeNotFound = errors.New("0034") - ErrPortfolioIDNotFound = errors.New("0035") - ErrProductIDNotFound = errors.New("0036") - ErrLedgerIDNotFound = errors.New("0037") - ErrOrganizationIDNotFound = errors.New("0038") - ErrParentOrganizationIDNotFound = errors.New("0039") - ErrInvalidType = errors.New("0040") - ErrTokenMissing = errors.New("0041") - ErrInvalidToken = errors.New("0042") - ErrInsufficientPrivileges = errors.New("0043") - ErrPermissionEnforcement = errors.New("0044") - ErrJWKFetch = errors.New("0045") - ErrInternalServer = errors.New("0046") - ErrBadRequest = errors.New("0047") - ErrInvalidDSLFileFormat = errors.New("0048") - ErrEmptyDSLFile = errors.New("0049") - ErrMetadataKeyLengthExceeded = errors.New("0050") - ErrMetadataValueLengthExceeded = errors.New("0051") - ErrAccountIDNotFound = errors.New("0052") - ErrUnexpectedFieldsInTheRequest = errors.New("0053") - ErrIDsNotFoundForAccounts = errors.New("0054") - ErrAssetIDNotFound = errors.New("0055") - ErrNoAssetsFound = errors.New("0056") - ErrNoProductsFound = errors.New("0057") - ErrNoPortfoliosFound = errors.New("0058") - ErrNoOrganizationsFound = errors.New("0059") - ErrNoLedgersFound = errors.New("0060") - ErrBalanceUpdateFailed = errors.New("0061") - ErrNoAccountIDsProvided = errors.New("0062") - ErrFailedToRetrieveAccountsByAliases = errors.New("0063") - ErrNoAccountsFound = errors.New("0064") - ErrInvalidPathParameter = errors.New("0065") - ErrInvalidAccountType = errors.New("0066") - ErrInvalidMetadataNesting = errors.New("0067") - ErrOperationIDNotFound = errors.New("0068") - ErrNoOperationsFound = errors.New("0069") - ErrTransactionIDNotFound = errors.New("0070") - ErrNoTransactionsFound = errors.New("0071") - ErrInvalidTransactionType = errors.New("0072") - ErrTransactionValueMismatch = errors.New("0073") - ErrForbiddenExternalAccountManipulation = errors.New("0074") - ErrAuditRecordNotRetrieved = errors.New("0075") - ErrAuditTreeRecordNotFound = errors.New("0076") - ErrInvalidDateFormat = errors.New("0077") - ErrInvalidFinalDate = errors.New("0078") - ErrDateRangeExceedsLimit = errors.New("0079") - ErrPaginationLimitExceeded = errors.New("0080") - ErrInvalidSortOrder = errors.New("0081") - ErrInvalidQueryParameter = errors.New("0082") - ErrInvalidDateRange = errors.New("0083") - ErrIdempotencyKey = errors.New("0084") - ErrAccountAliasNotFound = errors.New("0085") - ErrLockVersionAccountBalance = errors.New("0086") + ErrDuplicateLedger = errors.New("0001") + ErrLedgerNameConflict = errors.New("0002") + ErrAssetNameOrCodeDuplicate = errors.New("0003") + ErrCodeUppercaseRequirement = errors.New("0004") + ErrCurrencyCodeStandardCompliance = errors.New("0005") + ErrUnmodifiableField = errors.New("0006") + ErrEntityNotFound = errors.New("0007") + ErrActionNotPermitted = errors.New("0008") + ErrMissingFieldsInRequest = errors.New("0009") + ErrAccountTypeImmutable = errors.New("0010") + ErrInactiveAccountType = errors.New("0011") + ErrAccountBalanceDeletion = errors.New("0012") + ErrResourceAlreadyDeleted = errors.New("0013") + ErrProductIDInactive = errors.New("0014") + ErrDuplicateProductName = errors.New("0015") + ErrBalanceRemainingDeletion = errors.New("0016") + ErrInvalidScriptFormat = errors.New("0017") + ErrInsufficientFunds = errors.New("0018") + ErrAccountIneligibility = errors.New("0019") + ErrAliasUnavailability = errors.New("0020") + ErrParentTransactionIDNotFound = errors.New("0021") + ErrImmutableField = errors.New("0022") + ErrTransactionTimingRestriction = errors.New("0023") + ErrAccountStatusTransactionRestriction = errors.New("0024") + ErrInsufficientAccountBalance = errors.New("0025") + ErrTransactionMethodRestriction = errors.New("0026") + ErrDuplicateTransactionTemplateCode = errors.New("0027") + ErrDuplicateAssetPair = errors.New("0028") + ErrInvalidParentAccountID = errors.New("0029") + ErrMismatchedAssetCode = errors.New("0030") + ErrChartTypeNotFound = errors.New("0031") + ErrInvalidCountryCode = errors.New("0032") + ErrInvalidCodeFormat = errors.New("0033") + ErrAssetCodeNotFound = errors.New("0034") + ErrPortfolioIDNotFound = errors.New("0035") + ErrProductIDNotFound = errors.New("0036") + ErrLedgerIDNotFound = errors.New("0037") + ErrOrganizationIDNotFound = errors.New("0038") + ErrParentOrganizationIDNotFound = errors.New("0039") + ErrInvalidType = errors.New("0040") + ErrTokenMissing = errors.New("0041") + ErrInvalidToken = errors.New("0042") + ErrInsufficientPrivileges = errors.New("0043") + ErrPermissionEnforcement = errors.New("0044") + ErrJWKFetch = errors.New("0045") + ErrInternalServer = errors.New("0046") + ErrBadRequest = errors.New("0047") + ErrInvalidDSLFileFormat = errors.New("0048") + ErrEmptyDSLFile = errors.New("0049") + ErrMetadataKeyLengthExceeded = errors.New("0050") + ErrMetadataValueLengthExceeded = errors.New("0051") + ErrAccountIDNotFound = errors.New("0052") + ErrUnexpectedFieldsInTheRequest = errors.New("0053") + ErrIDsNotFoundForAccounts = errors.New("0054") + ErrAssetIDNotFound = errors.New("0055") + ErrNoAssetsFound = errors.New("0056") + ErrNoProductsFound = errors.New("0057") + ErrNoPortfoliosFound = errors.New("0058") + ErrNoOrganizationsFound = errors.New("0059") + ErrNoLedgersFound = errors.New("0060") + ErrBalanceUpdateFailed = errors.New("0061") + ErrNoAccountIDsProvided = errors.New("0062") + ErrFailedToRetrieveAccountsByAliases = errors.New("0063") + ErrNoAccountsFound = errors.New("0064") + ErrInvalidPathParameter = errors.New("0065") + ErrInvalidAccountType = errors.New("0066") + ErrInvalidMetadataNesting = errors.New("0067") + ErrOperationIDNotFound = errors.New("0068") + ErrNoOperationsFound = errors.New("0069") + ErrTransactionIDNotFound = errors.New("0070") + ErrNoTransactionsFound = errors.New("0071") + ErrInvalidTransactionType = errors.New("0072") + ErrTransactionValueMismatch = errors.New("0073") + ErrForbiddenExternalAccountManipulation = errors.New("0074") + ErrAuditRecordNotRetrieved = errors.New("0075") + ErrAuditTreeRecordNotFound = errors.New("0076") + ErrInvalidDateFormat = errors.New("0077") + ErrInvalidFinalDate = errors.New("0078") + ErrDateRangeExceedsLimit = errors.New("0079") + ErrPaginationLimitExceeded = errors.New("0080") + ErrInvalidSortOrder = errors.New("0081") + ErrInvalidQueryParameter = errors.New("0082") + ErrInvalidDateRange = errors.New("0083") + ErrIdempotencyKey = errors.New("0084") + ErrAccountAliasNotFound = errors.New("0085") + ErrLockVersionAccountBalance = errors.New("0086") + ErrTransactionIDHasAlreadyParentTransaction = errors.New("0087") + ErrTransactionIDIsAlreadyARevert = errors.New("0088") ) diff --git a/pkg/errors.go b/pkg/errors.go index 479a7914..bda0f76b 100644 --- a/pkg/errors.go +++ b/pkg/errors.go @@ -795,6 +795,18 @@ func ValidateBusinessError(err error, entityType string, args ...any) error { Title: "Race conditioning detected", Message: "A race condition was detected while processing your request. Please try again", }, + constant.ErrTransactionIDHasAlreadyParentTransaction: ValidationError{ + EntityType: entityType, + Code: constant.ErrTransactionIDHasAlreadyParentTransaction.Error(), + Title: "Transaction Revert already exist", + Message: "Transaction revert already exists. Please try again.", + }, + constant.ErrTransactionIDIsAlreadyARevert: ValidationError{ + EntityType: entityType, + Code: constant.ErrTransactionIDIsAlreadyARevert.Error(), + Title: "Transaction is already a reversal", + Message: "Transaction is already a reversal. Please try again", + }, } if mappedError, found := errorMap[err]; found { From 095f2d6942e7d8ab6be0822e66509d6d65392a10 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Thu, 16 Jan 2025 18:12:41 +0100 Subject: [PATCH 47/49] feat: add api on postman; adjust lint; generate swagger and open api; :sparkles: --- components/transaction/api/docs.go | 59 +++++++++++++++ components/transaction/api/openapi.yaml | 43 +++++++++++ components/transaction/api/swagger.json | 59 +++++++++++++++ components/transaction/api/swagger.yaml | 40 ++++++++++ .../internal/adapters/http/in/transaction.go | 2 + .../transaction/transaction.postgresql.go | 2 + .../services/command/create-transaction.go | 1 + postman/MIDAZ.postman_collection.json | 73 +++++++++++++++++-- 8 files changed, 271 insertions(+), 8 deletions(-) diff --git a/components/transaction/api/docs.go b/components/transaction/api/docs.go index 938098a0..e9dc5864 100644 --- a/components/transaction/api/docs.go +++ b/components/transaction/api/docs.go @@ -1044,6 +1044,65 @@ const docTemplate = `{ } } } + }, + "/v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/{transaction_id}/revert": { + "post": { + "description": "Revert a Transaction with Transaction ID only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Transactions" + ], + "summary": "Revert a Transaction", + "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": "Transaction ID", + "name": "transaction_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Transaction" + } + } + } + } } }, "definitions": { diff --git a/components/transaction/api/openapi.yaml b/components/transaction/api/openapi.yaml index a17bbacf..c064ae46 100644 --- a/components/transaction/api/openapi.yaml +++ b/components/transaction/api/openapi.yaml @@ -731,6 +731,49 @@ paths: tags: - Operations x-codegen-request-body-name: operation + /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/{transaction_id}/revert: + post: + description: Revert a Transaction with Transaction ID only + parameters: + - description: Authorization Bearer Token + in: header + name: Authorization + required: true + schema: + type: string + - description: Request ID + in: header + name: Midaz-Id + schema: + type: string + - description: Organization ID + in: path + name: organization_id + required: true + schema: + type: string + - description: Ledger ID + in: path + name: ledger_id + required: true + schema: + type: string + - description: Transaction ID + in: path + name: transaction_id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Transaction' + description: OK + summary: Revert a Transaction + tags: + - Transactions components: schemas: Amount: diff --git a/components/transaction/api/swagger.json b/components/transaction/api/swagger.json index 9ada0103..c2cced01 100644 --- a/components/transaction/api/swagger.json +++ b/components/transaction/api/swagger.json @@ -1038,6 +1038,65 @@ } } } + }, + "/v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/{transaction_id}/revert": { + "post": { + "description": "Revert a Transaction with Transaction ID only", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Transactions" + ], + "summary": "Revert a Transaction", + "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": "Transaction ID", + "name": "transaction_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Transaction" + } + } + } + } } }, "definitions": { diff --git a/components/transaction/api/swagger.yaml b/components/transaction/api/swagger.yaml index f34b8d1c..284ce4c5 100644 --- a/components/transaction/api/swagger.yaml +++ b/components/transaction/api/swagger.yaml @@ -1032,6 +1032,46 @@ paths: summary: Update an Operation tags: - Operations + /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/{transaction_id}/revert: + post: + consumes: + - application/json + description: Revert a Transaction with Transaction ID only + 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: Transaction ID + in: path + name: transaction_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/Transaction' + summary: Revert a Transaction + tags: + - Transactions /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/dsl: post: consumes: diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 11663946..32f20086 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -233,6 +233,7 @@ func (handler *TransactionHandler) RevertTransaction(c *fiber.Ctx) error { } froms := make([]goldModel.FromTo, 0) + for _, to := range tran.Body.Send.Distribute.To { to.IsFrom = true froms = append(froms, to) @@ -244,6 +245,7 @@ func (handler *TransactionHandler) RevertTransaction(c *fiber.Ctx) error { } tos := make([]goldModel.FromTo, 0) + for _, from := range tran.Body.Send.Source.From { from.IsFrom = false tos = append(tos, from) diff --git a/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go b/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go index 65b460aa..e9db33f2 100644 --- a/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go +++ b/components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go @@ -187,6 +187,7 @@ func (r *TransactionPostgreSQLRepository) FindAll(ctx context.Context, organizat for rows.Next() { var transaction TransactionPostgreSQLModel + var body string if err := rows.Scan( @@ -273,6 +274,7 @@ func (r *TransactionPostgreSQLRepository) ListByIDs(ctx context.Context, organiz for rows.Next() { var transaction TransactionPostgreSQLModel + var body string if err := rows.Scan( diff --git a/components/transaction/internal/services/command/create-transaction.go b/components/transaction/internal/services/command/create-transaction.go index 10f2ae5c..31f7a768 100644 --- a/components/transaction/internal/services/command/create-transaction.go +++ b/components/transaction/internal/services/command/create-transaction.go @@ -35,6 +35,7 @@ func (uc *UseCase) CreateTransaction(ctx context.Context, organizationID, ledger scale := float64(t.Send.Scale) var parentTransactionID *string + if transactionID != uuid.Nil { value := transactionID.String() parentTransactionID = &value diff --git a/postman/MIDAZ.postman_collection.json b/postman/MIDAZ.postman_collection.json index e6233720..b9888fac 100644 --- a/postman/MIDAZ.postman_collection.json +++ b/postman/MIDAZ.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "04ac9f89-2f0e-4aa7-80db-92644332f965", + "_postman_id": "011947ce-a59a-4f67-a335-cf486cbe8d06", "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", @@ -2308,7 +2308,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"chartOfAccountsGroupName\": \"PAG_CONTAS_CODE_1\",\n \"description\": \"description for the transaction person1 to person2 value of 100 reais\",\n \"metadata\": {\n \"mensagem\": \"pagamento\",\n \"valor\": \"100\"\n },\n \"send\": {\n \"asset\": \"BRL\",\n \"value\": 1000,\n \"scale\": 1,\n \"source\": {\n \"from\": [\n {\n \"account\": \"@external/BRL\",\n \"amount\": {\n \"asset\": \"BRL\",\n \"value\": 1000,\n \"scale\": 1\n },\n \"description\": \"Loan payment person1\",\n \"metadata\": {\n \"1\": \"m\",\n \"Cpf\": \"43049498x\"\n }\n }\n ]\n },\n \"distribute\": {\n \"to\": [\n {\n \"account\": \"0193f2d4-629f-702d-aeec-7cb80bb4097c\",\n \"amount\": {\n \"asset\": \"BRL\",\n \"value\": 1000,\n \"scale\": 1\n },\n \"metadata\": {\n \"mensagem\": \"tks\"\n }\n }\n ]\n }\n }\n}", + "raw": "{\n \"chartOfAccountsGroupName\": \"PAG_CONTAS_CODE_1\",\n \"description\": \"description for the transaction person1 to person2 value of 100 reais\",\n \"metadata\": {\n \"mensagem\": \"pagamento\",\n \"valor\": \"200\"\n },\n \"send\": {\n \"asset\": \"BRL\",\n \"value\": 200,\n \"scale\": 2,\n \"source\": {\n \"from\": [\n {\n \"account\": \"@external/BRL\",\n \"amount\": {\n \"asset\": \"BRL\",\n \"value\": 200,\n \"scale\": 2\n },\n \"description\": \"Loan payment person1\",\n \"metadata\": {\n \"1\": \"m\",\n \"Cpf\": \"43049498x\"\n }\n }\n ]\n },\n \"distribute\": {\n \"to\": [\n {\n \"account\": \"@mcgregor_brl\",\n \"amount\": {\n \"asset\": \"BRL\",\n \"value\": 200,\n \"scale\": 2\n },\n \"metadata\": {\n \"mensagem\": \"tks\"\n }\n }\n ]\n }\n }\n}", "options": { "raw": { "language": "json" @@ -2483,7 +2483,7 @@ "response": [] }, { - "name": "Transactions Revert", + "name": "Transactions Commit", "event": [ { "listen": "test", @@ -2542,7 +2542,7 @@ ] }, "url": { - "raw": "{{url_transaction}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/transactions/{{transaction_id}}/revert", + "raw": "{{url_transaction}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/transactions/{{transaction_id}}/commit", "host": [ "{{url_transaction}}" ], @@ -2554,7 +2554,7 @@ "{{ledger_id}}", "transactions", "{{transaction_id}}", - "revert" + "commit" ] }, "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." @@ -2562,7 +2562,7 @@ "response": [] }, { - "name": "Transactions Commit", + "name": "Transactions Cancel", "event": [ { "listen": "test", @@ -2621,7 +2621,7 @@ ] }, "url": { - "raw": "{{url_transaction}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/transactions/{{transaction_id}}/commit", + "raw": "{{url_transaction}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/transactions/{{transaction_id}}/cancel", "host": [ "{{url_transaction}}" ], @@ -2633,7 +2633,7 @@ "{{ledger_id}}", "transactions", "{{transaction_id}}", - "commit" + "cancel" ] }, "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." @@ -2784,6 +2784,58 @@ } }, "response": [] + }, + { + "name": "Transactions Revert", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "if (jsonData.hasOwnProperty('id')) {", + " console.log(\"parent_transaction_id before: \" + pm.collectionVariables.get(\"parent_transaction_id\"));", + " pm.collectionVariables.set(\"parent_transaction_id\", jsonData.id);", + " console.log(\"parent_transaction_id after: \" + pm.collectionVariables.get(\"parent_transaction_id\"));", + "}" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{url_transaction}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/transactions/{{transaction_id}}/revert", + "host": [ + "{{url_transaction}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "transactions", + "{{transaction_id}}", + "revert" + ] + }, + "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": [] } ] }, @@ -3652,6 +3704,11 @@ "key": "asset_rate_id", "value": "", "type": "string" + }, + { + "key": "parent_transaction_id", + "value": "", + "type": "string" } ] } \ No newline at end of file From e0d36ad33bb4962d4a7b4fc6646f5750e842a8f8 Mon Sep 17 00:00:00 2001 From: MartinezAvellan Date: Fri, 17 Jan 2025 11:18:52 +0100 Subject: [PATCH 48/49] fix: add revert logic to object :bug: --- .../internal/adapters/http/in/transaction.go | 43 +---------------- .../postgres/transaction/transaction.go | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/components/transaction/internal/adapters/http/in/transaction.go b/components/transaction/internal/adapters/http/in/transaction.go index 32f20086..e515bc1c 100644 --- a/components/transaction/internal/adapters/http/in/transaction.go +++ b/components/transaction/internal/adapters/http/in/transaction.go @@ -232,48 +232,9 @@ func (handler *TransactionHandler) RevertTransaction(c *fiber.Ctx) error { return http.WithError(c, err) } - froms := make([]goldModel.FromTo, 0) + transactionReverted := tran.TransactionRevert() - for _, to := range tran.Body.Send.Distribute.To { - to.IsFrom = true - froms = append(froms, to) - } - - newSource := goldModel.Source{ - From: froms, - Remaining: tran.Body.Send.Distribute.Remaining, - } - - tos := make([]goldModel.FromTo, 0) - - for _, from := range tran.Body.Send.Source.From { - from.IsFrom = false - tos = append(tos, from) - } - - newDistribute := goldModel.Distribute{ - To: tos, - Remaining: tran.Body.Send.Source.Remaining, - } - - send := goldModel.Send{ - Asset: tran.Body.Send.Asset, - Value: tran.Body.Send.Value, - Scale: tran.Body.Send.Scale, - Source: newSource, - Distribute: newDistribute, - } - - parserDSL := goldModel.Transaction{ - ChartOfAccountsGroupName: tran.Body.ChartOfAccountsGroupName, - Description: tran.Body.Description, - Code: tran.Body.Code, - Pending: tran.Body.Pending, - Metadata: tran.Body.Metadata, - Send: send, - } - - response := handler.createTransaction(c, logger, parserDSL) + response := handler.createTransaction(c, logger, transactionReverted) return response } diff --git a/components/transaction/internal/adapters/postgres/transaction/transaction.go b/components/transaction/internal/adapters/postgres/transaction/transaction.go index 545dfab1..2cc467cd 100644 --- a/components/transaction/internal/adapters/postgres/transaction/transaction.go +++ b/components/transaction/internal/adapters/postgres/transaction/transaction.go @@ -184,3 +184,49 @@ func (cti *CreateTransactionInput) FromDSl() *goldModel.Transaction { return dsl } + +// TransactionRevert is a func that revert transaction +func (t Transaction) TransactionRevert() goldModel.Transaction { + froms := make([]goldModel.FromTo, 0) + + for _, to := range t.Body.Send.Distribute.To { + to.IsFrom = true + froms = append(froms, to) + } + + newSource := goldModel.Source{ + From: froms, + Remaining: t.Body.Send.Distribute.Remaining, + } + + tos := make([]goldModel.FromTo, 0) + + for _, from := range t.Body.Send.Source.From { + from.IsFrom = false + tos = append(tos, from) + } + + newDistribute := goldModel.Distribute{ + To: tos, + Remaining: t.Body.Send.Source.Remaining, + } + + send := goldModel.Send{ + Asset: t.Body.Send.Asset, + Value: t.Body.Send.Value, + Scale: t.Body.Send.Scale, + Source: newSource, + Distribute: newDistribute, + } + + transaction := goldModel.Transaction{ + ChartOfAccountsGroupName: t.Body.ChartOfAccountsGroupName, + Description: t.Body.Description, + Code: t.Body.Code, + Pending: t.Body.Pending, + Metadata: t.Body.Metadata, + Send: send, + } + + return transaction +} From a5e06fbb6a21596bdf699f520ca969f0ebec34c6 Mon Sep 17 00:00:00 2001 From: lerian-studio Date: Fri, 17 Jan 2025 14:59:27 +0000 Subject: [PATCH 49/49] chore(release): 1.45.0-beta.3 ## [1.45.0-beta.3](https://github.com/LerianStudio/midaz/compare/v1.45.0-beta.2...v1.45.0-beta.3) (2025-01-17) ### Features * add api on postman; adjust lint; generate swagger and open api; :sparkles: ([095f2d6](https://github.com/LerianStudio/midaz/commit/095f2d6942e7d8ab6be0822e66509d6d65392a10)) * add transaction body on database; :sparkles: ([d5b6197](https://github.com/LerianStudio/midaz/commit/d5b619788782563d2c336fccaa01f4584ac54e57)) * final adjusts to rever transaction :sparkles: ([44b650c](https://github.com/LerianStudio/midaz/commit/44b650cb66cf14773119b7b94c790fa61a7e6231)) * new implementatios; :sparkles: ([a8f5a6d](https://github.com/LerianStudio/midaz/commit/a8f5a6deb065858eb90a3a9b74c641ecc304e4f5)) ### Bug Fixes * add revert logic to object :bug: ([e0d36ad](https://github.com/LerianStudio/midaz/commit/e0d36ad33bb4962d4a7b4fc6646f5750e842a8f8)) --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d6eb8fe..29f4d97f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## [1.45.0-beta.3](https://github.com/LerianStudio/midaz/compare/v1.45.0-beta.2...v1.45.0-beta.3) (2025-01-17) + + +### Features + +* add api on postman; adjust lint; generate swagger and open api; :sparkles: ([095f2d6](https://github.com/LerianStudio/midaz/commit/095f2d6942e7d8ab6be0822e66509d6d65392a10)) +* add transaction body on database; :sparkles: ([d5b6197](https://github.com/LerianStudio/midaz/commit/d5b619788782563d2c336fccaa01f4584ac54e57)) +* final adjusts to rever transaction :sparkles: ([44b650c](https://github.com/LerianStudio/midaz/commit/44b650cb66cf14773119b7b94c790fa61a7e6231)) +* new implementatios; :sparkles: ([a8f5a6d](https://github.com/LerianStudio/midaz/commit/a8f5a6deb065858eb90a3a9b74c641ecc304e4f5)) + + +### Bug Fixes + +* add revert logic to object :bug: ([e0d36ad](https://github.com/LerianStudio/midaz/commit/e0d36ad33bb4962d4a7b4fc6646f5750e842a8f8)) + ## [1.45.0-beta.2](https://github.com/LerianStudio/midaz/compare/v1.45.0-beta.1...v1.45.0-beta.2) (2025-01-15)