diff --git a/.github/workflows/packages-manager.yml b/.github/workflows/packages-manager.yml index 012c57b6..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,20 +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 - 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 $verificationPath) -replace '{{VERSION}}', "${{ needs.get_branch.outputs.version }}" -replace '{{VERSION}}', "${{ needs.get_branch.outputs.version }}" | Set-Content $verificationPath shell: pwsh - name: Publish Chocolatey package @@ -118,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/CHANGELOG.md b/CHANGELOG.md index 03e209aa..29f4d97f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,60 @@ +## [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) + + +### 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) + + +### 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) 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/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 index 89dbbf15..571e8fd3 100644 --- a/chocolatey/tools/VERIFICATION.txt +++ b/chocolatey/tools/VERIFICATION.txt @@ -1,5 +1,4 @@ -# VERIFICATION.txt - +VERIFICATION The purpose of the verification is to help moderators and the Chocolatey community to verify that the contents of this package can be trusted. @@ -8,3 +7,10 @@ identical to those on the GitHub release page for the windows_amd64 target. The binaries included in this package were downloaded directly from the official GitHub release page: https://github.com/LerianStudio/midaz/releases + +Download the zipped application `windows_amd64.zip` from the github release tab + +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 87c1c327..d4875150 100644 --- a/chocolatey/tools/chocolateyinstall.ps1 +++ b/chocolatey/tools/chocolateyinstall.ps1 @@ -1,4 +1,4 @@ -$version = 'v1.44.0' +$version = 'v1.45.0' $ErrorActionPreference = 'Stop'; @@ -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." diff --git a/components/audit/.env.example b/components/audit/.env.example index e487209f..c74246e8 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.45.0 SERVER_PORT=3005 SERVER_ADDRESS=:${SERVER_PORT} diff --git a/components/ledger/.env.example b/components/ledger/.env.example index 3149fb88..bb0b58c2 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.45.0 SERVER_PORT=3000 SERVER_ADDRESS=:${SERVER_PORT} 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/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/grpc/in/account.go b/components/ledger/internal/adapters/grpc/in/account.go index 31aa45b5..90502791 100644 --- a/components/ledger/internal/adapters/grpc/in/account.go +++ b/components/ledger/internal/adapters/grpc/in/account.go @@ -120,8 +120,8 @@ 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) { +// 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) @@ -196,3 +196,33 @@ func (ap *AccountProto) UpdateAccounts(ctx context.Context, update *account.Acco 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() + + organizationID, err := uuid.Parse(update.OrganizationId) + if err != nil { + return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), organizationID) + } + + ledgerID, err := uuid.Parse(update.LedgerId) + if err != nil { + return nil, pkg.ValidateBusinessError(constant.ErrInvalidPathParameter, reflect.TypeOf(mmodel.Account{}).Name(), ledgerID) + } + + 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, err + } + + return nil, nil +} diff --git a/components/ledger/internal/adapters/http/in/account.go b/components/ledger/internal/adapters/http/in/account.go index c89a5a3a..9b1a2436 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) { @@ -211,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.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.mock.go b/components/ledger/internal/adapters/postgres/account/account.mock.go index 0895340b..b3a8c3e4 100644 --- a/components/ledger/internal/adapters/postgres/account/account.mock.go +++ b/components/ledger/internal/adapters/postgres/account/account.mock.go @@ -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,180 +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(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", 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(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), 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), 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, "UpdateAccountByID", reflect.TypeOf((*MockRepository)(nil).UpdateAccountByID), ctx, organizationID, ledgerID, id, acc) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccounts", reflect.TypeOf((*MockRepository)(nil).UpdateAccounts), arg0, arg1, arg2, arg3) } diff --git a/components/ledger/internal/adapters/postgres/account/account.postgresql.go b/components/ledger/internal/adapters/postgres/account/account.postgresql.go index ebf8bf24..3488a1b0 100644 --- a/components/ledger/internal/adapters/postgres/account/account.postgresql.go +++ b/components/ledger/internal/adapters/postgres/account/account.postgresql.go @@ -4,9 +4,11 @@ import ( "context" "database/sql" "errors" + "github.com/LerianStudio/midaz/pkg/mgrpc/account" "reflect" "strconv" "strings" + "sync" "time" "github.com/LerianStudio/midaz/pkg/mpointers" @@ -33,6 +35,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) @@ -41,6 +44,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. @@ -92,7 +96,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, @@ -113,6 +117,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, @@ -177,10 +182,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) @@ -225,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, @@ -271,7 +273,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 +282,28 @@ 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.Version, + &acc.CreatedAt, + &acc.UpdatedAt, + &acc.DeletedAt, ); err != nil { mopentelemetry.HandleSpanError(&span, "Failed to scan row", err) @@ -311,7 +314,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 +342,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 +351,28 @@ 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.Version, + &acc.CreatedAt, + &acc.UpdatedAt, + &acc.DeletedAt, ); err != nil { mopentelemetry.HandleSpanError(&span, "Failed to scan row", err) @@ -379,7 +383,76 @@ 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). +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" + + acc := &AccountPostgreSQLModel{} + + ctx, spanQuery := tracer.Start(ctx, "postgres.find_with_deleted.query") + + row := db.QueryRowContext(ctx, query, args...) + + spanQuery.End() + + if err := row.Scan( + &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.Version, + &acc.CreatedAt, + &acc.UpdatedAt, + &acc.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 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. @@ -480,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, @@ -551,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, @@ -765,6 +840,7 @@ func (r *AccountPostgreSQLRepository) ListAccountsByIDs(ctx context.Context, org &acc.AllowReceiving, &acc.Alias, &acc.Type, + &acc.Version, &acc.CreatedAt, &acc.UpdatedAt, &acc.DeletedAt, @@ -835,6 +911,7 @@ func (r *AccountPostgreSQLRepository) ListAccountsByAlias(ctx context.Context, o &acc.AllowReceiving, &acc.Alias, &acc.Type, + &acc.Version, &acc.CreatedAt, &acc.UpdatedAt, &acc.DeletedAt, @@ -939,3 +1016,102 @@ 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, span := tracer.Start(ctx, "postgres.update_accounts") + defer span.End() + + db, err := r.connection.GetDB() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to get database connection", err) + + return err + } + + tx, err := db.Begin() + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to init transaction", err) + + return err + } + + var wg sync.WaitGroup + + errChan := make(chan error, len(accounts)) + + for _, acc := range accounts { + wg.Add(1) + + go func(acc *account.Account) { + defer wg.Done() + + 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, "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, acc.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` + + result, err := tx.ExecContext(ctx, query, args...) + if err != nil { + errChan <- err + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil || rowsAffected == 0 { + if err == nil { + err = sql.ErrNoRows + } + errChan <- err + } + }(acc) + } + + wg.Wait() + close(errChan) + + for err := range errChan { + if err != nil { + rollbackErr := tx.Rollback() + if rollbackErr != nil { + return rollbackErr + } + + return err + } + } + + 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 commitErr + } + + 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..3f5f0b84 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" @@ -24,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) @@ -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 +} 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/components/ledger/internal/services/query/get-alias-accounts_test.go b/components/ledger/internal/services/query/get-alias-accounts_test.go index dbe04334..5440797b 100644 --- a/components/ledger/internal/services/query/get-alias-accounts_test.go +++ b/components/ledger/internal/services/query/get-alias-accounts_test.go @@ -3,12 +3,12 @@ package query import ( "context" "errors" + "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" 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/mdz/.env.example b/components/mdz/.env.example index 5da79e4e..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.0 +VERSION=v1.45.0 diff --git a/components/transaction/.env.example b/components/transaction/.env.example index ce0b4635..2640ac7e 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.45.0 APP_CONTEXT=/transaction/v1 SERVER_PORT=3002 SERVER_ADDRESS=:${SERVER_PORT} 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 b200ab1c..e515bc1c 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,53 @@ 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 Parent Transaction on query", err) + + 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) + } + + 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) + } + + 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) + } + + transactionReverted := tran.TransactionRevert() + + response := handler.createTransaction(c, logger, transactionReverted) + + return response } // UpdateTransaction method that patch transaction created before @@ -385,8 +442,9 @@ 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) - _, spanRedis := tracer.Start(ctx, "handler.create_transaction_idempotency") + _, spanIdempotency := tracer.Start(ctx, "handler.create_transaction_idempotency") ts, _ := pkg.StructToJSONString(parserDSL) hash := pkg.HashSHA256(ts) @@ -394,14 +452,14 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L err := handler.Command.CreateOrCheckIdempotencyKey(ctx, organizationID, ledgerID, key, hash, ttl) if err != nil { - mopentelemetry.HandleSpanError(&spanRedis, "Redis idempotency key", err) + mopentelemetry.HandleSpanError(&spanIdempotency, "Redis idempotency key", err) - logger.Error("Redis idempotency key:", err.Error()) + logger.Infof("Redis idempotency key: %v", err.Error()) return http.WithError(c, err) } - spanRedis.End() + spanIdempotency.End() _, spanValidateDSL := tracer.Start(ctx, "handler.create_transaction_validate_dsl") @@ -416,29 +474,29 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L spanValidateDSL.End() + _, spanRaceCondition := tracer.Start(ctx, "handler.create_transaction.race_condition") + + handler.Command.AllKeysUnlocked(ctx, organizationID, ledgerID, validate.Aliases, hash) + + spanRaceCondition.End() + ctxGetAccounts, spanGetAccounts := tracer.Start(ctx, "handler.create_transaction.get_accounts") token := http.GetTokenHeader(c) - accounts, err := handler.getAccounts(ctxGetAccounts, logger, token, organizationID, ledgerID, validate.Aliases) + accounts, err := handler.getAccountsAndValidate(ctxGetAccounts, logger, token, hash, organizationID, ledgerID, validate, parserDSL) if err != nil { mopentelemetry.HandleSpanError(&spanGetAccounts, "Failed to get accounts", err) - return http.WithError(c, err) - } + _, spanReleaseLock := tracer.Start(ctx, "handler.update_accounts.delete_locks_race_condition") + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) - 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) + spanReleaseLock.End() return http.WithError(c, err) } - spanValidateAccounts.End() + spanGetAccounts.End() ctxCreateTransaction, spanCreateTransaction := tracer.Start(ctx, "handler.create_transaction.create_transaction") @@ -446,15 +504,20 @@ 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) + tran, err := handler.Command.CreateTransaction(ctxCreateTransaction, organizationID, ledgerID, transactionID, &parserDSL) if err != nil { mopentelemetry.HandleSpanError(&spanCreateTransaction, "Failed to create transaction", err) 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) } @@ -476,36 +539,63 @@ func (handler *TransactionHandler) createTransaction(c *fiber.Ctx, logger mlog.L 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, spanProcessAccounts := tracer.Start(ctx, "handler.create_transaction.process_accounts") + ctxProcessAccounts, spanUpdateAccounts := tracer.Start(ctx, "handler.create_transaction.update_accounts") - err = mopentelemetry.SetSpanAttributesFromStruct(&spanProcessAccounts, "payload_handler_process_accounts", accounts) + err = mopentelemetry.SetSpanAttributesFromStruct(&spanUpdateAccounts, "payload_handler_update_accounts", accounts) if err != nil { - mopentelemetry.HandleSpanError(&spanProcessAccounts, "Failed to convert accounts from struct to JSON string", err) + mopentelemetry.HandleSpanError(&spanUpdateAccounts, "Failed to convert accounts from struct to JSON string", err) } - err = handler.processAccounts(ctxProcessAccounts, logger, *validate, token, organizationID, ledgerID, accounts) + err = handler.Command.UpdateAccounts(ctxProcessAccounts, logger, *validate, token, organizationID, ledgerID, accounts) if err != nil { - mopentelemetry.HandleSpanError(&spanProcessAccounts, "Failed to process accounts", err) + mopentelemetry.HandleSpanError(&spanUpdateAccounts, "Failed to update accounts", err) + + _, spanReleaseLock := tracer.Start(ctx, "handler.update_accounts.delete_locks_race_condition") + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) + + spanReleaseLock.End() + + 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) + + logger.Errorf("Failed to update Transaction with ID: %s, Error: %s", tran.ID, err.Error()) + + return http.WithError(c, er) + } + + spanUpdateTransactionStatus.End() return http.WithError(c, err) } - spanProcessAccounts.End() + spanUpdateAccounts.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) 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) } @@ -513,7 +603,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) @@ -531,11 +620,53 @@ 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) 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() + + 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 + } + + err = goldModel.ValidateAccounts(body, *validate, accounts) + if err != nil { + mopentelemetry.HandleSpanError(&span, "Failed to validate accounts", err) + + handler.Command.DeleteLocks(ctx, organizationID, ledgerID, validate.Aliases, hash) + + 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) + + logger.Error("Failed to get account by alias gRPC on Ledger", err.Error()) + + return nil, err + } + + if searchAgain { + return handler.getAccountsAndValidate(ctx, logger, token, hash, organizationID, ledgerID, validate, body) + } + + 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) @@ -582,105 +713,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/adapters/postgres/transaction/transaction.go b/components/transaction/internal/adapters/postgres/transaction/transaction.go index eab9e2fb..2cc467cd 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, } @@ -180,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 +} 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 ff98293d..e9db33f2 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" @@ -28,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 @@ -80,7 +82,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 +95,7 @@ func (r *TransactionPostgreSQLRepository) Create(ctx context.Context, transactio record.ChartOfAccountsGroupName, record.LedgerID, record.OrganizationID, + record.Body, record.CreatedAt, record.UpdatedAt, record.DeletedAt, @@ -184,6 +187,9 @@ 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, @@ -197,6 +203,7 @@ func (r *TransactionPostgreSQLRepository) FindAll(ctx context.Context, organizat &transaction.ChartOfAccountsGroupName, &transaction.LedgerID, &transaction.OrganizationID, + &body, &transaction.CreatedAt, &transaction.UpdatedAt, &transaction.DeletedAt, @@ -206,6 +213,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()) } @@ -260,6 +274,9 @@ 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, @@ -273,6 +290,7 @@ func (r *TransactionPostgreSQLRepository) ListByIDs(ctx context.Context, organiz &transaction.ChartOfAccountsGroupName, &transaction.LedgerID, &transaction.OrganizationID, + &body, &transaction.CreatedAt, &transaction.UpdatedAt, &transaction.DeletedAt, @@ -282,6 +300,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()) } @@ -310,6 +335,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 +357,7 @@ func (r *TransactionPostgreSQLRepository) Find(ctx context.Context, organization &transaction.ChartOfAccountsGroupName, &transaction.LedgerID, &transaction.OrganizationID, + &body, &transaction.CreatedAt, &transaction.UpdatedAt, &transaction.DeletedAt, @@ -343,6 +371,75 @@ 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 +} + +// 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, nil + } + + 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/adapters/redis/consumer.redis.go b/components/transaction/internal/adapters/redis/consumer.redis.go index 22bb5574..d6036951 100644 --- a/components/transaction/internal/adapters/redis/consumer.redis.go +++ b/components/transaction/internal/adapters/redis/consumer.redis.go @@ -14,8 +14,10 @@ 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 + Incr(ctx context.Context, key string) int64 } // RedisConsumerRepository is a Redis implementation of the Redis consumer. @@ -61,6 +63,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_nx") + 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 nx 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 +116,43 @@ 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 } + +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 3506c88b..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() @@ -82,3 +96,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..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") @@ -22,19 +22,14 @@ 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) + 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) 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..66789fc1 --- /dev/null +++ b/components/transaction/internal/services/command/create-lock-race-condition.go @@ -0,0 +1,148 @@ +package command + +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" + "github.com/redis/go-redis/v9" + "strconv" + "sync" + "time" +) + +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()) + + ctx, span := tracer.Start(ctx, "redis.all_keys_unlocked") + defer span.End() + + var wg sync.WaitGroup + + resultChan := make(chan bool, len(keys)) + + for _, key := range keys { + internalKey := pkg.LockInternalKey(organizationID, ledgerID, key) + + logger.Infof("Account try to lock on redis: %v", internalKey) + + wg.Add(1) + + go uc.checkAndReleaseLock(ctx, &wg, internalKey, hash, resultChan) + } + + wg.Wait() + close(resultChan) +} + +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()) + + _, span := tracer.Start(ctx, "redis.check_and_release_lock") + defer span.End() + + defer wg.Done() + + for { + success, err := uc.RedisRepo.SetNX(context.Background(), internalKey, hash, constant.TimeSetLock) + if err != nil { + resultChan <- false + return + } + + logger.Infof("Account locked on redis: %v", internalKey) + + if success { + resultChan <- true + return + } + + time.Sleep(constant.CheckAndReleaseLock * time.Millisecond) + } +} + +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()) + + ctx, span := tracer.Start(ctx, "redis.delete_locks") + defer span.End() + + for _, key := range keys { + internalKey := pkg.LockInternalKey(organizationID, ledgerID, key) + + logger.Infof("Account releasing lock on redis: %v", internalKey) + + 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) + } + } + } +} + +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 { + internalKey := pkg.LockVersionInternalKey(organizationID, ledgerID, key, strconv.FormatInt(acc.Version, 10)) + + logger.Infof("Account balance version releasing lock on redis: %v", internalKey) + + isSuccess, err := uc.RedisRepo.SetNX(ctx, internalKey, "0", constant.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 + } + + total := uc.RedisRepo.Incr(ctx, internalKey) + logger.Infof("%v attempt(s) to get Account balance", internalKey) + + if total > constant.RedisTimesRetry { + 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(constant.LockRetry * time.Millisecond) + + 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/create-transaction.go b/components/transaction/internal/services/command/create-transaction.go index 9df4f4a5..31f7a768 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,16 @@ 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, @@ -46,6 +53,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/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/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 } 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 +} 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) +} 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, 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= diff --git a/pkg/constant/account.go b/pkg/constant/account.go index 24585c4e..187b25ac 100644 --- a/pkg/constant/account.go +++ b/pkg/constant/account.go @@ -3,4 +3,9 @@ package constant const ( DefaultExternalAccountAliasPrefix = "@external/" ExternalAccountType = "external" + TimeSetLock = 1 + TimeSetLockBalance = 5 + LockRetry = 100 + RedisTimesRetry = 3 + CheckAndReleaseLock = 200 ) diff --git a/pkg/constant/errors.go b/pkg/constant/errors.go index 8dc2d27a..9aa50034 100644 --- a/pkg/constant/errors.go +++ b/pkg/constant/errors.go @@ -8,88 +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") + 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 dc418467..bda0f76b 100644 --- a/pkg/errors.go +++ b/pkg/errors.go @@ -783,6 +783,30 @@ 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.", + }, + 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", + }, + 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 { diff --git a/pkg/gold/transaction/model/validations.go b/pkg/gold/transaction/model/validations.go index ef118273..be98aa8c 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,26 @@ 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, "validateBalance", acc.Alias) + } + } + } } return nil @@ -33,15 +53,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) } } } @@ -53,15 +73,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) } } } @@ -78,7 +98,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) } @@ -137,6 +157,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/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 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/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/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), 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, } } diff --git a/pkg/utils.go b/pkg/utils.go index 00b342fd..195f14bc 100644 --- a/pkg/utils.go +++ b/pkg/utils.go @@ -278,3 +278,21 @@ 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 +} + +func LockVersionInternalKey(organizationID, ledgerID uuid.UUID, key, version string) string { + lockVersionInternalKey := LockInternalKey(organizationID, ledgerID, key) + ":" + version + + return lockVersionInternalKey +} 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