From cd4cf4874503756b6b051723f512fde41323e609 Mon Sep 17 00:00:00 2001 From: Gabriel Ferreira Date: Fri, 17 May 2024 16:53:31 -0300 Subject: [PATCH 1/4] feat: Open tech for all --- .githooks/commit-msg/commit-msg | 52 + .githooks/pre-commit/pre-commit | 51 + .githooks/pre-push/pre-push | 17 + .githooks/pre-receive/pre-receive | 21 + .github/CODEOWNERS | 7 + .github/FUNDING.yml | 1 + .github/ISSUE_TEMPLATE/BUG-REPORT.yaml | 80 ++ .github/ISSUE_TEMPLATE/FEATURE-REQUEST.yaml | 63 + .github/ISSUE_TEMPLATE/config.yaml | 1 + .github/dependabot.yml | 7 + .github/pull_request_template.md | 26 + .github/workflows/build.yml | 66 + .github/workflows/codeql.yml | 49 + .github/workflows/golangci-lint.yml | 34 + .github/workflows/gosec.yml | 34 + .github/workflows/release-notification.yml | 21 + .github/workflows/release.yml | 99 ++ .github/workflows/unit-tests.yml | 36 + .gitignore | 28 +- .golangci.yml | 155 +++ .goreleaser.yml | 46 + .releaserc.yml | 66 + CHANGELOG.md | 642 +++++++++ CODE_OF_CONDUCT.md | 128 ++ CONTRIBUTING.md | 124 ++ GOVERNANCE.md | 46 + LICENSE | 2 +- Makefile | 69 + README.md | 30 +- SECURITY.md | 46 + STRUCTURE.md | 118 ++ SUPPORT.md | 16 + common/app.go | 97 ++ common/console/console.go | 31 + common/errors.go | 158 +++ common/mlog/log.go | 222 +++ common/mlog/nil.go | 56 + common/mmongo/mongo.go | 60 + common/mpointers/pointers.go | 28 + common/mpostgres/api.go | 146 ++ common/mpostgres/builder.go | 31 + common/mpostgres/postgres.go | 120 ++ common/mpostgres/query.options.go | 49 + common/mpostgres/regex.go | 102 ++ common/mzap/injector.go | 59 + common/mzap/zap.go | 67 + common/net/http/doc.go | 25 + common/net/http/errors.go | 70 + common/net/http/handler.go | 51 + common/net/http/headers.go | 8 + common/net/http/httputils.go | 38 + common/net/http/proxy.go | 26 + common/net/http/response.go | 108 ++ common/net/http/withBasicAuth.go | 64 + common/net/http/withBody.go | 228 ++++ common/net/http/withBody_test.go | 44 + common/net/http/withCORS.go | 35 + common/net/http/withCorrelationID.go | 18 + common/net/http/withJWT.go | 232 ++++ common/net/http/withLogging.go | 192 +++ common/os.go | 140 ++ common/shell/ascii.sh | 32 + common/shell/colors.sh | 21 + common/shell/logo.txt | 7 + common/stringUtils.go | 61 + common/stringUtils_test.go | 91 ++ common/utils.go | 47 + components/auth/.env.example | 29 + components/auth/Makefile | 62 + components/auth/docker-compose.yml | 136 ++ components/ledger/.env.example | 23 + components/ledger/Dockerfile | 20 + components/ledger/Makefile | 73 + components/ledger/api/v1.yml | 834 ++++++++++++ components/ledger/diagram.png | Bin 0 -> 183422 bytes components/ledger/docker-compose.yml | 134 ++ .../database/mongodb/metadata.mongodb.go | 177 +++ .../database/postgres/account.postgresql.go | 353 +++++ .../postgres/instrument.postgresql.go | 257 ++++ .../database/postgres/ledger.postgresql.go | 276 ++++ .../postgres/organization.postgresql.go | 305 +++++ .../database/postgres/portfolio.postgresql.go | 289 ++++ .../database/postgres/product.postgresql.go | 294 ++++ .../ledger/internal/app/command/command.go | 35 + .../internal/app/command/create-account.go | 104 ++ .../app/command/create-account_test.go | 63 + .../internal/app/command/create-instrument.go | 67 + .../app/command/create-instrument_test.go | 60 + .../internal/app/command/create-ledger.go | 62 + .../app/command/create-ledger_test.go | 60 + .../app/command/create-matadata_test.go | 53 + .../app/command/create-organization.go | 67 + .../app/command/create-organization_test.go | 53 + .../internal/app/command/create-portfolio.go | 67 + .../app/command/create-portfolio_test.go | 63 + .../internal/app/command/create-product.go | 71 + .../app/command/create-product_test.go | 61 + .../internal/app/command/delete-account.go | 37 + .../app/command/delete-account_test.go | 55 + .../internal/app/command/delete-instrument.go | 37 + .../app/command/delete-instrument_test.go | 54 + .../internal/app/command/delete-ledger.go | 37 + .../app/command/delete-ledger_test.go | 52 + .../app/command/delete-matadata_test.go | 52 + .../app/command/delete-organization.go | 37 + .../app/command/delete-organization_test.go | 50 + .../internal/app/command/delete-portfolio.go | 37 + .../app/command/delete-portfolio_test.go | 54 + .../internal/app/command/delete-product.go | 37 + .../app/command/delete-product_test.go | 54 + .../internal/app/command/update-account.go | 66 + .../app/command/update-account_test.go | 67 + .../internal/app/command/update-instrument.go | 72 + .../app/command/update-instrument_test.go | 69 + .../internal/app/command/update-ledger.go | 70 + .../app/command/update-ledger_test.go | 68 + .../app/command/update-metadata_test.go | 54 + .../app/command/update-organization.go | 71 + .../app/command/update-organization_test.go | 55 + .../internal/app/command/update-portfolio.go | 69 + .../app/command/update-portfolio_test.go | 71 + .../internal/app/command/update-product.go | 67 + .../app/command/update-product_test.go | 69 + components/ledger/internal/app/errors.go | 6 + .../internal/app/query/get-all-accounts.go | 62 + .../app/query/get-all-accounts_test.go | 55 + .../internal/app/query/get-all-instruments.go | 62 + .../app/query/get-all-instruments_test.go | 54 + .../internal/app/query/get-all-ledgers.go | 62 + .../app/query/get-all-ledgers_test.go | 52 + .../app/query/get-all-metadata-accounts.go | 62 + .../query/get-all-metadata-accounts_test.go | 56 + .../app/query/get-all-metadata-instruments.go | 62 + .../get-all-metadata-instruments_test.go | 56 + .../app/query/get-all-metadata-ledgers.go | 62 + .../query/get-all-metadata-ledgers_test.go | 56 + .../query/get-all-metadata-organizations.go | 62 + .../get-all-metadata-organizations_test.go | 56 + .../app/query/get-all-metadata-portfolios.go | 62 + .../query/get-all-metadata-portfolios_test.go | 56 + .../app/query/get-all-metadata-products.go | 62 + .../query/get-all-metadata-products_test.go | 56 + .../app/query/get-all-organizations.go | 61 + .../app/query/get-all-organizations_test.go | 50 + .../internal/app/query/get-all-portfolios.go | 62 + .../app/query/get-all-portfolios_test.go | 54 + .../internal/app/query/get-all-products.go | 62 + .../app/query/get-all-products_test.go | 54 + .../internal/app/query/get-id-account.go | 50 + .../internal/app/query/get-id-account_test.go | 67 + .../internal/app/query/get-id-instrument.go | 50 + .../app/query/get-id-instrument_test.go | 62 + .../internal/app/query/get-id-ledger.go | 50 + .../internal/app/query/get-id-ledger_test.go | 56 + .../app/query/get-id-metadata_test.go | 58 + .../internal/app/query/get-id-organization.go | 50 + .../app/query/get-id-organization_test.go | 54 + .../internal/app/query/get-id-portfolio.go | 50 + .../app/query/get-id-portfolio_test.go | 62 + .../internal/app/query/get-id-product.go | 50 + .../internal/app/query/get-id-product_test.go | 62 + components/ledger/internal/app/query/query.go | 35 + .../internal/domain/metadata/metadata.go | 72 + .../domain/metadata/metadata_repository.go | 16 + .../domain/onboarding/ledger/ledger.go | 101 ++ .../onboarding/ledger/ledger_repository.go | 19 + .../onboarding/organization/organization.go | 135 ++ .../organization/organization_repository.go | 19 + .../domain/portfolio/account/account.go | 178 +++ .../portfolio/account/account_repository.go | 19 + .../domain/portfolio/instrument/instrument.go | 114 ++ .../instrument/instrument_repository.go | 19 + .../domain/portfolio/portfolio/portfolio.go | 110 ++ .../portfolio/portfolio_repository.go | 20 + .../domain/portfolio/product/product.go | 105 ++ .../portfolio/product/product_repository.go | 20 + components/ledger/internal/gen/inject.go | 104 ++ .../internal/gen/mock/account/account_mock.go | 131 ++ .../gen/mock/instrument/instrument_mock.go | 131 ++ .../internal/gen/mock/ledger/ledger_mock.go | 131 ++ .../gen/mock/metadata/metadata_mock.go | 113 ++ .../mock/organization/organization_mock.go | 131 ++ .../gen/mock/portfolio/portfolio_mock.go | 146 ++ .../internal/gen/mock/product/product_mock.go | 146 ++ components/ledger/internal/gen/wire_gen.go | 136 ++ components/ledger/internal/main.go | 11 + components/ledger/internal/ports/account.go | 150 +++ .../ledger/internal/ports/http/routes.go | 79 ++ .../ledger/internal/ports/instrument.go | 156 +++ components/ledger/internal/ports/ledger.go | 155 +++ .../ledger/internal/ports/organization.go | 146 ++ components/ledger/internal/ports/portfolio.go | 153 +++ components/ledger/internal/ports/product.go | 148 ++ components/ledger/internal/service/config.go | 37 + components/ledger/internal/service/server.go | 39 + components/ledger/internal/service/service.go | 21 + .../000001_create_organization_table.down.sql | 1 + .../000001_create_organization_table.up.sql | 15 + .../000002_create_ledger_table.down.sql | 1 + .../000002_create_ledger_table.up.sql | 12 + .../000003_create_instrument_table.down.sql | 1 + .../000003_create_instrument_table.up.sql | 17 + .../000004_create_product_table.down.sql | 1 + .../000004_create_product_table.up.sql | 14 + .../000005_create_portfolio_table.down.sql | 1 + .../000005_create_portfolio_table.up.sql | 15 + .../000006_create_account_table.down.sql | 1 + .../000006_create_account_table.up.sql | 30 + components/ledger/setup/00_init.sql | 2 + components/mdz/cmd/login/login.go | 162 +++ components/mdz/cmd/root.go | 54 + components/mdz/cmd/ui/ui.go | 95 ++ components/mdz/cmd/version/version.go | 68 + components/mdz/main.go | 9 + components/mdz/pkg/claims.go | 19 + components/mdz/pkg/command.go | 392 ++++++ components/mdz/pkg/config.go | 176 +++ components/mdz/pkg/controller.go | 30 + components/mdz/pkg/flags.go | 106 ++ components/mdz/pkg/http.go | 163 +++ components/mdz/pkg/manager.go | 73 + components/mdz/pkg/profile.go | 261 ++++ components/mdz/pkg/relyingparty.go | 13 + components/mdz/pkg/utils.go | 54 + config/auth/hydra.yml | 32 + config/auth/kratos.yml | 84 ++ .../identity-schemas/default_user.schema.json | 46 + .../organization_user.schema.json | 91 ++ go.mod | 113 ++ go.sum | 384 ++++++ image/README/docker-ps.png | Bin 0 -> 177968 bytes image/README/midaz-banner.png | Bin 0 -> 13968 bytes make.sh | 163 +++ postman/MIDAZ DEV.postman_environment.json | 51 + postman/MIDAZ.postman_collection.json | 1200 +++++++++++++++++ revive.toml | 29 + 236 files changed, 19880 insertions(+), 24 deletions(-) create mode 100755 .githooks/commit-msg/commit-msg create mode 100755 .githooks/pre-commit/pre-commit create mode 100755 .githooks/pre-push/pre-push create mode 100644 .githooks/pre-receive/pre-receive create mode 100644 .github/CODEOWNERS create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/BUG-REPORT.yaml create mode 100644 .github/ISSUE_TEMPLATE/FEATURE-REQUEST.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yaml create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .github/workflows/gosec.yml create mode 100644 .github/workflows/release-notification.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/unit-tests.yml create mode 100644 .golangci.yml create mode 100644 .goreleaser.yml create mode 100644 .releaserc.yml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 GOVERNANCE.md create mode 100644 Makefile create mode 100644 SECURITY.md create mode 100644 STRUCTURE.md create mode 100644 SUPPORT.md create mode 100644 common/app.go create mode 100644 common/console/console.go create mode 100644 common/errors.go create mode 100644 common/mlog/log.go create mode 100644 common/mlog/nil.go create mode 100644 common/mmongo/mongo.go create mode 100644 common/mpointers/pointers.go create mode 100644 common/mpostgres/api.go create mode 100644 common/mpostgres/builder.go create mode 100644 common/mpostgres/postgres.go create mode 100644 common/mpostgres/query.options.go create mode 100644 common/mpostgres/regex.go create mode 100644 common/mzap/injector.go create mode 100644 common/mzap/zap.go create mode 100644 common/net/http/doc.go create mode 100644 common/net/http/errors.go create mode 100644 common/net/http/handler.go create mode 100644 common/net/http/headers.go create mode 100644 common/net/http/httputils.go create mode 100644 common/net/http/proxy.go create mode 100644 common/net/http/response.go create mode 100644 common/net/http/withBasicAuth.go create mode 100644 common/net/http/withBody.go create mode 100644 common/net/http/withBody_test.go create mode 100644 common/net/http/withCORS.go create mode 100644 common/net/http/withCorrelationID.go create mode 100644 common/net/http/withJWT.go create mode 100644 common/net/http/withLogging.go create mode 100644 common/os.go create mode 100644 common/shell/ascii.sh create mode 100644 common/shell/colors.sh create mode 100644 common/shell/logo.txt create mode 100644 common/stringUtils.go create mode 100644 common/stringUtils_test.go create mode 100644 common/utils.go create mode 100644 components/auth/.env.example create mode 100644 components/auth/Makefile create mode 100644 components/auth/docker-compose.yml create mode 100644 components/ledger/.env.example create mode 100644 components/ledger/Dockerfile create mode 100644 components/ledger/Makefile create mode 100644 components/ledger/api/v1.yml create mode 100644 components/ledger/diagram.png create mode 100644 components/ledger/docker-compose.yml create mode 100644 components/ledger/internal/adapters/database/mongodb/metadata.mongodb.go create mode 100644 components/ledger/internal/adapters/database/postgres/account.postgresql.go create mode 100644 components/ledger/internal/adapters/database/postgres/instrument.postgresql.go create mode 100644 components/ledger/internal/adapters/database/postgres/ledger.postgresql.go create mode 100644 components/ledger/internal/adapters/database/postgres/organization.postgresql.go create mode 100644 components/ledger/internal/adapters/database/postgres/portfolio.postgresql.go create mode 100644 components/ledger/internal/adapters/database/postgres/product.postgresql.go create mode 100644 components/ledger/internal/app/command/command.go create mode 100644 components/ledger/internal/app/command/create-account.go create mode 100644 components/ledger/internal/app/command/create-account_test.go create mode 100644 components/ledger/internal/app/command/create-instrument.go create mode 100644 components/ledger/internal/app/command/create-instrument_test.go create mode 100644 components/ledger/internal/app/command/create-ledger.go create mode 100644 components/ledger/internal/app/command/create-ledger_test.go create mode 100644 components/ledger/internal/app/command/create-matadata_test.go create mode 100644 components/ledger/internal/app/command/create-organization.go create mode 100644 components/ledger/internal/app/command/create-organization_test.go create mode 100644 components/ledger/internal/app/command/create-portfolio.go create mode 100644 components/ledger/internal/app/command/create-portfolio_test.go create mode 100644 components/ledger/internal/app/command/create-product.go create mode 100644 components/ledger/internal/app/command/create-product_test.go create mode 100644 components/ledger/internal/app/command/delete-account.go create mode 100644 components/ledger/internal/app/command/delete-account_test.go create mode 100644 components/ledger/internal/app/command/delete-instrument.go create mode 100644 components/ledger/internal/app/command/delete-instrument_test.go create mode 100644 components/ledger/internal/app/command/delete-ledger.go create mode 100644 components/ledger/internal/app/command/delete-ledger_test.go create mode 100644 components/ledger/internal/app/command/delete-matadata_test.go create mode 100644 components/ledger/internal/app/command/delete-organization.go create mode 100644 components/ledger/internal/app/command/delete-organization_test.go create mode 100644 components/ledger/internal/app/command/delete-portfolio.go create mode 100644 components/ledger/internal/app/command/delete-portfolio_test.go create mode 100644 components/ledger/internal/app/command/delete-product.go create mode 100644 components/ledger/internal/app/command/delete-product_test.go create mode 100644 components/ledger/internal/app/command/update-account.go create mode 100644 components/ledger/internal/app/command/update-account_test.go create mode 100644 components/ledger/internal/app/command/update-instrument.go create mode 100644 components/ledger/internal/app/command/update-instrument_test.go create mode 100644 components/ledger/internal/app/command/update-ledger.go create mode 100644 components/ledger/internal/app/command/update-ledger_test.go create mode 100644 components/ledger/internal/app/command/update-metadata_test.go create mode 100644 components/ledger/internal/app/command/update-organization.go create mode 100644 components/ledger/internal/app/command/update-organization_test.go create mode 100644 components/ledger/internal/app/command/update-portfolio.go create mode 100644 components/ledger/internal/app/command/update-portfolio_test.go create mode 100644 components/ledger/internal/app/command/update-product.go create mode 100644 components/ledger/internal/app/command/update-product_test.go create mode 100644 components/ledger/internal/app/errors.go create mode 100644 components/ledger/internal/app/query/get-all-accounts.go create mode 100644 components/ledger/internal/app/query/get-all-accounts_test.go create mode 100644 components/ledger/internal/app/query/get-all-instruments.go create mode 100644 components/ledger/internal/app/query/get-all-instruments_test.go create mode 100644 components/ledger/internal/app/query/get-all-ledgers.go create mode 100644 components/ledger/internal/app/query/get-all-ledgers_test.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-accounts.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-accounts_test.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-instruments.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-instruments_test.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-ledgers.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-ledgers_test.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-organizations.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-organizations_test.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-portfolios.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-portfolios_test.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-products.go create mode 100644 components/ledger/internal/app/query/get-all-metadata-products_test.go create mode 100644 components/ledger/internal/app/query/get-all-organizations.go create mode 100644 components/ledger/internal/app/query/get-all-organizations_test.go create mode 100644 components/ledger/internal/app/query/get-all-portfolios.go create mode 100644 components/ledger/internal/app/query/get-all-portfolios_test.go create mode 100644 components/ledger/internal/app/query/get-all-products.go create mode 100644 components/ledger/internal/app/query/get-all-products_test.go create mode 100644 components/ledger/internal/app/query/get-id-account.go create mode 100644 components/ledger/internal/app/query/get-id-account_test.go create mode 100644 components/ledger/internal/app/query/get-id-instrument.go create mode 100644 components/ledger/internal/app/query/get-id-instrument_test.go create mode 100644 components/ledger/internal/app/query/get-id-ledger.go create mode 100644 components/ledger/internal/app/query/get-id-ledger_test.go create mode 100644 components/ledger/internal/app/query/get-id-metadata_test.go create mode 100644 components/ledger/internal/app/query/get-id-organization.go create mode 100644 components/ledger/internal/app/query/get-id-organization_test.go create mode 100644 components/ledger/internal/app/query/get-id-portfolio.go create mode 100644 components/ledger/internal/app/query/get-id-portfolio_test.go create mode 100644 components/ledger/internal/app/query/get-id-product.go create mode 100644 components/ledger/internal/app/query/get-id-product_test.go create mode 100644 components/ledger/internal/app/query/query.go create mode 100644 components/ledger/internal/domain/metadata/metadata.go create mode 100644 components/ledger/internal/domain/metadata/metadata_repository.go create mode 100644 components/ledger/internal/domain/onboarding/ledger/ledger.go create mode 100644 components/ledger/internal/domain/onboarding/ledger/ledger_repository.go create mode 100644 components/ledger/internal/domain/onboarding/organization/organization.go create mode 100644 components/ledger/internal/domain/onboarding/organization/organization_repository.go create mode 100644 components/ledger/internal/domain/portfolio/account/account.go create mode 100644 components/ledger/internal/domain/portfolio/account/account_repository.go create mode 100644 components/ledger/internal/domain/portfolio/instrument/instrument.go create mode 100644 components/ledger/internal/domain/portfolio/instrument/instrument_repository.go create mode 100644 components/ledger/internal/domain/portfolio/portfolio/portfolio.go create mode 100644 components/ledger/internal/domain/portfolio/portfolio/portfolio_repository.go create mode 100644 components/ledger/internal/domain/portfolio/product/product.go create mode 100644 components/ledger/internal/domain/portfolio/product/product_repository.go create mode 100644 components/ledger/internal/gen/inject.go create mode 100644 components/ledger/internal/gen/mock/account/account_mock.go create mode 100644 components/ledger/internal/gen/mock/instrument/instrument_mock.go create mode 100644 components/ledger/internal/gen/mock/ledger/ledger_mock.go create mode 100644 components/ledger/internal/gen/mock/metadata/metadata_mock.go create mode 100644 components/ledger/internal/gen/mock/organization/organization_mock.go create mode 100644 components/ledger/internal/gen/mock/portfolio/portfolio_mock.go create mode 100644 components/ledger/internal/gen/mock/product/product_mock.go create mode 100644 components/ledger/internal/gen/wire_gen.go create mode 100644 components/ledger/internal/main.go create mode 100644 components/ledger/internal/ports/account.go create mode 100644 components/ledger/internal/ports/http/routes.go create mode 100644 components/ledger/internal/ports/instrument.go create mode 100644 components/ledger/internal/ports/ledger.go create mode 100644 components/ledger/internal/ports/organization.go create mode 100644 components/ledger/internal/ports/portfolio.go create mode 100644 components/ledger/internal/ports/product.go create mode 100644 components/ledger/internal/service/config.go create mode 100644 components/ledger/internal/service/server.go create mode 100644 components/ledger/internal/service/service.go create mode 100644 components/ledger/migrations/000001_create_organization_table.down.sql create mode 100644 components/ledger/migrations/000001_create_organization_table.up.sql create mode 100644 components/ledger/migrations/000002_create_ledger_table.down.sql create mode 100644 components/ledger/migrations/000002_create_ledger_table.up.sql create mode 100644 components/ledger/migrations/000003_create_instrument_table.down.sql create mode 100644 components/ledger/migrations/000003_create_instrument_table.up.sql create mode 100644 components/ledger/migrations/000004_create_product_table.down.sql create mode 100644 components/ledger/migrations/000004_create_product_table.up.sql create mode 100644 components/ledger/migrations/000005_create_portfolio_table.down.sql create mode 100644 components/ledger/migrations/000005_create_portfolio_table.up.sql create mode 100644 components/ledger/migrations/000006_create_account_table.down.sql create mode 100644 components/ledger/migrations/000006_create_account_table.up.sql create mode 100644 components/ledger/setup/00_init.sql create mode 100644 components/mdz/cmd/login/login.go create mode 100644 components/mdz/cmd/root.go create mode 100644 components/mdz/cmd/ui/ui.go create mode 100644 components/mdz/cmd/version/version.go create mode 100644 components/mdz/main.go create mode 100644 components/mdz/pkg/claims.go create mode 100644 components/mdz/pkg/command.go create mode 100644 components/mdz/pkg/config.go create mode 100644 components/mdz/pkg/controller.go create mode 100644 components/mdz/pkg/flags.go create mode 100644 components/mdz/pkg/http.go create mode 100644 components/mdz/pkg/manager.go create mode 100644 components/mdz/pkg/profile.go create mode 100644 components/mdz/pkg/relyingparty.go create mode 100644 components/mdz/pkg/utils.go create mode 100644 config/auth/hydra.yml create mode 100644 config/auth/kratos.yml create mode 100644 config/identity-schemas/default_user.schema.json create mode 100644 config/identity-schemas/organization_user.schema.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 image/README/docker-ps.png create mode 100644 image/README/midaz-banner.png create mode 100755 make.sh create mode 100644 postman/MIDAZ DEV.postman_environment.json create mode 100644 postman/MIDAZ.postman_collection.json create mode 100644 revive.toml diff --git a/.githooks/commit-msg/commit-msg b/.githooks/commit-msg/commit-msg new file mode 100755 index 00000000..0e6d69b4 --- /dev/null +++ b/.githooks/commit-msg/commit-msg @@ -0,0 +1,52 @@ +#!/bin/sh +# +# Add a specific emoji to the end of the first line in every commit message +# based on the conventional commits keyword. + +if [ ! -f "$1" ] || grep -q "fixup!" "$1"; then + # Exit if we didn't get a target file for some reason + # or we have a fixup commit + exit 0 +fi + +KEYWORD=$(head -n 1 "$1" | awk '{print $1}' | sed -e 's/://') + +case $KEYWORD in + "feat"|"feat("*) + EMOJI=":sparkles:" + ;; + "fix"|"fix("*) + EMOJI=":bug:" + ;; + "docs"|"docs("*) + EMOJI=":books:" + ;; + "style"|"style("*) + EMOJI=":gem:" + ;; + "refactor"|"refactor("*) + EMOJI=":hammer:" + ;; + "perf"|"perf("*) + EMOJI=":rocket:" + ;; + "test"|"test("*) + EMOJI=":rotating_light:" + ;; + "build"|"build("*) + EMOJI=":package:" + ;; + "ci"|"ci("*) + EMOJI=":construction_worker:" + ;; + "chore"|"chore("*) + EMOJI=":wrench:" + ;; + *) + EMOJI="" + ;; +esac + +MESSAGE=$(sed -E "1s/(.*)/\\1 $EMOJI/" <"$1") + +echo "$MESSAGE" >"$1" diff --git a/.githooks/pre-commit/pre-commit b/.githooks/pre-commit/pre-commit new file mode 100755 index 00000000..e4e8040d --- /dev/null +++ b/.githooks/pre-commit/pre-commit @@ -0,0 +1,51 @@ +#!/bin/bash + +source "$PWD"/common/shell/colors.sh +source "$PWD"/common/shell/ascii.sh + +branch=$(git rev-parse --abbrev-ref HEAD) + +if [[ $branch == "main" || $branch == "develop" || $branch == release/* ]]; then + echo "${bold}You can't commit directly to protected branches" + exit 1 +fi + +commit_msg_type_regex='feature|fix|refactor|style|test|docs|build' +commit_msg_scope_regex='.{1,20}' +commit_msg_description_regex='.{1,100}' +commit_msg_regex="^(${commit_msg_type_regex})(\(${commit_msg_scope_regex}\))?: (${commit_msg_description_regex})\$" +merge_msg_regex="^Merge branch '.+'\$" + +zero_commit="0000000000000000000000000000000000000000" + +# Do not traverse over commits that are already in the repository +excludeExisting="--not --all" + +error="" +while read oldrev newrev refname; do + # branch or tag get deleted + if [ "$newrev" = "$zero_commit" ]; then + continue + fi + + # Check for new branch or tag + if [ "$oldrev" = "$zero_commit" ]; then + rev_span=$(git rev-list $newrev $excludeExisting) + else + rev_span=$(git rev-list $oldrev..$newrev $excludeExisting) + fi + + for commit in $rev_span; do + commit_msg_header=$(git show -s --format=%s $commit) + if ! [[ "$commit_msg_header" =~ (${commit_msg_regex})|(${merge_msg_regex}) ]]; then + echo "$commit" >&2 + echo "ERROR: Invalid commit message format" >&2 + echo "$commit_msg_header" >&2 + error="true" + fi + done +done + +if [ -n "$error" ]; then + exit 1 +fi \ No newline at end of file diff --git a/.githooks/pre-push/pre-push b/.githooks/pre-push/pre-push new file mode 100755 index 00000000..2d8a3708 --- /dev/null +++ b/.githooks/pre-push/pre-push @@ -0,0 +1,17 @@ +#!/bin/bash + +source "$PWD"/common/shell/colors.sh +source "$PWD"/common/shell/ascii.sh + +while read local_ref local_sha remote_ref remote_sha; do + if [[ "$local_ref" =~ ^refs/heads/ ]]; then + branch_name=$(echo "$local_ref" | sed 's|^refs/heads/||') + + if [[ ! "$branch_name" =~ ^(feature|fix|hotfix|docs|refactor|build|test)/.*$ ]]; then + echo "${bold}Branch names must start with 'feature/', 'fix/', 'refactor/', 'docs/', 'test/' or 'hotfix/' followed by either a task id or feature name." + exit 1 + fi + fi +done + +exit 0 diff --git a/.githooks/pre-receive/pre-receive b/.githooks/pre-receive/pre-receive new file mode 100644 index 00000000..6e1aa30d --- /dev/null +++ b/.githooks/pre-receive/pre-receive @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +zero_commit="0000000000000000000000000000000000000000" + +while read oldrev newrev refname; do + + if [[ $oldrev == $zero_commit ]]; then + continue + fi + + if [[ $refname == "refs/heads/main" && $newrev != $zero_commit ]]; then + branch_name=$(basename $refname) + + if [[ $branch_name == release/* ]]; then + continue + else + echo "Error: You can only merge branches that start with 'release/' into the main branch." + exit 1 + fi + fi +done \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..bebd2a6b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Default owners for the entire repository +* @LerianStudio/Dev @LerianStudio/Devops + +# Owners for specific directories and files +.github/* @LerianStudio/Devops +.githooks/* @LerianStudio/Devops +components/* @LerianStudio/Dev \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..99111103 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [LerianStudio] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml new file mode 100644 index 00000000..8ec72e3d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml @@ -0,0 +1,80 @@ +name: "๐Ÿ› Bug Report" +description: Create a new ticket for a bug. +title: "๐Ÿ› [BUG] - " +labels: [ + "bug" +] +body: + - type: textarea + id: description + attributes: + label: "Description" + description: Please enter an explicit description of your issue + placeholder: Short and explicit description of your incident... + validations: + required: true + - type: input + id: reprod-url + attributes: + label: "Reproduction URL" + description: Please enter your GitHub URL to provide a reproduction of the issue + placeholder: ex. https://github.com/USERNAME/REPO-NAME + validations: + required: true + - type: textarea + id: reprod + attributes: + label: "Reproduction steps" + description: Please enter an explicit description of your issue + value: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + render: bash + validations: + required: true + - type: textarea + id: screenshot + attributes: + label: "Screenshots" + description: If applicable, add screenshots to help explain your problem. + value: | + ![DESCRIPTION](LINK.png) + render: bash + validations: + required: false + - type: textarea + id: logs + attributes: + label: "Logs" + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: bash + validations: + required: false + - type: dropdown + id: browsers + attributes: + label: "Browsers" + description: What browsers are you seeing the problem on ? + multiple: true + options: + - Firefox + - Chrome + - Safari + - Microsoft Edge + - Opera + validations: + required: false + - type: dropdown + id: os + attributes: + label: "OS" + description: What is the impacted environment ? + multiple: true + options: + - Windows + - Linux + - Mac + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yaml b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yaml new file mode 100644 index 00000000..d4bdfbea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yaml @@ -0,0 +1,63 @@ +name: "๐Ÿ’ก Feature Request" +description: Create a new ticket for a new feature request +title: "๐Ÿ’ก [REQUEST] - <title>" +labels: [ + "feature" +] +body: + - type: input + id: start_date + attributes: + label: "Start Date" + description: Start of development + placeholder: "month/day/year" + validations: + required: false + - type: textarea + id: implementation_pr + attributes: + label: "Implementation PR" + description: Pull request used + placeholder: "#Pull Request ID" + validations: + required: false + - type: textarea + id: reference_issues + attributes: + label: "Reference Issues" + description: Common issues + placeholder: "#Issues IDs" + validations: + required: false + - type: textarea + id: summary + attributes: + label: "Summary" + description: Provide a brief explanation of the feature + placeholder: Describe in a few lines your feature request + validations: + required: true + - type: textarea + id: basic_example + attributes: + label: "Basic Example" + description: Indicate here some basic examples of your feature. + placeholder: A few specific words about your feature request. + validations: + required: true + - type: textarea + id: drawbacks + attributes: + label: "Drawbacks" + description: What are the drawbacks/impacts of your feature request ? + placeholder: Identify the drawbacks and impacts while being neutral on your feature request + validations: + required: true + - type: textarea + id: unresolved_question + attributes: + label: "Unresolved questions" + description: What questions still remain unresolved ? + placeholder: Identify any unresolved issues. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 00000000..ec4bb386 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..65d2304f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + target-branch: "develop" \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..3d1272c4 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,26 @@ +# Midaz Pull Request Checklist + +## Pull Request Type +[//]: # (Check the appropriate box for the type of pull request.) + +- [ ] Ledger +- [ ] Auth +- [ ] Mdz +- [ ] Transaction +- [ ] Pipeline +- [ ] Documentation + +## Checklist +Please check each item after it's completed. + +- [ ] I have tested these changes locally. +- [ ] I have updated the documentation accordingly. +- [ ] I have added necessary comments to the code, especially in complex areas. +- [ ] I have ensured that my changes adhere to the project's coding standards. +- [ ] I have checked for any potential security issues. +- [ ] I have ensured that all tests pass. +- [ ] I have updated the version appropriately (if applicable). +- [ ] I have confirmed this code is ready for review. + +## Additional Notes +[//]: # (Add any additional notes, context, or explanation that could be helpful for reviewers.) \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..8fb5755b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,66 @@ +name: "Build Pipeline" + +on: + push: + tags: + - '**' + +permissions: + id-token: write + contents: read + pull-requests: write + +jobs: + build_and_publish: + runs-on: ubuntu-latest + env: + APP_NAME: midaz-ledger + WORKING_DIR: components/ledger + DOCKERHUB_ORG: lerianstudio + name: Build And Publish Docker Image to Midaz + steps: + - uses: actions/checkout@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: "${{ secrets.DOCKER_USERNAME }}" + password: "${{ secrets.DOCKER_PASSWORD }}" + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.DOCKERHUB_ORG }}/${{ env.APP_NAME }} + tags: | + type=semver,pattern={{version}} + type=ref,event=branch,suffix=-${{ github.sha }} + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + file: ${{ env.WORKING_DIR }}/Dockerfile + load: true + tags: ${{ steps.meta.outputs.tags }} + + - name: Extract tag name + shell: bash + run: echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + id: extract_tag + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: '${{ env.DOCKERHUB_ORG }}/${{ env.APP_NAME }}:${{ steps.extract_tag.outputs.tag }}' + format: 'table' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + exit-code: '1' + + - name: Push Docker image + uses: docker/build-push-action@v5 + with: + file: ${{ env.WORKING_DIR }}/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..f2e889d8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,49 @@ +name: "CodeQL" + +on: + pull_request: + branches: + - develop + - main + types: + - opened + - edited + - synchronize + - reopened + +permissions: + id-token: write + contents: read + pull-requests: read + actions: read + security-events: write + +jobs: + CodeQL: + runs-on: ubuntu-latest + name: Run CodeQL to Midaz + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + include: + - language: go + build-mode: autobuild + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + cache: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 00000000..ad783051 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,34 @@ +name: "GoLangCI-Lint" + +on: + pull_request: + branches: + - develop + - main + types: + - opened + - edited + - synchronize + - reopened + +permissions: + id-token: write + contents: read + pull-requests: read + +jobs: + GoLangCI-Lint: + runs-on: ubuntu-latest + name: Run GoLangCI-Lint to Midaz + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + cache: false + + - name: GoLangCI-Lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest \ No newline at end of file diff --git a/.github/workflows/gosec.yml b/.github/workflows/gosec.yml new file mode 100644 index 00000000..618d402c --- /dev/null +++ b/.github/workflows/gosec.yml @@ -0,0 +1,34 @@ +name: "GoSec" + +on: + pull_request: + branches: + - develop + - main + types: + - opened + - edited + - synchronize + - reopened + +permissions: + id-token: write + contents: read + pull-requests: read + +jobs: + GoSec: + runs-on: ubuntu-latest + name: Run GoSec to Midaz + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + cache: false + + - name: Gosec Scanner + uses: securego/gosec@master + with: + args: ./... \ No newline at end of file diff --git a/.github/workflows/release-notification.yml b/.github/workflows/release-notification.yml new file mode 100644 index 00000000..f509ae17 --- /dev/null +++ b/.github/workflows/release-notification.yml @@ -0,0 +1,21 @@ +on: + release: + types: [published] + +jobs: + github-releases-to-discord: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Github Releases To Discord + uses: SethCohen/github-releases-to-discord@v1.13.1 + if: '!github.event.prerelease' + with: + webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }} + color: "2105893" + username: "Release Changelog" + content: "||@everyone||" + footer_title: "Changelog" + footer_timestamp: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..3e1c185c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,99 @@ +name: "Release Pipeline" + +on: + push: + branches: + - develop + - main + - hotfix/v* + paths-ignore: + - '.gitignore' + - '**/*.env' # Ignores all .env files + - '*.env' # Ignores .env files in the root directory + - '**/*.md' # Ignores all .md files + - '*.md' # Ignores .md files in the root directory + - '**/*.txt' # Ignores all .env files + - '*.txt' # Ignores .env files in the root directory + tags-ignore: ['**'] + +permissions: + id-token: write + contents: write + pull-requests: write + +jobs: + integration_tests: + name: Integration test + runs-on: ubuntu-latest + env: + WORKING_DIR: components/ledger + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Unit Tests + working-directory: ${{ env.WORKING_DIR }} + shell: bash + run: make test + + publish_release: + runs-on: ubuntu-latest + needs: integration_tests + environment: + name: create_release + env: + WORKING_DIR: components/ledger + name: Create release to Midaz + steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }} + private-key: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_PRIVATE_KEY }} + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + id: import_gpg + with: + gpg_private_key: ${{ secrets.LERIAN_CI_CD_USER_GPG_KEY }} + passphrase: ${{ secrets.LERIAN_CI_CD_USER_GPG_KEY_PASSWORD }} + git_committer_name: ${{ secrets.LERIAN_CI_CD_USER_NAME }} + git_committer_email: ${{ secrets.LERIAN_CI_CD_USER_EMAIL }} + git_config_global: true + git_user_signingkey: true + git_commit_gpgsign: true + + - name: Semantic Release + uses: cycjimmy/semantic-release-action@v4 + id: semantic + with: + ci: false + semantic_version: 23.0.8 + extra_plugins: | + conventional-changelog-conventionalcommits@v7.0.2 + @saithodev/semantic-release-backmerge + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + GIT_AUTHOR_NAME: ${{ secrets.LERIAN_CI_CD_USER_NAME }} + GIT_AUTHOR_EMAIL: ${{ secrets.LERIAN_CI_CD_USER_EMAIL }} + GIT_COMMITTER_NAME: ${{ secrets.LERIAN_CI_CD_USER_NAME }} + GIT_COMMITTER_EMAIL: ${{ secrets.LERIAN_CI_CD_USER_EMAIL }} + + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + cache: false + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} \ No newline at end of file diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 00000000..5a47393d --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,36 @@ +name: "Unit Tests" + +on: + pull_request: + branches: + - develop + - main + types: + - opened + - edited + - synchronize + - reopened + +permissions: + id-token: write + contents: read + pull-requests: read + +jobs: + unit-tests: + runs-on: ubuntu-latest + name: Run Unit Tests to Midaz + env: + WORKING_DIR: components/ledger + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + cache: false + + - name: Unit Tests + working-directory: ${{ env.WORKING_DIR }} + shell: bash + run: make test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3b735ec4..e5982599 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,7 @@ -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work +.vscode +*__debug_bin1675735721 +.DS_Store +.env +.idea +*.iml +components/ledger/_docker-compose.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..3295d0c0 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,155 @@ +linters: + enable: + - bodyclose + - gocognit + - goconst + - depguard + - dogsled + - dupword # Detects duplicate words. + - durationcheck + - errchkjson + - exportloopref # Detects pointers to enclosing loop variables. + - gocritic # Metalinter; detects bugs, performance, and styling issues. + - gocyclo + - gofumpt # Detects whether code was gofumpt-ed. + - goimports + - gosec # Detects security problems. + - gosimple + - loggercheck + - govet + # - golint + - ineffassign + - megacheck + - wsl + - misspell # Detects commonly misspelled English words in comments. + - nakedret + - nilerr # Detects code that returns nil even if it checks that the error is not nil. + - nolintlint # Detects ill-formed or insufficient nolint directives. + - perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative. + - prealloc # Detects slice declarations that could potentially be pre-allocated. + - predeclared # Detects code that shadows one of Go's predeclared identifiers + - reassign + - revive # Metalinter; drop-in replacement for golint. + - stylecheck # Replacement for golint + - tenv # Detects using os.Setenv instead of t.Setenv. + - thelper # Detects test helpers without t.Helper(). + - tparallel # Detects inappropriate usage of t.Parallel(). + - typecheck + - unconvert # Detects unnecessary type conversions. + - unparam + - unused + - usestdlibvars + - vet + - wastedassign + - errcheck + +run: + timeout: 5m + tests: false + +linters-settings: + depguard: + rules: + main: + deny: + - pkg: io/ioutil + desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil + gocyclo: + min-complexity: 16 + govet: + enable: + - shadow + settings: + shadow: + strict: true + revive: + rules: + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing + - name: import-shadowing + severity: warning + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block + - name: empty-block + severity: warning + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines + - name: empty-lines + severity: warning + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any + - name: use-any + severity: warning + disabled: false +issues: + # The default exclusion rules are a bit too permissive, so copying the relevant ones below + exclude-use-default: false + exclude: + - parameter .* always receives + exclude-rules: + # We prefer to use an "exclude-list" so that new "default" exclusions are not + # automatically inherited. We can decide whether or not to follow upstream + # defaults when updating golang-ci-lint versions. + # Unfortunately, this means we have to copy the whole exclusion pattern, as + # (unlike the "include" option), the "exclude" option does not take exclusion + # ID's. + # + # These exclusion patterns are copied from the default excluses at: + # https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104 + # EXC0001 + - text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked" + linters: + - errcheck + # EXC0003 + - text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this" + linters: + - revive + # EXC0006 + - text: "Use of unsafe calls should be audited" + linters: + - gosec + # EXC0007 + - text: "Subprocess launch(ed with variable|ing should be audited)" + linters: + - gosec + # EXC0008 + - text: "G307" + linters: + - gosec + # EXC0009 + - text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)" + linters: + - gosec + # EXC0010 + - text: "Potential file inclusion via variable" + linters: + - gosec + - text: "G113" + linters: + - gosec + - text: "G104" + linters: + - gosec + - text: "G204: Subprocess launched with a potential tainted input or cmd arguments" + linters: + - gosec + - text: "G306: Expect WriteFile permissions to be 0600 or less" + linters: + - gosec + - text: "package-comments: should have a package comment" + linters: + - revive + - path: _test\.go + linters: + - errcheck + - gosec + - text: "ST1000: at least one file in a package should have a package comment" + linters: + - stylecheck + # Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives. + - text: '^shadow: declaration of "(err|ok)" shadows declaration' + linters: + - govet + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-issues-per-linter: 0 + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 00000000..d3af8a1c --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,46 @@ +project_name: mdz + +builds: + - id: "mdz" + main: ./components/mdz/main.go + binary: mdz + goos: + - linux + - darwin + - windows + goarch: + - amd64 + goarm: + - "6" + ldflags: + - -s -w # Flags to reduce binary size + +archives: + - id: "mdz" + builds: + - "mdz" + format: tar.gz + name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + wrap_in_directory: true + +snapshot: + name_template: "{{ .Tag }}-next" + +changelog: + skip: false + +git: + prerelease_suffix: "-" + +release: + prerelease: auto + +# brews: +# - name: mdz +# description: "Midaz CLI" +# homepage: "https://github.com/LerianStudio/midaz/components/mdz" +# tap: +# owner: LerianStudio +# name: homebrew-tap +# url_template: "https://github.com/LerianStudio/midaz/components/mdz/releases/download/{{ .Tag }}/{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.tar.gz" +# caveats: "Thanks for installing mdz!" diff --git a/.releaserc.yml b/.releaserc.yml new file mode 100644 index 00000000..d86646ec --- /dev/null +++ b/.releaserc.yml @@ -0,0 +1,66 @@ +plugins: + [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "parserOpts": + { "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"] }, + "releaseRules": + [ + { "type": "feat", "release": "minor" }, + { "type": "perf", "release": "minor" }, + { "type": "build", "release": "minor" }, + { "type": "chore", "release": "patch" }, + { "type": "ci", "release": "patch" }, + { "type": "test", "release": "patch" }, + { "type": "fix", "release": "minor" }, + { "type": "refactor", "release": "minor" }, + { "type": "docs", "release": "patch" }, + { "breaking": true, "release": "major" } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "parserOpts": { + "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"] + }, + "writerOpts": { + "commitsSort": ["subject", "scope"] + } + } + ], + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md" + } + ], + [ + "@semantic-release/git", + { + "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}", + "assets": ["CHANGELOG.md"] + } + ], + [ + "@semantic-release/github" + ], + [ + "@saithodev/semantic-release-backmerge", + { + "backmergeBranches": [{"from": "main", "to": "develop"}], + "message": "chore(release): Preparations for next release [skip ci]" + } + ] + ] + +branches: + - main + - name: develop + prerelease: "beta" + - name: hotfix/** + prerelease: "hf" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ef486bec --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,642 @@ +## [1.17.0](https://github.com/LerianStudio/midaz-private/compare/v1.16.0...v1.17.0) (2024-05-17) + + +### Features + +* enable CodeQL and adjust Readme :sparkles: ([7037bba](https://github.com/LerianStudio/midaz-private/commit/7037bba5a16d8e96d15e56f9f0b137524ed17a14)) + + +### Bug Fixes + +* clint :bug: ([9953ad5](https://github.com/LerianStudio/midaz-private/commit/9953ad58e904bf0d30bac70389f880c690a77b6d)) +* source and imports :bug: ([b91ec61](https://github.com/LerianStudio/midaz-private/commit/b91ec61193be7e2a0d78ae8f2047e90335c434e5)) + +## [1.17.0-beta.1](https://github.com/LerianStudio/midaz-private/compare/v1.16.0...v1.17.0-beta.1) (2024-05-17) + + +### Features + +* enable CodeQL and adjust Readme :sparkles: ([7037bba](https://github.com/LerianStudio/midaz-private/commit/7037bba5a16d8e96d15e56f9f0b137524ed17a14)) + + +### Bug Fixes + +* clint :bug: ([9953ad5](https://github.com/LerianStudio/midaz-private/commit/9953ad58e904bf0d30bac70389f880c690a77b6d)) +* source and imports :bug: ([b91ec61](https://github.com/LerianStudio/midaz-private/commit/b91ec61193be7e2a0d78ae8f2047e90335c434e5)) + +## [1.16.0](https://github.com/LerianStudio/midaz-private/compare/v1.15.0...v1.16.0) (2024-05-17) + +## [1.15.0](https://github.com/LerianStudio/midaz-private/compare/v1.14.0...v1.15.0) (2024-05-13) + + +### Bug Fixes + +* adapters :bug: ([f1eab22](https://github.com/LerianStudio/midaz-private/commit/f1eab221117afc8b4f132eb75c2485f034de68aa)) +* domain :bug: ([f066eec](https://github.com/LerianStudio/midaz-private/commit/f066eec4d497fac2bde509e81315f8f11027ff6c)) +* final :bug: ([3071ab2](https://github.com/LerianStudio/midaz-private/commit/3071ab246cbb085f2df438664f1440b385723ad9)) +* gen :bug: ([dd601a5](https://github.com/LerianStudio/midaz-private/commit/dd601a59dec321d9b98c2cad08fa94b8b505c42a)) +* import :bug: ([d66ffae](https://github.com/LerianStudio/midaz-private/commit/d66ffae65b0ebc14cbef4c746f3d047a5e3bca5b)) +* imports :bug: ([b4649ec](https://github.com/LerianStudio/midaz-private/commit/b4649ecb2824fc7ce4d94c01a6cd6e393a5ed910)) +* metadata :bug: ([a15b08e](https://github.com/LerianStudio/midaz-private/commit/a15b08e1004cfa69532ed9d080bf9d91b6a8740d)) +* routes :bug: ([340ebf3](https://github.com/LerianStudio/midaz-private/commit/340ebf39106236c2fc134fa079243b526ba7093f)) + +## [1.15.0-beta.1](https://github.com/LerianStudio/midaz-private/compare/v1.14.0...v1.15.0-beta.1) (2024-05-13) + + +### Bug Fixes + +* adapters :bug: ([f1eab22](https://github.com/LerianStudio/midaz-private/commit/f1eab221117afc8b4f132eb75c2485f034de68aa)) +* domain :bug: ([f066eec](https://github.com/LerianStudio/midaz-private/commit/f066eec4d497fac2bde509e81315f8f11027ff6c)) +* final :bug: ([3071ab2](https://github.com/LerianStudio/midaz-private/commit/3071ab246cbb085f2df438664f1440b385723ad9)) +* gen :bug: ([dd601a5](https://github.com/LerianStudio/midaz-private/commit/dd601a59dec321d9b98c2cad08fa94b8b505c42a)) +* import :bug: ([d66ffae](https://github.com/LerianStudio/midaz-private/commit/d66ffae65b0ebc14cbef4c746f3d047a5e3bca5b)) +* imports :bug: ([b4649ec](https://github.com/LerianStudio/midaz-private/commit/b4649ecb2824fc7ce4d94c01a6cd6e393a5ed910)) +* metadata :bug: ([a15b08e](https://github.com/LerianStudio/midaz-private/commit/a15b08e1004cfa69532ed9d080bf9d91b6a8740d)) +* routes :bug: ([340ebf3](https://github.com/LerianStudio/midaz-private/commit/340ebf39106236c2fc134fa079243b526ba7093f)) + +## [1.14.0](https://github.com/LerianStudio/midaz-private/compare/v1.13.0...v1.14.0) (2024-05-10) + + +### Bug Fixes + +* get connection everytime and mongo database name :bug: ([36e9ffa](https://github.com/LerianStudio/midaz-private/commit/36e9ffa586a1dbca8c043d3eaa0ac80f34d431b4)) + +## [1.14.0-beta.1](https://github.com/LerianStudio/midaz-private/compare/v1.13.0...v1.14.0-beta.1) (2024-05-10) + + +### Bug Fixes + +* get connection everytime and mongo database name :bug: ([36e9ffa](https://github.com/LerianStudio/midaz-private/commit/36e9ffa586a1dbca8c043d3eaa0ac80f34d431b4)) + +## [1.13.0](https://github.com/LerianStudio/midaz-private/compare/v1.12.0...v1.13.0) (2024-05-10) + + +### Bug Fixes + +* gen :bug: ([d196ebb](https://github.com/LerianStudio/midaz-private/commit/d196ebb742ac9a7df39f6224ace0bbcdd17a1a4b)) +* make lint :bug: ([b89f0f4](https://github.com/LerianStudio/midaz-private/commit/b89f0f4eaa8067fa339b855012f10557ce68faa3)) +* make lint and make formmat :bug: ([c559f01](https://github.com/LerianStudio/midaz-private/commit/c559f012b9e4a2ba60d6e2acffd06cceba9f9893)) +* remove docker-composer version and make lint :bug: ([b002f0b](https://github.com/LerianStudio/midaz-private/commit/b002f0be0e1cb8ee17661855c55549ad275b20ff)) + +## [1.13.0-beta.1](https://github.com/LerianStudio/midaz-private/compare/v1.12.0...v1.13.0-beta.1) (2024-05-10) + + +### Bug Fixes + +* gen :bug: ([d196ebb](https://github.com/LerianStudio/midaz-private/commit/d196ebb742ac9a7df39f6224ace0bbcdd17a1a4b)) +* make lint :bug: ([b89f0f4](https://github.com/LerianStudio/midaz-private/commit/b89f0f4eaa8067fa339b855012f10557ce68faa3)) +* make lint and make formmat :bug: ([c559f01](https://github.com/LerianStudio/midaz-private/commit/c559f012b9e4a2ba60d6e2acffd06cceba9f9893)) +* remove docker-composer version and make lint :bug: ([b002f0b](https://github.com/LerianStudio/midaz-private/commit/b002f0be0e1cb8ee17661855c55549ad275b20ff)) + +## [1.12.0](https://github.com/LerianStudio/midaz-private/compare/v1.11.0...v1.12.0) (2024-05-09) + + +### Bug Fixes + +* adapters :bug: ([6ca68a5](https://github.com/LerianStudio/midaz-private/commit/6ca68a59c203da4448cff46c33221a1c6666a168)) +* adapters :bug: ([34f3944](https://github.com/LerianStudio/midaz-private/commit/34f39444aba0027e8ae3afc0b10ee09b4f812b49)) +* command tests :bug: ([4ccd163](https://github.com/LerianStudio/midaz-private/commit/4ccd163e39f2c292b4952ba4df5531626684b7c8)) +* domain :bug: ([5742d35](https://github.com/LerianStudio/midaz-private/commit/5742d353bddf58c9afd11303918c5d44574b8ae5)) +* make lint :bug: ([cbbc9bb](https://github.com/LerianStudio/midaz-private/commit/cbbc9bbe324482c01d59f58d1c9f2793392c539f)) +* migrations :bug: ([7120e4c](https://github.com/LerianStudio/midaz-private/commit/7120e4c7c7012e06e8ffdbc708bbe185863fb1f7)) +* mock :bug: ([62a08fd](https://github.com/LerianStudio/midaz-private/commit/62a08fdd401f13b3d4a13d047253dde315537a8f)) +* ports :bug: ([b1142f3](https://github.com/LerianStudio/midaz-private/commit/b1142f3d500c5a6241df681e471172a189ccf105)) +* postman :bug: ([ab44d0a](https://github.com/LerianStudio/midaz-private/commit/ab44d0a31b3a4fb41920abedbd64508dfbf65bde)) +* query tests :bug: ([c974c5d](https://github.com/LerianStudio/midaz-private/commit/c974c5d8137b6387b86a7f7894c153ac62be12d6)) + +## [1.11.0](https://github.com/LerianStudio/midaz-private/compare/v1.10.0...v1.11.0) (2024-05-08) + + +### Features + +* Creating parentOrganizationId to Organizations ([b1f7c9f](https://github.com/LerianStudio/midaz-private/commit/b1f7c9fe147d3440cbc896221364a2519329e8fa)) + + +### Bug Fixes + +* adapters :bug: ([8735d43](https://github.com/LerianStudio/midaz-private/commit/8735d43e4f5dc05f1ae8ccb0ed087e5761c9501e)) +* adapters :bug: ([d763478](https://github.com/LerianStudio/midaz-private/commit/d763478d5a9bb44783e77ab0167340df4445c5ee)) +* add version in conventional-changelog-conventionalcommits extra plugin :bug: ([b6d100b](https://github.com/LerianStudio/midaz-private/commit/b6d100b928d18d2a35331a87feca50c779c8447f)) +* command :bug: ([97fb718](https://github.com/LerianStudio/midaz-private/commit/97fb718f3725f652746d34434989ba7bf18aaf63)) +* command sql ([5cf410f](https://github.com/LerianStudio/midaz-private/commit/5cf410fc6b7eef63698ff4cbc3c48eea7651b3e4)) +* commands :bug: ([eb2eda0](https://github.com/LerianStudio/midaz-private/commit/eb2eda09212af7c1837a6d1fa6b987a52a9509c6)) +* domains :bug: ([3c7a6bd](https://github.com/LerianStudio/midaz-private/commit/3c7a6bd39fd1f9f182242461b44694882243f84e)) +* final adjustments ([9ad840e](https://github.com/LerianStudio/midaz-private/commit/9ad840ef0e1e39cad31288cc9b39bbd368d575e0)) +* gofmt ([a9f0544](https://github.com/LerianStudio/midaz-private/commit/a9f0544a38508e9d3b37794a1b44af88216c63bb)) +* handlers and routes ([98ba8ea](https://github.com/LerianStudio/midaz-private/commit/98ba8eae8369e85727292ba7ecc66c792f9390d3)) +* interface and postgres implementation ([ae4fa6f](https://github.com/LerianStudio/midaz-private/commit/ae4fa6ffdba9f5f91c169611eea864e3efb3cb09)) +* lint ([23bdd49](https://github.com/LerianStudio/midaz-private/commit/23bdd49daced9c6040134706871a6c7811d267fe)) +* make lint, make sec and tests :bug: ([b8df6a4](https://github.com/LerianStudio/midaz-private/commit/b8df6a45ddecf7cd61e5db2a41e2d1cd7ace404d)) +* make sec and make lint ([fac8e3a](https://github.com/LerianStudio/midaz-private/commit/fac8e3a392a5139236fd8dab1badb656e7e2fc35)) +* migrations ([82c82ba](https://github.com/LerianStudio/midaz-private/commit/82c82ba7b20c966d5dc6de13937c3386b21cc699)) +* migrations :bug: ([f5a2ddf](https://github.com/LerianStudio/midaz-private/commit/f5a2ddfcb83558abcda52159af3de36ae0c0bdb3)) +* ports :bug: ([96e2b8c](https://github.com/LerianStudio/midaz-private/commit/96e2b8cf800fdb23496804f326799dc0e91c39cd)) +* ports :bug: ([37d1010](https://github.com/LerianStudio/midaz-private/commit/37d1010e4bed83b73d993582e137135c048771c7)) +* ports :bug: ([4e2664c](https://github.com/LerianStudio/midaz-private/commit/4e2664ce27a48fca5b359a102e685c56bc81be0f)) +* postman :bug: ([dd7d9c3](https://github.com/LerianStudio/midaz-private/commit/dd7d9c39c9ff182b2ffd7d4d1ebb274b2492a541)) +* queries :bug: ([ecaaa34](https://github.com/LerianStudio/midaz-private/commit/ecaaa34b715aca14159167b26a1a52a16667e884)) +* query sql ([fdc2de8](https://github.com/LerianStudio/midaz-private/commit/fdc2de8841246382ed4cf7edf6026990e041f187)) +* **divisions:** remove everything from divisions ([5cbed6e](https://github.com/LerianStudio/midaz-private/commit/5cbed6e67ad219ef7aacb4190121f1b6ce804999)) +* remove immudb from ledger ([6264110](https://github.com/LerianStudio/midaz-private/commit/6264110af51d4d9d2222d760be91a2983ee4f050)) +* template ([5519aa2](https://github.com/LerianStudio/midaz-private/commit/5519aa2d614bb845108b87f967b91c32814040f8)) +* tests ([4c3be58](https://github.com/LerianStudio/midaz-private/commit/4c3be58a69a79e2aec1453a93dc7b411388ddac4)) + +## [1.11.0-beta.3](https://github.com/LerianStudio/midaz-private/compare/v1.11.0-beta.2...v1.11.0-beta.3) (2024-05-08) + + +### Bug Fixes + +* adapters :bug: ([8735d43](https://github.com/LerianStudio/midaz-private/commit/8735d43e4f5dc05f1ae8ccb0ed087e5761c9501e)) +* adapters :bug: ([d763478](https://github.com/LerianStudio/midaz-private/commit/d763478d5a9bb44783e77ab0167340df4445c5ee)) +* command :bug: ([97fb718](https://github.com/LerianStudio/midaz-private/commit/97fb718f3725f652746d34434989ba7bf18aaf63)) +* commands :bug: ([eb2eda0](https://github.com/LerianStudio/midaz-private/commit/eb2eda09212af7c1837a6d1fa6b987a52a9509c6)) +* domains :bug: ([3c7a6bd](https://github.com/LerianStudio/midaz-private/commit/3c7a6bd39fd1f9f182242461b44694882243f84e)) +* make lint, make sec and tests :bug: ([b8df6a4](https://github.com/LerianStudio/midaz-private/commit/b8df6a45ddecf7cd61e5db2a41e2d1cd7ace404d)) +* migrations :bug: ([f5a2ddf](https://github.com/LerianStudio/midaz-private/commit/f5a2ddfcb83558abcda52159af3de36ae0c0bdb3)) +* ports :bug: ([96e2b8c](https://github.com/LerianStudio/midaz-private/commit/96e2b8cf800fdb23496804f326799dc0e91c39cd)) +* ports :bug: ([37d1010](https://github.com/LerianStudio/midaz-private/commit/37d1010e4bed83b73d993582e137135c048771c7)) +* ports :bug: ([4e2664c](https://github.com/LerianStudio/midaz-private/commit/4e2664ce27a48fca5b359a102e685c56bc81be0f)) +* postman :bug: ([dd7d9c3](https://github.com/LerianStudio/midaz-private/commit/dd7d9c39c9ff182b2ffd7d4d1ebb274b2492a541)) +* queries :bug: ([ecaaa34](https://github.com/LerianStudio/midaz-private/commit/ecaaa34b715aca14159167b26a1a52a16667e884)) + +## [1.11.0-beta.2](https://github.com/LerianStudio/midaz-private/compare/v1.11.0-beta.1...v1.11.0-beta.2) (2024-05-07) + + +### Features + +* Creating parentOrganizationId to Organizations ([b1f7c9f](https://github.com/LerianStudio/midaz-private/commit/b1f7c9fe147d3440cbc896221364a2519329e8fa)) + + +### Bug Fixes + +* add version in conventional-changelog-conventionalcommits extra plugin :bug: ([b6d100b](https://github.com/LerianStudio/midaz-private/commit/b6d100b928d18d2a35331a87feca50c779c8447f)) +* command sql ([5cf410f](https://github.com/LerianStudio/midaz-private/commit/5cf410fc6b7eef63698ff4cbc3c48eea7651b3e4)) +* final adjustments ([9ad840e](https://github.com/LerianStudio/midaz-private/commit/9ad840ef0e1e39cad31288cc9b39bbd368d575e0)) +* gofmt ([a9f0544](https://github.com/LerianStudio/midaz-private/commit/a9f0544a38508e9d3b37794a1b44af88216c63bb)) +* handlers and routes ([98ba8ea](https://github.com/LerianStudio/midaz-private/commit/98ba8eae8369e85727292ba7ecc66c792f9390d3)) +* interface and postgres implementation ([ae4fa6f](https://github.com/LerianStudio/midaz-private/commit/ae4fa6ffdba9f5f91c169611eea864e3efb3cb09)) +* lint ([23bdd49](https://github.com/LerianStudio/midaz-private/commit/23bdd49daced9c6040134706871a6c7811d267fe)) +* make sec and make lint ([fac8e3a](https://github.com/LerianStudio/midaz-private/commit/fac8e3a392a5139236fd8dab1badb656e7e2fc35)) +* migrations ([82c82ba](https://github.com/LerianStudio/midaz-private/commit/82c82ba7b20c966d5dc6de13937c3386b21cc699)) +* query sql ([fdc2de8](https://github.com/LerianStudio/midaz-private/commit/fdc2de8841246382ed4cf7edf6026990e041f187)) +* **divisions:** remove everything from divisions ([5cbed6e](https://github.com/LerianStudio/midaz-private/commit/5cbed6e67ad219ef7aacb4190121f1b6ce804999)) +* remove immudb from ledger ([6264110](https://github.com/LerianStudio/midaz-private/commit/6264110af51d4d9d2222d760be91a2983ee4f050)) +* template ([5519aa2](https://github.com/LerianStudio/midaz-private/commit/5519aa2d614bb845108b87f967b91c32814040f8)) +* tests ([4c3be58](https://github.com/LerianStudio/midaz-private/commit/4c3be58a69a79e2aec1453a93dc7b411388ddac4)) + +## [1.11.0-beta.1](https://github.com/LerianStudio/midaz-private/compare/v1.10.0...v1.11.0-beta.1) (2024-04-30) + +## [1.10.0](https://github.com/LerianStudio/midaz-private/compare/v1.9.0...v1.10.0) (2024-04-25) + + +### Features + +* **doc:** add first version of open api doc ([16b3bc7](https://github.com/LerianStudio/midaz-private/commit/16b3bc7d462a7e9ee2b81e1db7976d0322a9a202)) +* **doc:** add initial swagger impl ([d50a18b](https://github.com/LerianStudio/midaz-private/commit/d50a18b368416f35eb0028010fc1cfd241654d4d)) +* Add primary and replica immudb to the transaction domain, along with improvements such as variable renaming. ([b68d76a](https://github.com/LerianStudio/midaz-private/commit/b68d76a042f3844ead90c46adcca4eca4cbaca3c)) +* **doc:** introduce updated version of doc ([048fee7](https://github.com/LerianStudio/midaz-private/commit/048fee79d2c1f427689f37c50b41202b3666c6ab)) + + +### Bug Fixes + +* **metadata:** add length validation in metadata fields key and value ([d7faaad](https://github.com/LerianStudio/midaz-private/commit/d7faaad7cac780d99014cf95cc8725d5e7a8caa3)) +* **doc:** adjust doc path ([244aae7](https://github.com/LerianStudio/midaz-private/commit/244aae7f0334c9a781f2bc47673a3dd90f4a28af)) +* **lint:** adjust linter issues ([9dd364f](https://github.com/LerianStudio/midaz-private/commit/9dd364fa8b5af52ca290feb8c135650c94d2f21f)) +* **linter:** adjust linter issues ([9ebc80b](https://github.com/LerianStudio/midaz-private/commit/9ebc80b55a246e044054ce78bb43e9c2cbca5d9e)) +* error merge ([8da0131](https://github.com/LerianStudio/midaz-private/commit/8da013131f9a57ae5fdd2011d02c16493a230d4d)) +* **metadata:** remove empty-lines extra empty line at the start of a block ([5837adf](https://github.com/LerianStudio/midaz-private/commit/5837adf877bccd641cf22f64c34f02c790500271)) +* removing fake secrets from .env.example :bug: ([700fc11](https://github.com/LerianStudio/midaz-private/commit/700fc110e78fa14203df39b812a55bfcbf7d5f01)) +* removing fake secrets from .env.example :bug: ([8c025f0](https://github.com/LerianStudio/midaz-private/commit/8c025f05c8cd1378dbf377e9d730dd8206d5f871)) +* removing one immudb common :bug: ([35f1e43](https://github.com/LerianStudio/midaz-private/commit/35f1e4366a362fddc4c605937cd2eb27c4fffc06)) +* removing one immudb common :bug: ([e0b7aae](https://github.com/LerianStudio/midaz-private/commit/e0b7aae1bf0d05fb7117f5eb7ee737b3b3f4c4bf)) + +## [1.10.0-beta.3](https://github.com/LerianStudio/midaz-private/compare/v1.10.0-beta.2...v1.10.0-beta.3) (2024-04-25) + + +### Features + +* **doc:** add first version of open api doc ([16b3bc7](https://github.com/LerianStudio/midaz-private/commit/16b3bc7d462a7e9ee2b81e1db7976d0322a9a202)) +* **doc:** add initial swagger impl ([d50a18b](https://github.com/LerianStudio/midaz-private/commit/d50a18b368416f35eb0028010fc1cfd241654d4d)) +* Add primary and replica immudb to the transaction domain, along with improvements such as variable renaming. ([b68d76a](https://github.com/LerianStudio/midaz-private/commit/b68d76a042f3844ead90c46adcca4eca4cbaca3c)) +* **doc:** introduce updated version of doc ([048fee7](https://github.com/LerianStudio/midaz-private/commit/048fee79d2c1f427689f37c50b41202b3666c6ab)) + + +### Bug Fixes + +* **metadata:** add length validation in metadata fields key and value ([d7faaad](https://github.com/LerianStudio/midaz-private/commit/d7faaad7cac780d99014cf95cc8725d5e7a8caa3)) +* **doc:** adjust doc path ([244aae7](https://github.com/LerianStudio/midaz-private/commit/244aae7f0334c9a781f2bc47673a3dd90f4a28af)) +* **linter:** adjust linter issues ([9ebc80b](https://github.com/LerianStudio/midaz-private/commit/9ebc80b55a246e044054ce78bb43e9c2cbca5d9e)) +* error merge ([8da0131](https://github.com/LerianStudio/midaz-private/commit/8da013131f9a57ae5fdd2011d02c16493a230d4d)) +* **metadata:** remove empty-lines extra empty line at the start of a block ([5837adf](https://github.com/LerianStudio/midaz-private/commit/5837adf877bccd641cf22f64c34f02c790500271)) +* removing fake secrets from .env.example :bug: ([700fc11](https://github.com/LerianStudio/midaz-private/commit/700fc110e78fa14203df39b812a55bfcbf7d5f01)) +* removing fake secrets from .env.example :bug: ([8c025f0](https://github.com/LerianStudio/midaz-private/commit/8c025f05c8cd1378dbf377e9d730dd8206d5f871)) +* removing one immudb common :bug: ([35f1e43](https://github.com/LerianStudio/midaz-private/commit/35f1e4366a362fddc4c605937cd2eb27c4fffc06)) +* removing one immudb common :bug: ([e0b7aae](https://github.com/LerianStudio/midaz-private/commit/e0b7aae1bf0d05fb7117f5eb7ee737b3b3f4c4bf)) + +## [1.10.0-beta.2](https://github.com/LerianStudio/midaz-private/compare/v1.10.0-beta.1...v1.10.0-beta.2) (2024-04-23) + +## [1.10.0-beta.1](https://github.com/LerianStudio/midaz-private/compare/v1.9.0...v1.10.0-beta.1) (2024-04-22) + + +### Bug Fixes + +* **lint:** adjust linter issues ([9dd364f](https://github.com/LerianStudio/midaz-private/commit/9dd364fa8b5af52ca290feb8c135650c94d2f21f)) + +## [1.9.0](https://github.com/LerianStudio/midaz-private/compare/v1.8.0...v1.9.0) (2024-04-19) + + +### Features + +* add func to convert from camel to snake case ([4d49b7e](https://github.com/LerianStudio/midaz-private/commit/4d49b7e1d9575495b89e26106b55cb387cba7f89)) +* **MZ-136:** add sort by created_at desc for list queries ([5af3e81](https://github.com/LerianStudio/midaz-private/commit/5af3e8194bafc2f3badfa20d8b5f4effb28b45e6)) +* add steps to goreleaser into release workflow :sparkles: ([394470d](https://github.com/LerianStudio/midaz-private/commit/394470d411ea74fbc755a2e938f3a441a5912961)) +* **routes:** uncomment portfolio routes ([d16cddc](https://github.com/LerianStudio/midaz-private/commit/d16cddc6ce9972b45435d2bef64886ff163420d0)) + + +### Bug Fixes + +* **portfolio:** add missing updated_at logic for update portfolio flow ([b1e572d](https://github.com/LerianStudio/midaz-private/commit/b1e572ddf981fd5dbb236d7cef8503437f2a6308)) +* **linter:** adjust linter issues ([cac1b7d](https://github.com/LerianStudio/midaz-private/commit/cac1b7d2f2eb1b24d1e7a56735fae55545e92ef6)) +* **linter:** adjust linter issues ([9953697](https://github.com/LerianStudio/midaz-private/commit/99536973603b80e1741d744c130b211107757ce0)) +* debug goreleaser ([ab68e55](https://github.com/LerianStudio/midaz-private/commit/ab68e55e3cacb4545b9bc1d37161b68bfc5b4a1e)) +* **linter:** remove cuddled declarations ([5a0554c](https://github.com/LerianStudio/midaz-private/commit/5a0554c0340ccc7038ebea98228e07afff25c0e1)) +* **linter:** remove usage of interface ([6523326](https://github.com/LerianStudio/midaz-private/commit/6523326bd19238ab786facdc452589770fd448fe)) +* **sql:** remove wrong usage of any instead of in for list queries ([b187140](https://github.com/LerianStudio/midaz-private/commit/b1871405df0f762a62ec9a89375bf6408ba104f9)) + +## [1.9.0-beta.6](https://github.com/LerianStudio/midaz-private/compare/v1.9.0-beta.5...v1.9.0-beta.6) (2024-04-19) + + +### Bug Fixes + +* **portfolio:** add missing updated_at logic for update portfolio flow ([b1e572d](https://github.com/LerianStudio/midaz-private/commit/b1e572ddf981fd5dbb236d7cef8503437f2a6308)) + +## [1.9.0-beta.5](https://github.com/LerianStudio/midaz-private/compare/v1.9.0-beta.4...v1.9.0-beta.5) (2024-04-19) + + +### Features + +* add func to convert from camel to snake case ([4d49b7e](https://github.com/LerianStudio/midaz-private/commit/4d49b7e1d9575495b89e26106b55cb387cba7f89)) +* **MZ-136:** add sort by created_at desc for list queries ([5af3e81](https://github.com/LerianStudio/midaz-private/commit/5af3e8194bafc2f3badfa20d8b5f4effb28b45e6)) +* **routes:** uncomment portfolio routes ([d16cddc](https://github.com/LerianStudio/midaz-private/commit/d16cddc6ce9972b45435d2bef64886ff163420d0)) + + +### Bug Fixes + +* **linter:** adjust linter issues ([cac1b7d](https://github.com/LerianStudio/midaz-private/commit/cac1b7d2f2eb1b24d1e7a56735fae55545e92ef6)) +* **linter:** adjust linter issues ([9953697](https://github.com/LerianStudio/midaz-private/commit/99536973603b80e1741d744c130b211107757ce0)) +* debug goreleaser ([ab68e55](https://github.com/LerianStudio/midaz-private/commit/ab68e55e3cacb4545b9bc1d37161b68bfc5b4a1e)) +* **linter:** remove cuddled declarations ([5a0554c](https://github.com/LerianStudio/midaz-private/commit/5a0554c0340ccc7038ebea98228e07afff25c0e1)) +* **linter:** remove usage of interface ([6523326](https://github.com/LerianStudio/midaz-private/commit/6523326bd19238ab786facdc452589770fd448fe)) +* **sql:** remove wrong usage of any instead of in for list queries ([b187140](https://github.com/LerianStudio/midaz-private/commit/b1871405df0f762a62ec9a89375bf6408ba104f9)) + +## [1.9.0-beta.4](https://github.com/LerianStudio/midaz-private/compare/v1.9.0-beta.3...v1.9.0-beta.4) (2024-04-18) + +## [1.9.0-beta.3](https://github.com/LerianStudio/midaz-private/compare/v1.9.0-beta.2...v1.9.0-beta.3) (2024-04-18) + +## [1.9.0-beta.2](https://github.com/LerianStudio/midaz-private/compare/v1.9.0-beta.1...v1.9.0-beta.2) (2024-04-18) + + +### Features + +* add steps to goreleaser into release workflow :sparkles: ([394470d](https://github.com/LerianStudio/midaz-private/commit/394470d411ea74fbc755a2e938f3a441a5912961)) + +## [1.9.0-beta.1](https://github.com/LerianStudio/midaz-private/compare/v1.8.0...v1.9.0-beta.1) (2024-04-18) + +## [1.8.0](https://github.com/LerianStudio/midaz-private/compare/v1.7.0...v1.8.0) (2024-04-17) + + +### Features + +* Creating a very cool feature :sparkles: ([b84daf1](https://github.com/LerianStudio/midaz-private/commit/b84daf135b224dd229a22a56d228123e1ede5bf5)) + +## [1.8.0-beta.1](https://github.com/LerianStudio/midaz-private/compare/v1.7.0...v1.8.0-beta.1) (2024-04-17) + + +### Features + +* Creating a very cool feature :sparkles: ([b84daf1](https://github.com/LerianStudio/midaz-private/commit/b84daf135b224dd229a22a56d228123e1ede5bf5)) + +## [1.7.0](https://github.com/LerianStudio/midaz-private/compare/v1.6.0...v1.7.0) (2024-04-17) + +## [1.7.0-beta.1](https://github.com/LerianStudio/midaz-private/compare/v1.6.0...v1.7.0-beta.1) (2024-04-17) + +## [1.6.0](https://github.com/LerianStudio/midaz/compare/v1.5.0...v1.6.0) (2024-04-16) + +## [1.6.0-beta.2](https://github.com/LerianStudio/midaz/compare/v1.6.0-beta.1...v1.6.0-beta.2) (2024-04-16) + +## [1.6.0-beta.1](https://github.com/LerianStudio/midaz/compare/v1.5.0...v1.6.0-beta.1) (2024-04-16) + +## [1.5.0](https://github.com/LerianStudio/midaz/compare/v1.4.0...v1.5.0) (2024-04-16) + + +### Features + +* Remove slack notifications in release and build jobs :sparkles: ([3c629cb](https://github.com/LerianStudio/midaz/commit/3c629cb7ea635fdc4d7101737f8f2026c418f2b1)) + +## [1.5.0-beta.1](https://github.com/LerianStudio/midaz/compare/v1.4.0...v1.5.0-beta.1) (2024-04-16) + + +### Features + +* Remove slack notifications in release and build jobs :sparkles: ([3c629cb](https://github.com/LerianStudio/midaz/commit/3c629cb7ea635fdc4d7101737f8f2026c418f2b1)) + +## [1.4.0](https://github.com/LerianStudio/midaz/compare/v1.3.0...v1.4.0) (2024-04-16) + + +### Bug Fixes + +* Remove slack notifications and add changelog notification to Discord :bug: ([315dbd6](https://github.com/LerianStudio/midaz/commit/315dbd616afac5e0c7410d5f65831681b3bb93fe)) + +## [1.3.0](https://github.com/LerianStudio/midaz/compare/v1.2.0...v1.3.0) (2024-04-16) + + +### Features + +* **portfolio:** refactor portfolio model and migration ([f9f0157](https://github.com/LerianStudio/midaz/commit/f9f015795510e2b1c84e41c5e1678f836bd3de7d)) +* remove ignored files in pipelines ([a6f0ace](https://github.com/LerianStudio/midaz/commit/a6f0ace582c7e7c70b4d089abcceab279758db92)) + + +### Bug Fixes + +* **envs:** add usage of env vars for replica database ([e243e45](https://github.com/LerianStudio/midaz/commit/e243e4506b10babe0b52efbbf74056c8a0300362)) +* **linter:** adjust formatting; adjust line separators ([e9df066](https://github.com/LerianStudio/midaz/commit/e9df066dda7cec40dc520720b64b8e5d63b48c86)) +* **compose:** adjust replica database configuration for correct port settings; use of own healthcheck; adjust dependency with primary healthy status ([244f693](https://github.com/LerianStudio/midaz/commit/244f693e625fb13694d9296841ea1cf6e34128a6)) +* ajustando o tamanho do map ([67d5177](https://github.com/LerianStudio/midaz/commit/67d5177fd9b8f357f3dec37930d5b40fcfba4cea)) +* ajuste na classe get-all-accounts ([ded8579](https://github.com/LerianStudio/midaz/commit/ded8579784966369be59d2cf54e0a8c67e58b12f)) +* lint ajustes ([a09e718](https://github.com/LerianStudio/midaz/commit/a09e718efd511679106a4e9ac3fffd1c4ff7caa6)) +* Rollback line :bug: ([da4c101](https://github.com/LerianStudio/midaz/commit/da4c1012a84bb5d8eedd578aed16896d28884d06)) +* **sec:** update dependencies version to patch vulnerabilities ([40dc35f](https://github.com/LerianStudio/midaz/commit/40dc35faf244cf24642d830e91fea41237068ead)) + +## [1.3.0-beta.1](https://github.com/LerianStudio/midaz/compare/v1.2.0...v1.3.0-beta.1) (2024-04-16) + + +### Features + +* **portfolio:** refactor portfolio model and migration ([f9f0157](https://github.com/LerianStudio/midaz/commit/f9f015795510e2b1c84e41c5e1678f836bd3de7d)) +* remove ignored files in pipelines ([a6f0ace](https://github.com/LerianStudio/midaz/commit/a6f0ace582c7e7c70b4d089abcceab279758db92)) + + +### Bug Fixes + +* **envs:** add usage of env vars for replica database ([e243e45](https://github.com/LerianStudio/midaz/commit/e243e4506b10babe0b52efbbf74056c8a0300362)) +* **linter:** adjust formatting; adjust line separators ([e9df066](https://github.com/LerianStudio/midaz/commit/e9df066dda7cec40dc520720b64b8e5d63b48c86)) +* **compose:** adjust replica database configuration for correct port settings; use of own healthcheck; adjust dependency with primary healthy status ([244f693](https://github.com/LerianStudio/midaz/commit/244f693e625fb13694d9296841ea1cf6e34128a6)) +* ajustando o tamanho do map ([67d5177](https://github.com/LerianStudio/midaz/commit/67d5177fd9b8f357f3dec37930d5b40fcfba4cea)) +* ajuste na classe get-all-accounts ([ded8579](https://github.com/LerianStudio/midaz/commit/ded8579784966369be59d2cf54e0a8c67e58b12f)) +* lint ajustes ([a09e718](https://github.com/LerianStudio/midaz/commit/a09e718efd511679106a4e9ac3fffd1c4ff7caa6)) +* Rollback line :bug: ([da4c101](https://github.com/LerianStudio/midaz/commit/da4c1012a84bb5d8eedd578aed16896d28884d06)) +* **sec:** update dependencies version to patch vulnerabilities ([40dc35f](https://github.com/LerianStudio/midaz/commit/40dc35faf244cf24642d830e91fea41237068ead)) + +## [1.2.0](https://github.com/LerianStudio/midaz/compare/v1.1.0...v1.2.0) (2024-04-15) + + +### Features + +* split test jobs + add CODEOWNERS file + dependabot config :sparkles: ([04d1a57](https://github.com/LerianStudio/midaz/commit/04d1a57f15692cd1bf54b7ba37b1832165bcbeb5)) + + +### Bug Fixes + +* codeowners rules :bug: ([45e3abb](https://github.com/LerianStudio/midaz/commit/45e3abbd70dd4516c0e063ba57dda4d7615976d1)) + +## [1.2.0-beta.1](https://github.com/LerianStudio/midaz/compare/v1.1.0...v1.2.0-beta.1) (2024-04-15) + + +### Features + +* split test jobs + add CODEOWNERS file + dependabot config :sparkles: ([04d1a57](https://github.com/LerianStudio/midaz/commit/04d1a57f15692cd1bf54b7ba37b1832165bcbeb5)) + + +### Bug Fixes + +* codeowners rules :bug: ([45e3abb](https://github.com/LerianStudio/midaz/commit/45e3abbd70dd4516c0e063ba57dda4d7615976d1)) + +## [1.1.0](https://github.com/LerianStudio/midaz/compare/v1.0.3...v1.1.0) (2024-04-14) + + +### Features + +* **mpostgres:** Add create, update, delete functions :sparkles: ([bb993c7](https://github.com/LerianStudio/midaz/commit/bb993c784f192898b65e65b4af3c4ec20f40afa0)) +* **database:** Add dbresolver for primary and replica DBs :sparkles: ([de73be2](https://github.com/LerianStudio/midaz/commit/de73be261dcc8a0ee67f849918f0689ebc81afc6)) +* **common:** Add generic Contains function to utils :sparkles: ([0122d60](https://github.com/LerianStudio/midaz/commit/0122d60aaaf284bbd0975f06bcd61e20fa4f4a0e)) +* add gpg sign to bot commits :sparkles: ([a0169e4](https://github.com/LerianStudio/midaz/commit/a0169e46d7399078c7dd2bc183a2616fe3b31d49)) +* add gpg sign to bot commits :sparkles: ([62c95f0](https://github.com/LerianStudio/midaz/commit/62c95f0e11b22c8cd4414c58c134dfa41624b11d)) +* **common:** Add pointer and string utilities, update account fields :sparkles: ([e783f4a](https://github.com/LerianStudio/midaz/commit/e783f4a2cc83ebb8d729351bc7ddd29b3813c6f2)) +* **mpostgres:** Add SQL query builder and repository methods :sparkles: ([23294a2](https://github.com/LerianStudio/midaz/commit/23294a2760464b3f2811cd56be06a6a2b64a2d3a)) +* **database connection:** Enable MongoDB connection and fix docker-compose :sparkles: ([990c5f0](https://github.com/LerianStudio/midaz/commit/990c5f09dadd07c8480b99665fd4f41137f4d4b3)) + + +### Bug Fixes + +* debug gpg sign :bug: ([a0d7c78](https://github.com/LerianStudio/midaz/commit/a0d7c78b4a656a9d5fbf158b3d65500d58b7fa7a)) +* **ledger:** update host in pg_basebackup command :bug: ([1bb3d38](https://github.com/LerianStudio/midaz/commit/1bb3d38c708aa135e58960932153d0ff3d3ad636)) + +## [1.1.0-beta.4](https://github.com/LerianStudio/midaz/compare/v1.1.0-beta.3...v1.1.0-beta.4) (2024-04-14) + + +### Features + +* **mpostgres:** Add create, update, delete functions :sparkles: ([bb993c7](https://github.com/LerianStudio/midaz/commit/bb993c784f192898b65e65b4af3c4ec20f40afa0)) +* **database:** Add dbresolver for primary and replica DBs :sparkles: ([de73be2](https://github.com/LerianStudio/midaz/commit/de73be261dcc8a0ee67f849918f0689ebc81afc6)) +* **common:** Add generic Contains function to utils :sparkles: ([0122d60](https://github.com/LerianStudio/midaz/commit/0122d60aaaf284bbd0975f06bcd61e20fa4f4a0e)) +* **common:** Add pointer and string utilities, update account fields :sparkles: ([e783f4a](https://github.com/LerianStudio/midaz/commit/e783f4a2cc83ebb8d729351bc7ddd29b3813c6f2)) +* **mpostgres:** Add SQL query builder and repository methods :sparkles: ([23294a2](https://github.com/LerianStudio/midaz/commit/23294a2760464b3f2811cd56be06a6a2b64a2d3a)) +* **database connection:** Enable MongoDB connection and fix docker-compose :sparkles: ([990c5f0](https://github.com/LerianStudio/midaz/commit/990c5f09dadd07c8480b99665fd4f41137f4d4b3)) + + +### Bug Fixes + +* **ledger:** update host in pg_basebackup command :bug: ([1bb3d38](https://github.com/LerianStudio/midaz/commit/1bb3d38c708aa135e58960932153d0ff3d3ad636)) + +## [1.1.0-beta.3](https://github.com/LerianStudio/midaz/compare/v1.1.0-beta.2...v1.1.0-beta.3) (2024-04-12) + +## [1.1.0-beta.2](https://github.com/LerianStudio/midaz/compare/v1.1.0-beta.1...v1.1.0-beta.2) (2024-04-12) + +## [1.1.0-beta.1](https://github.com/LerianStudio/midaz/compare/v1.0.4-beta.2...v1.1.0-beta.1) (2024-04-12) + + +### Features + +* add gpg sign to bot commits :sparkles: ([a0169e4](https://github.com/LerianStudio/midaz/commit/a0169e46d7399078c7dd2bc183a2616fe3b31d49)) +* add gpg sign to bot commits :sparkles: ([62c95f0](https://github.com/LerianStudio/midaz/commit/62c95f0e11b22c8cd4414c58c134dfa41624b11d)) + + +### Bug Fixes + +* debug gpg sign :bug: ([a0d7c78](https://github.com/LerianStudio/midaz/commit/a0d7c78b4a656a9d5fbf158b3d65500d58b7fa7a)) + +## [1.1.0-beta.1](https://github.com/LerianStudio/midaz/compare/v1.0.4-beta.2...v1.1.0-beta.1) (2024-04-12) + + +### Features + +* add gpg sign to bot commits :sparkles: ([a0169e4](https://github.com/LerianStudio/midaz/commit/a0169e46d7399078c7dd2bc183a2616fe3b31d49)) +* add gpg sign to bot commits :sparkles: ([62c95f0](https://github.com/LerianStudio/midaz/commit/62c95f0e11b22c8cd4414c58c134dfa41624b11d)) + +## [1.0.4-beta.2](https://github.com/LerianStudio/midaz/compare/v1.0.4-beta.1...v1.0.4-beta.2) (2024-04-12) + +## [1.0.4-beta.1](https://github.com/LerianStudio/midaz/compare/v1.0.3...v1.0.4-beta.1) (2024-04-11) + +## [1.0.3](https://github.com/LerianStudio/midaz/compare/v1.0.2...v1.0.3) (2024-04-11) + +## [1.0.3-beta.1](https://github.com/LerianStudio/midaz/compare/v1.0.2...v1.0.3-beta.1) (2024-04-11) + +## [1.0.2](https://github.com/LerianStudio/midaz/compare/v1.0.1...v1.0.2) (2024-04-11) + +## [1.0.2-beta.1](https://github.com/LerianStudio/midaz/compare/v1.0.1...v1.0.2-beta.1) (2024-04-11) + +## [1.0.1](https://github.com/LerianStudio/midaz/compare/v1.0.0...v1.0.1) (2024-04-11) + +## [1.0.1-beta.6](https://github.com/LerianStudio/midaz/compare/v1.0.1-beta.5...v1.0.1-beta.6) (2024-04-11) + +## [1.0.1-beta.5](https://github.com/LerianStudio/midaz/compare/v1.0.1-beta.4...v1.0.1-beta.5) (2024-04-11) + +## [1.0.1-beta.4](https://github.com/LerianStudio/midaz/compare/v1.0.1-beta.3...v1.0.1-beta.4) (2024-04-11) + +## [1.0.1-beta.3](https://github.com/LerianStudio/midaz/compare/v1.0.1-beta.2...v1.0.1-beta.3) (2024-04-11) + +## [1.0.1-beta.2](https://github.com/LerianStudio/midaz/compare/v1.0.1-beta.1...v1.0.1-beta.2) (2024-04-11) + +## [1.0.1-beta.1](https://github.com/LerianStudio/midaz/compare/v1.0.0...v1.0.1-beta.1) (2024-04-11) + +## [1.0.0-beta.8](https://github.com/LerianStudio/midaz/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2024-04-11) + + +### Bug Fixes + +* app name to dockerhub push ([7d1400d](https://github.com/LerianStudio/midaz/commit/7d1400db642dce8df87a4b931969fc9c5177024e)) + +## [1.0.0-beta.7](https://github.com/LerianStudio/midaz/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2024-04-11) + + +### Bug Fixes + +* fix comma ([0db9660](https://github.com/LerianStudio/midaz/commit/0db9660729203937529885effa5c5996f5c75f67)) + +## [1.0.0-beta.7](https://github.com/LerianStudio/midaz/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2024-04-11) + +## [1.0.0-beta.6](https://github.com/LerianStudio/midaz/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2024-04-11) + +## [1.0.0-beta.5](https://github.com/LerianStudio/midaz/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2024-04-11) + +## [1.0.0-beta.4](https://github.com/LerianStudio/midaz/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2024-04-11) + +## [1.0.0-beta.3](https://github.com/LerianStudio/midaz/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2024-04-11) + +## [1.0.0-beta.2](https://github.com/LerianStudio/midaz/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2024-04-11) + + +### Bug Fixes + +* identation ([5796b66](https://github.com/LerianStudio/midaz/commit/5796b662b737fa4a26c7bb9cc575d95fbb91b357)) + +## 1.0.0-beta.1 (2024-04-11) + + +### Features + +* add accounts testes ([b621dc1](https://github.com/LerianStudio/midaz/commit/b621dc142a8a04514d7477b89abe72f03be3beaa)) +* **shell:** Add ASCII and color shell scripts ([d079910](https://github.com/LerianStudio/midaz/commit/d079910b467e8b6429cbf351222482afebc7a250)) +* add child-account testes ([e0620eb](https://github.com/LerianStudio/midaz/commit/e0620eb5de05ef498a53c2b1a659f0e93444ef28)) +* **Makefile:** Add cover test command :sparkles: ([f549db3](https://github.com/LerianStudio/midaz/commit/f549db3d18f55be1273b76640c604aea6a448ff7)) +* **NoSQL:** Add Create metadata with id organization ([d70b5d7](https://github.com/LerianStudio/midaz/commit/d70b5d7364ae025deb0f676c27e867a7dda9c046)) +* add DDL scripts for database migration ([25e5df3](https://github.com/LerianStudio/midaz/commit/25e5df35ea5585dbff69df330a4d3af70b0ed93b)) +* **Organization:** Add Delete ([ee76903](https://github.com/LerianStudio/midaz/commit/ee76903780547ea612ce0e2490c1878714d074e2)) +* **NoSQL:** Add dpdate & delete metadata on mongodb ([44bf06e](https://github.com/LerianStudio/midaz/commit/44bf06ea9cd394d2d57cc89dff52dabec7168c59)) +* **mpostgres:** Add file system migration source :sparkles: ([a776433](https://github.com/LerianStudio/midaz/commit/a7764332079bf00ee8cc504e8978b50181d4d0ec)) +* **organization:** Add find functionality for organization ([96049ef](https://github.com/LerianStudio/midaz/commit/96049ef44841458252f618fff1a93e49d9c88984)) +* add generate and create mocks :sparkles: ([1d8ffa0](https://github.com/LerianStudio/midaz/commit/1d8ffa08bf4535eff60deba3204b6d2ffb88039d)) +* **NoSQL:** Add Get all Organizations and add your own Metadata ([33804e0](https://github.com/LerianStudio/midaz/commit/33804e020b5c3e6f31b20d023c0867c0619cfb40)) +* **NoSQL:** Add Get all Organizations by Metadata ([4acb1fb](https://github.com/LerianStudio/midaz/commit/4acb1fb875656b3b0a7578903772d4d9198db36d)) +* **Organization:** Add Get All ([2bf231a](https://github.com/LerianStudio/midaz/commit/2bf231ae4a6e623ec39fc857e47c8a28b1be3878)) +* **NoSQL:** Add Get metadata with id organization ([afb4bbd](https://github.com/LerianStudio/midaz/commit/afb4bbdc3059a4c1bfe88f8ccd677f8bfe08ba47)) +* **auth:** add initial auth configuration for ory stack usage ([1c0c621](https://github.com/LerianStudio/midaz/commit/1c0c621a7b0e29992e1cb0183674ae69ecc9e52c)) +* add instrument testes ([c4a9cc0](https://github.com/LerianStudio/midaz/commit/c4a9cc0773a8920e569b92182b6bf758d7787083)) +* **NoSQL:** Add libs mongodb ([28fbfaf](https://github.com/LerianStudio/midaz/commit/28fbfafcad304436afb32d986e477468bf38c4f3)) +* **create-division:** Add metadata creation to CreateDivision ([67fc945](https://github.com/LerianStudio/midaz/commit/67fc945a575d1452c3f183e2bef50f49f7999ba0)) +* add metadata testes ([e2cc055](https://github.com/LerianStudio/midaz/commit/e2cc05569bdd40285e9349f0cd41d5dbbc37673e)) +* **NoSQL:** Add mongodb on docker-compose ([88b81ab](https://github.com/LerianStudio/midaz/commit/88b81abc8654a30789e3b191dbf48ffa2a7f30eb)) +* **ledger:** Add new ledger API components ([c657d3d](https://github.com/LerianStudio/midaz/commit/c657d3da7bb2ac8d66a8a47c8d4422519026df5e)) +* add portfolio testes ([78e2727](https://github.com/LerianStudio/midaz/commit/78e2727e3fc9cf24100ec9f0f1d8881f33483a61)) +* **components:** Add security scan and improve http client :sparkles: ([78d9736](https://github.com/LerianStudio/midaz/commit/78d973655e8e739b8a8f5c7eeb04f285f428e587)) +* **postgres:** add source database name to connection struct ([39b22d2](https://github.com/LerianStudio/midaz/commit/39b22d2b62c66a105af0e7c7820f293324987122)) +* **organization:** Add status field to Organization model ([72283b3](https://github.com/LerianStudio/midaz/commit/72283b3298a2aab2ab506889d7d2fbab6a9d0aa4)) +* **Organization:** Add Update ([0b01ac0](https://github.com/LerianStudio/midaz/commit/0b01ac0bd597d44db27678ff33e248f4de6d76eb)) +* **Product:** Add ([6789c74](https://github.com/LerianStudio/midaz/commit/6789c7452b4171aaa3a1ae468beeb30136adc1e4)) +* **NoSQL:** Adjusts and add redis on docker-compose.yaml only ([5122bf3](https://github.com/LerianStudio/midaz/commit/5122bf31d9f41eef14c822445ba35c135c3a6b26)) +* **NoSQL:** Config geral ([2a7f3ef](https://github.com/LerianStudio/midaz/commit/2a7f3ef29f43ba7eabc282a281ca797a734b9270)) +* **Divisions:** Create divisions and some adjusts ([8bfe439](https://github.com/LerianStudio/midaz/commit/8bfe439d7f0bbec6918b4ab015376531a44a8d9c)) +* **Ledger:** Create Ledger ([afae31b](https://github.com/LerianStudio/midaz/commit/afae31bd42c58158bbe1f45d468a1550489b0ee7)) +* **Account:** CREATE ([2d91261](https://github.com/LerianStudio/midaz/commit/2d912611dcfc303574fd6e30c67ddc1d875a77ec)) +* **chiuld-account:** create ([4ebfed1](https://github.com/LerianStudio/midaz/commit/4ebfed11415771412fb57e51c368b7a7c41c5c30)) +* **Portfolio:** Create ([87b6840](https://github.com/LerianStudio/midaz/commit/87b684088a13648aaddb2f5b8f4084ec9c0daf4f)) +* **instrument:** crud ([2b29335](https://github.com/LerianStudio/midaz/commit/2b2933531406810517d8ee95d7cc17e573b326de)) +* **Division:** Delete Division ([50d87e7](https://github.com/LerianStudio/midaz/commit/50d87e7be5c621f28c418ea986fa6182a2013c89)) +* **Ledger:** Delete Ledger ([b1900e4](https://github.com/LerianStudio/midaz/commit/b1900e4e3147ae68aecd79fcf964b8bc139d4350)) +* **account:** delete ([bf766c7](https://github.com/LerianStudio/midaz/commit/bf766c744254416280dcf7597c0540ca44cb9bb5)) +* **child-account:** delete ([ddbfdf9](https://github.com/LerianStudio/midaz/commit/ddbfdf9f99eb36454b761d7dbc8f0d49e064c9ba)) +* **Portfolio:** Delete ([86ab6c4](https://github.com/LerianStudio/midaz/commit/86ab6c439fe4b24d9ee3dc1df2b0b76306eab76f)) +* **Product:** Delete ([6bd1519](https://github.com/LerianStudio/midaz/commit/6bd1519c994eb09560b38c538d03fc9e5a4cae07)) +* division add tests :sparkles: ([8708d36](https://github.com/LerianStudio/midaz/commit/8708d364581590ad193f24f959b48f961a750679)) +* **ledger:** Enable ledger repository and handler ([1139a92](https://github.com/LerianStudio/midaz/commit/1139a92001fe2ff908299b1e5d6649851216ee45)) +* **ledger:** Enable ledger use case operations ([ff70c70](https://github.com/LerianStudio/midaz/commit/ff70c70ff39604a46c67b0bd75e3d45ac2cc87a2)) +* **Division:** Get all divisions and get all divisions by Metadata ([4888367](https://github.com/LerianStudio/midaz/commit/48883671cd81e1b3d4d1ed37a8a149ea0935cd95)) +* **Ledger:** Get all Ledgers and get all Ledgers by Metadata ([ec4db79](https://github.com/LerianStudio/midaz/commit/ec4db79ccd003dffa2b0b9cf7cda645c477655be)) +* **chiuld-account:** get all ([3092638](https://github.com/LerianStudio/midaz/commit/30926384a9e8a07799a5c879f249c49c1d3e46f7)) +* **Portfolio:** Get All ([2ed3ed1](https://github.com/LerianStudio/midaz/commit/2ed3ed14616479d1272bc5683946fe67dfd34d5b)) +* **Product:** Get All ([66503ab](https://github.com/LerianStudio/midaz/commit/66503ab8569c58712b62e5d5a0a277cbeb1092dd)) +* **Product:** Get All ([f928ad5](https://github.com/LerianStudio/midaz/commit/f928ad51b5aefef21f07d9942158a1ce95f1cdf5)) +* **Account:** GET BY ID ([4dd8ba6](https://github.com/LerianStudio/midaz/commit/4dd8ba61bd5722ca3a3b00345e74b0bebe7623e0)) +* **child-account:** get by id ([d217ded](https://github.com/LerianStudio/midaz/commit/d217ded8e8bc3c2be20c020f9543d72b1b96f032)) +* **chiuld-account:** get by id ([2933571](https://github.com/LerianStudio/midaz/commit/29335717a9bab4226b7060fb596e462939783ab6)) +* **Portfolio:** Get By ID ([25e6e27](https://github.com/LerianStudio/midaz/commit/25e6e27ab53a007fd7b0ba23ef043d9b58782a90)) +* **Product:** Get By Id ([7a382c6](https://github.com/LerianStudio/midaz/commit/7a382c6bcba88bf988d1e169ae385d0974be8fef)) +* **Division:** Get division by id organization and id division ([574b226](https://github.com/LerianStudio/midaz/commit/574b2260ed447710ae1e908ebca65cc932debf64)) +* **Ledger:** Get Ledger by ID ([c37f64f](https://github.com/LerianStudio/midaz/commit/c37f64fa3fecc36fa020795134a0ced0500adc43)) +* **mdz:** go.mod ([dd0bcf9](https://github.com/LerianStudio/midaz/commit/dd0bcf9bb05e5ff84c4466cf232ede9f133b01ca)) +* **ledger:** Implement organization model and repo ([6cefe6c](https://github.com/LerianStudio/midaz/commit/6cefe6c30df0e8107af391ac46cd157e59f08227)) +* ledger add tests :sparkles: ([17e9b1d](https://github.com/LerianStudio/midaz/commit/17e9b1d4f3da1d1b3591d4583cc7e7eb75900a18)) +* **mdz:** login, ui and version commands. auth & ledger boilerplate ([5127802](https://github.com/LerianStudio/midaz/commit/512780223723d0d498d2bf4c13bcac97749927c4)) +* **Portfolio:** Metadata and productId ([514e978](https://github.com/LerianStudio/midaz/commit/514e97827f53c2307b327da748425a0b2c802c1b)) +* **organization:** organization add tests :sparkles: ([f59154d](https://github.com/LerianStudio/midaz/commit/f59154da934e35d22e04b0ed232ec9677db0316f)) +* **instrument:** postman ([959eec7](https://github.com/LerianStudio/midaz/commit/959eec7fa281d90e95658c5d96150b7c7b8eb6a4)) +* product add tests :sparkles: ([55b517a](https://github.com/LerianStudio/midaz/commit/55b517aa376b69db8544f60f6df694bc5c37fe40)) +* **command:** test create organization ([df5ec02](https://github.com/LerianStudio/midaz/commit/df5ec02d6bd3b4d9dc2c5cf40d27c47302dfecd5)) +* **Division:** Update Division ([7e61a02](https://github.com/LerianStudio/midaz/commit/7e61a02e46e0d4a74c451116a9c081a215a467b5)) +* **Ledger:** Update Ledger ([c92f8bd](https://github.com/LerianStudio/midaz/commit/c92f8bd08bebdc9e6f4c40a329367d4fd8e3876a)) +* **Account:** UPDATE ([0ee68e0](https://github.com/LerianStudio/midaz/commit/0ee68e06a8ad8551689c6d921a633a9d7aa343fd)) +* **chiuld-account:** update ([186ad62](https://github.com/LerianStudio/midaz/commit/186ad621f893bc5b31b73cea73caf32bffd16a2f)) +* **Portfolio:** Update ([a74e8f1](https://github.com/LerianStudio/midaz/commit/a74e8f13d4f350561860cc9b7fa9a0de4b17790f)) +* **Product:** Update ([1da5729](https://github.com/LerianStudio/midaz/commit/1da5729266cb78236ebb624a1e60f0df904b7703)) + + +### Bug Fixes + +* add parameter to fetch and change token ([132b6aa](https://github.com/LerianStudio/midaz/commit/132b6aa12eb4666079480fabb330a907853cc9ac)) +* **auth:** adjust compose and .env usage based on project goals and standards ([b3536ba](https://github.com/LerianStudio/midaz/commit/b3536ba2c2c893a803039307778b2defcaad829a)) +* adjust database configuration ([5bc8558](https://github.com/LerianStudio/midaz/commit/5bc85587de0164a8b8e935d61a34b4720bf50f4f)) +* **auth:** adjust directories usage based on project goals and standards ([37b10d4](https://github.com/LerianStudio/midaz/commit/37b10d4ab120f9e8a6669bc3dcf7a9f90f823c8d)) +* **division:** adjust method name :bug: ([6c4a154](https://github.com/LerianStudio/midaz/commit/6c4a154067f6cc2595be278312c99eec5c5f5f73)) +* change token ([d29805a](https://github.com/LerianStudio/midaz/commit/d29805a66ef563f25afcb989368466302889a925)) +* **ledger:** Correct typo in Dockerfile build command :bug: ([7ffecf2](https://github.com/LerianStudio/midaz/commit/7ffecf20188ffab444d498b12f27f588ff23a9b3)) +* create test and some lints :bug: ([82ef4b8](https://github.com/LerianStudio/midaz/commit/82ef4b8c4a841a80c4fa84ee483da1887e6c749d)) +* debug file :bug: ([54047b0](https://github.com/LerianStudio/midaz/commit/54047b0af88c5ea1995b213141c08b3579695842)) +* debug semantic-release ([399322c](https://github.com/LerianStudio/midaz/commit/399322c78dd7c9206c882f643b232d759d27af72)) +* disable job and fix syntax :bug: ([958d002](https://github.com/LerianStudio/midaz/commit/958d002053dc246cca79915d16e454ea1be7dcd4)) +* fix :bug: ([70d7fa3](https://github.com/LerianStudio/midaz/commit/70d7fa3d85dd922cb1c2f1eca218c6d2cbecae80)) +* **ledger:** fix and refactor some adjusts :bug: ([2ca0d63](https://github.com/LerianStudio/midaz/commit/2ca0d63836bb43e442715c74811fa49e0b09b72d)) +* fix args :bug: ([e7f73d6](https://github.com/LerianStudio/midaz/commit/e7f73d6896452c33bf9ee885b652b6452a00c4ae)) +* fix extra plugins for semantic-release :bug: ([e98b558](https://github.com/LerianStudio/midaz/commit/e98b558c6d451dc43bcdc6c0204e9aae03b44ade)) +* fix permission :bug: ([762101a](https://github.com/LerianStudio/midaz/commit/762101a74300d886c25269fcc2a1d709d2c0d662)) +* fix script output :bug: ([97a59b4](https://github.com/LerianStudio/midaz/commit/97a59b471547d18e8fa4e1fabb410b12aff9b2bf)) +* fix semantic-release behavior :bug: ([1883b39](https://github.com/LerianStudio/midaz/commit/1883b391406e7d50dfcc7720a9ba1ca6d2b0b6a8)) +* fix syntax :bug: ([365bbd8](https://github.com/LerianStudio/midaz/commit/365bbd84e94ab0eb2459e2bd4b653d0a7d60dfdd)) +* identation ([9d3ec69](https://github.com/LerianStudio/midaz/commit/9d3ec694ae0e7e6646af85b29d15125afbd63769)) +* move replication file to folder migration :bug: ([53db96e](https://github.com/LerianStudio/midaz/commit/53db96e6a24794bc7f897d641012cceaf19415ea)) +* move replication file to folder setup :bug: ([348a8da](https://github.com/LerianStudio/midaz/commit/348a8da57a70f6cb73145db2bc6afd2363c3d284)) +* PR suggestions of @Ralphbaer implemented :bug: ([8cbc696](https://github.com/LerianStudio/midaz/commit/8cbc69628a2b848e632b31ab839d6ddf047064b0)) +* remove auto-migration from DB connection process ([7ab1501](https://github.com/LerianStudio/midaz/commit/7ab1501492dc9909cb8eb2454c8b0ec24d413baa)) +* remove rule to exclude path :bug: ([48b2cf8](https://github.com/LerianStudio/midaz/commit/48b2cf8607d6b49e8cdd7c4f001448a5dfdaa666)) +* remove wrong rule :bug: ([b732416](https://github.com/LerianStudio/midaz/commit/b732416c1e69094c1fa2526159f39e0eaee0cc1f)) +* semantic-release ([138e1cc](https://github.com/LerianStudio/midaz/commit/138e1cca68d5b250222001645e608aef8b2c7b77)) +* Update merge-back.yml ([5c90141](https://github.com/LerianStudio/midaz/commit/5c901412f9daff57e16013e38a8fbc1ac93222c2)) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..0c471710 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +github@leriand.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..017dbd7f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,124 @@ +# Contributing to Midaz + +At Midaz, we believe in the power of collaboration and the incredible impact that each individual can make. Whether you're fixing a bug, proposing a new feature, improving documentation, or offering your unique perspective, your contributions are highly valued and play a crucial role in the evolution of Midaz. + +#### Why Contribute? + +* **Impact**: Your work will directly impact and improve a project used by organizations around the world, making their operations smoother and more efficient. +* **Learn and Grow**: Contributing to Midaz offers you a unique opportunity to learn from a community of talented developers, enhancing your skills and knowledge in architecture design, CQRS, Ports & Adapters, and more. +* **Community**: Join a welcoming and supportive community of developers who share your passion for creating high-quality, open-source software. + +#### How Can You Contribute? + +* **Code Contributions**: From minor fixes to major features, your code contributions are always welcome. Our architecture and minimal dependencies make it easy for you to understand and enhance Midaz. +* **Documentation**: Help us improve our documentation to make Midaz more accessible and understandable for everyone. +* **Feedback and Ideas**: Share your insights, suggestions, and innovative ideas to help us shape the future of Midaz. +* **Testing**: Assist in testing new releases or features, providing valuable feedback to ensure stability and usability. + +## Our Workflow + +Our contribution process is straightforward: + +``` +[Issue] > Pull request > Commit Signing > Code Review > Merge +``` + +For most changes, we ask that you first create an issue to discuss your proposed changes. This helps us to track the conversation and feedback. However, for minor edits like typos, you can directly submit a pull request. + +## Commit Message Guidelines + +We adopt the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) format to ensure our commit history is readable and easy to follow. This format is part of a broader set of guidelines designed to facilitate automatic versioning and changelog generation: + +``` +<type>[optional scope]: <description> + +[optional body] + +[optional footer(s)] +``` + +--- + +<br /> +The commit contains the following structural elements, to communicate intent to the +consumers of your library: + +<br /> + +1. **fix:** a commit of the _type_ `fix` patches a bug in your codebase (this correlates with [`PATCH`](http://semver.org/#summary) in Semantic Versioning). +2. **feat:** a commit of the _type_ `feat` introduces a new feature to the codebase (this correlates with [`MINOR`](http://semver.org/#summary) in Semantic Versioning). +3. **BREAKING CHANGE:** a commit that has a footer `BREAKING CHANGE:`, or appends a `!` after the type/scope, introduces a breaking API change (correlating with [`MAJOR`](http://semver.org/#summary) in Semantic Versioning). + A BREAKING CHANGE can be part of commits of any _type_. +4. _types_ other than `fix:` and `feat:` are allowed, for example [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) (based on the [the Angular convention](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines)) recommends `build:`, `chore:`, + `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others. +5. _footers_ other than `BREAKING CHANGE: <description>` may be provided and follow a convention similar to + [git trailer format](https://git-scm.com/docs/git-interpret-trailers). + +Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE). +`<br /><br />` +A scope may be provided to a commit's type, to provide additional contextual information and is contained within parenthesis, e.g., `feat(parser): add ability to parse arrays`. + +## How to Submit a Pull Request + +#### Commit Signing Requirement + +For the integrity and verification of contributions, we require that all commits be signed, adhering to the [developercertificate.org](https://developercertificate.org/). This certifies that you have the rights to submit the work under our project's license and that you agree to the DCO statement: + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +Then you just add a line to every git commit message: + +Signed-off-by: Joe Smith <joe.smith@example.com> +Use your real name (sorry, no pseudonyms or anonymous contributions.) + +If you set your user.name and user.email git configs, you can sign your commit automatically with git commit -s. + +By following these guidelines, you help ensure Midaz is a welcoming, efficient, and valuable project for everyone. Thank you for your contributions and for being a part of our community! + +Before sending us a pull request, please ensure that, + +- Fork the midaz repo on GitHub, clone it on your machine. +- Create a feature or fix branch with your changes. +- You are working against the latest source on the `main` branch. +- Modify the source; please focus only on the specific change you are contributing. +- Ensure local tests pass. +- Commit to your fork using clear commit messages. +- Send us a pull request, answering any default questions in the pull request interface. +- Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation +- Once you've pushed your commits to GitHub, make sure that your branch can be auto-merged (there are no merge conflicts). If not, on your computer, merge main into your branch, resolve any merge conflicts, make sure everything still runs correctly and passes all the tests, and then push up those changes. +- Once the change has been approved and merged, we will inform you in a comment. \ No newline at end of file diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 00000000..c5119a5c --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,46 @@ +# Midaz Project Governance + +Welcome to the Midaz Project Governance documentation. This document outlines our governance model, roles, responsibilities, and the procedures we follow to ensure a transparent and fair environment for every contributor. + +## 1. Governance Model and Decision-Making Process + +Midaz operates under a meritocratic governance model. Decisions are made based on discussion and consensus among community members. When consensus cannot be reached, decisions may be escalated to the Steering Committee, which will consider all viewpoints and make a decision in the best interest of the project. + +### Decision-Making Process: + +* **Discussion** : Proposals and ideas are discussed openly in project forums. Every community member is encouraged to voice their opinions and provide feedback. +* **Consensus** : Efforts are made to reach an agreement that is acceptable to all active participants. +* **Vote** : If consensus cannot be achieved, a formal vote is held among Maintainers or the Steering Committee to make a final decision. + +## 2. Roles and Responsibilities + +### Maintainers: + +* **Responsibilities** : Reviewing code submissions, managing build processes, ensuring code quality and security, and mentoring new contributors. +* **Authority** : Can merge code changes and accept or reject contributions. + +### Contributors: + +* **Responsibilities** : Submitting bug reports, feature requests, and participating in the community discussions. +* **Authority** : Contributors do not have direct code merge authority but can suggest changes through pull requests. + +### Steering Committee: + +* **Responsibilities** : Overseeing the projectโ€™s strategic direction, resolving escalated issues, and making final decisions when consensus is not possible. +* **Authority** : Can override decisions, change governance rules, and resolve high-level conflicts. + +## 3. Voting Procedures and Conflict Resolution + +### Voting Procedures: + +* **Eligibility** : All Maintainers are eligible to vote. For significant changes, a wider group may be included. +* **Method** : Voting typically takes place online via a transparent, accessible platform. Majority vote decides outcomes. + +### Conflict Resolution: + +* **Process** : In the event of a conflict, parties are encouraged to resolve disputes through direct communication and mediation by peers. +* **Escalation** : Unresolved conflicts may be escalated to the Steering Committee for final resolution. + +## 4. Code of Conduct and Enforcement + +Our [Midaz - Code of Conduct](https://github.com/LerianStudio/midaz-private/blob/main/CODE_OF_CONDUCT.md) is central to fostering an inclusive and productive environment. All community members are expected to follow the principles of respect, fairness, and openness. diff --git a/LICENSE b/LICENSE index 261eeb9e..fdc5bdb0 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..a38e2bfb --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +AUTH_DIR := ./components/auth +LEDGER_DIR := ./components/ledger + +.PHONY: auth ledger + +build: + ./make.sh "build" + +help: + @echo "Management commands" + @echo "" + @echo "Usage:" + @echo " ## Root Commands" + @echo " make build Build all project services." + @echo " make test Run tests on all projects." + @echo " make clean Clean the directory tree of produced artifacts." + @echo " make lint Run static code analysis (lint)." + @echo " make format Run code formatter." + @echo " make checkEnvs Check if github hooks are instaled and secret env on files are not exposed." + @echo " make gen Generates all project code to connect its components using Wire." + @echo "" + @echo " ## Utility Commands" + @echo " make setup-git-hooks Setup git hooks." + @echo "" + +test: + go test -v ./... ./... + +cover: + go test -cover ./... + +clean: + ./make.sh "clean" + +lint: + ./make.sh "lint" + +format: + ./make.sh "format" + +check-logs: + ./make.sh "checkLogs" + +check-tests: + ./make.sh "checkTests" + +setup-git-hooks: + ./make.sh "setupGitHooks" + +goreleaser: + goreleaser release --snapshot --skip-publish --rm-dist + +tidy: + go mod tidy + +sec: + gosec ./... + +auth: + $(MAKE) -C $(AUTH_DIR) restart + +gen_auth: + $(MAKE) -C $(AUTH_DIR) gen + +ledger: + $(MAKE) -C $(LEDGER_DIR) restart + +gen: + $(MAKE) -C $(LEDGER_DIR) gen \ No newline at end of file diff --git a/README.md b/README.md index 65d6e989..f5943e6e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ -# midaz -Midaz, an open-source, cloud-native, immutable, multi-currency, multi-asset, Core Ledger Application for storing and tracking transactions. +![banner](image/README/midaz-banner.png) + +<div align="center"> + + [![Latest Release](https://img.shields.io/github/v/release/LerianStudio/midaz?include_prereleases)](https://github.com/LerianStudio/midaz/releases) + [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/LerianStudio/midaz/blob/main/LICENSE) + [![Go Report](https://goreportcard.com/badge/github.com/lerianstudio/midaz)](https://goreportcard.com/report/github.com/lerianstudio/midaz) + [![Discord](https://img.shields.io/badge/Discord-Lerian%20Studio-%237289da.svg?logo=discord)](https://discord.gg/DnhqKwkGv3) + +</div> + +**Midaz** is an open source ledger designed to offer multi-asset and multi-currency transaction capabilities within a single, natively immutable and fully auditable platform. + +It supports complex "n:n" transactions, with multiple senders and receivers within the same registry, and facilitates direct exchanges between assets and/or currencies in one go. The solution is built on a double-entry chart-of-accounts engine, enhancing the accounting process and ensuring seamless integration with financial reports. + +By adhering to these guiding principles, Midaz offers a robust ecosystem that is not only powerful and efficient but also aligned with the best practices in software architecture. Our commitment to these values ensures that Midaz remains a cutting-edge solution for developers seeking to build scalable, resilient, and adaptable applications. + +## Getting Started + +To begin using Midaz, please follow the step-by-step instructions provided in our [Getting Started](https://midaz.gitbook.io/midaz/overview/getting-started) guide. + +## Learn More About Midaz + +For a comprehensive understanding of all Midaz features and capabilities, refer to our [Complete Documentation](https://midaz.gitbook.io/midaz). + +## License + +Midaz is released under the terms of the Apache License 2.0. See [COPYING](https://github.com/LerianStudio/midaz/blob/main/LICENSE) for more information or see [https://opensource.org/license/apache-2-0](https://opensource.org/license/apache-2-0). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..d0277c2b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,46 @@ +# Security & Compliance + +Welcome to the Midaz Security Practices documentation. This document provides an overview of our security measures, policies for responsible disclosure, and recommendations for secure usage. + +## 1. Overview of Security Practices + +Midaz utilizes the Ory Open-source Stack, which is designed with robust security measures to protect both the software and its users. Our security infrastructure includes multiple components: + +* **Identity Management and Authentication** : An identity and user management server that handles user registration, login, and user profile management using modern identity. +* **Authorization and Access Control** : OAuth2 provider, which handles authentication and authorization. +* **Token Issuance and Management** : Implementation of attribute-based access control (ABAC) and access control policies. +* **Identity and Access Proxy** : Identity and Access Proxy (IAP) that validates incoming requests. + +These components ensure that Midaz maintains high security standards and protects against unauthorized access and other potential security threats. + +If you have more interest about the Ory Open-source Stack, see more in [Ory Website](https://www.ory.sh/). + +## 2. Recommendations for Secure Usage and Configuration + +To ensure the security of your deployments, we recommend the following best practices: + +* **Avoid Hardcoding Sensitive Information** : Hardcoding passwords, API keys, or other sensitive data directly in the source code can lead to security vulnerabilities if the codebase is exposed or accessed by unauthorized individuals. Use environment variables or secure secrets management tools like HashiCorp Vault to manage sensitive configurations securely. +* **Use Secrets Management** : Store sensitive configuration such as passwords and API keys using secrets management tools. This prevents sensitive data from being exposed in your configuration files or source code. +* **Regular Updates** : Keep your Midaz installation and its dependencies up-to-date to protect against vulnerabilities. +* **Secure Configuration** : Follow our configuration guidelines to set up Midaz securely, available in the official documentation. + +## 3. Responsible Disclosure Policy + +For transparency, any known securities improvements join in our **[Github Discussions](https://github.com/LerianStudio/midaz/discussions)**. This allows our community to follow the progress and updates related to security patches and enhancements. + +If you discover a security vulnerability within Midaz, please report it using our responsible disclosure policy. Do not disclose the issue publicly until we have had the opportunity to address it. + +* **Initial Contact** : Researcher submits vulnerability via secure email. +* **Acknowledgment** : Your team acknowledges receipt within 24 hours. +* **Verification** : Your security team verifies the vulnerability. +* **Impact Assessment** : Determine the severity and potential impact of the vulnerability. +* **Resolution** : Develop and deploy a fix. +* **Notification** : Inform the researcher of the resolution. +* **Public Disclosure** : Coordinate with the researcher to publicly disclose the vulnerability responsibly. + +### Contact Information: + +* **Security Email** : [security@lerian.studio](security@lerian.studio) +* **PGP Key** : Available for secure communications on our security page - We strongly recommend [Mailvelope](https://mailvelope.com/en) tool to encrypt sender and receiver emails. + +We aim to review all reports within 24 hours and will work with you to understand and resolve the issue quickly and confidentially. diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 00000000..9bd0bd76 --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,118 @@ +# Project Structure Overview + +Welcome to the comprehensive guide on the structure of our project, which is designed with a focus on scalability, maintainability, and clear separation of concerns in line with the Command Query Responsibility Segregation (CQRS) pattern. This architecture not only enhances our project's efficiency and performance but also ensures that our codebase is organized in a way that allows developers to navigate and contribute effectively. + +#### Directory Layout + +The project is structured into several key directories, each serving specific roles: + +``` +โ”œโ”€โ”€ common +โ”‚ย ย  โ”œโ”€โ”€ console +โ”‚ย ย  โ”œโ”€โ”€ mlog +โ”‚ย ย  โ”œโ”€โ”€ mmongo +โ”‚ย ย  โ”œโ”€โ”€ mpointers +โ”‚ย ย  โ”œโ”€โ”€ mpostgres +โ”‚ย ย  โ”œโ”€โ”€ mzap +โ”‚ย ย  โ”œโ”€โ”€ net +โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ http +โ”‚ย ย  โ””โ”€โ”€ shell +โ”œโ”€โ”€ components +โ”‚ย ย  โ”œโ”€โ”€ auth +โ”‚ย ย  โ”œโ”€โ”€ ledger +โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ api +โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ internal +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ adapters +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ database +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ mongodb +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ postgres +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ app +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ command +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ query +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ domain +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ metadata +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ onboarding +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ledger +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ organization +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ portfolio +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ account +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ instrument +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ portfolio +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ product +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ gen +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ mock +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ account +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ instrument +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ledger +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ metadata +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ organization +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ portfolio +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ product +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ports +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ http +โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ service +โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ migrations +โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ setup +โ”‚ย ย  โ””โ”€โ”€ mdz +โ”‚ย ย  โ”œโ”€โ”€ cmd +โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ login +โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ ui +โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ version +โ”‚ย ย  โ””โ”€โ”€ pkg +โ”œโ”€โ”€ config +โ”‚ย ย  โ”œโ”€โ”€ auth +โ”‚ย ย  โ””โ”€โ”€ identity-schemas +โ”œโ”€โ”€ image +โ”‚ย ย  โ””โ”€โ”€ README +โ””โ”€โ”€ postman + +``` + +#### Common Utilities (`./common`) + +* `console`: Description of the console utilities and their usage. +* `mlog`: Overview of the logging framework and configuration details. +* `mmongo`, `mpostgres`: Database utilities, including setup and configuration. +* `mpointers`: Explanation of any custom pointer utilities or enhancements used in the project. +* `mzap`: Details on the structured logger adapted for high-performance scenarios. +* `net/http`: Information on HTTP helpers and network communication utilities. +* `shell`: Guide on shell utilities, including scripting and automation tools. + +#### Components (`./components`) + +##### Ledger (`./components/ledger`) + +###### API (`./ledger/api`) + +* **Endpoints** : List and describe all API endpoints, including parameters, request/response formats, and error codes. + +###### Internal (`./ledger/internal`) + +* **Adapters** (`./adapters`): + * **Database** : Connection and operation guides for MongoDB and PostgreSQL. +* **Application Logic** (`./app`): + * **Command** : Documentation of command handlers, including how commands are processed. + * **Query** : Details on query handlers, how queries are executed, and their return structures. +* **Domain** (`./domain`): + * Description of domain models such as Onboarding, Portfolio, Transaction, etc., and their relationships. +* **Services** (`./service`): + * Detailed information on business logic services, their roles, and interactions in the application. + +##### MDZ (`./components/mdz`) + +* **Command Line Tools** (`./cmd`): Guides on how to use various command-line tools included in the MDZ component. +* **Packages** (`./pkg`): Information on additional packages provided within the MDZ component. + +### Configuration (`./config`) + +* **Identity Schemas** (`./identity-schemas`): Guide on setting up and modifying identity schemas. + +### Miscellaneous + +#### Images (`./image`) + +* **README** : Purpose of images stored and how to use them in the project. + +#### Postman Collections (`./postman`) + +* **Usage** : How to import and use the provided Postman collections for API testing. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 00000000..d52d8a8d --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,16 @@ +# Support Hub: Connect, Contribute, Collaborate + +Midaz is a mission-critical service, and we are committed to maintaining it up-to-date and bug-free. Our mission, along with the community, is to open the doors to receive improvements and address bugs swiftly. If you have any suggestions, such as improvements, ideas, questions, or anything else that can enhance Midaz, please contact us using the channels listed below. + +In our repository, there is a section for discussing Midaz using the Discussions tab. Feel free to share your opinions, ideas, and report bugs. + +### Channels + +1. **Github:** Enter our public repository and join the [Discussions](https://github.com/LerianStudio/midaz-private/discussions). +2. **Discord**: Enter our [Discord](https://discord.gg/DnhqKwkGv3). + +**Pick an Issue, Share Ideas and Questions**: Check out the open issues, ideas and questions labeled for beginners or propose your own project. + +Don't be afraid to take the first step. Whether it's your first time contributing to an open-source project or you're an experienced developer, your contributions are welcome here. Together, we can make Midaz better for everyone. + +Your ideas, your passion, and your contributions can make a significant difference. Let's build something amazing together. diff --git a/common/app.go b/common/app.go new file mode 100644 index 00000000..97e56a88 --- /dev/null +++ b/common/app.go @@ -0,0 +1,97 @@ +package common + +import ( + "fmt" + "sync" + + "github.com/LerianStudio/midaz/common/console" + "github.com/LerianStudio/midaz/common/mlog" +) + +// App represents an application that will run as a deployable component. +// It's an entrypoint at main.go. +type App interface { + Run(launcher *Launcher) error +} + +// LauncherOption defines a function option for Launcher. +type LauncherOption func(l *Launcher) + +// WithLogger adds a mlog.Logger component to launcher. +func WithLogger(logger mlog.Logger) LauncherOption { + return func(l *Launcher) { + l.Logger = logger + } +} + +// RunApp start all process registered before to the launcher. +func RunApp(name string, app App) LauncherOption { + return func(l *Launcher) { + l.Add(name, app) + } +} + +// Launcher manages apps. +type Launcher struct { + Logger mlog.Logger + apps map[string]App + wg *sync.WaitGroup + Verbose bool +} + +// Add runs an application in a goroutine. +func (l *Launcher) Add(appName string, a App) *Launcher { + l.apps[appName] = a + return l +} + +// Run run every application registered before with Run method. +func (l *Launcher) Run() { + count := len(l.apps) + l.wg.Add(count) + + logf := func(format string, args ...any) { + if l.Logger != nil { + l.Logger.Infof(format, args...) + } + } + + fmt.Println(console.Title("Launcher Run")) + + logf(fmt.Sprintf("Starting %d app(s)\n", count)) + + for name, app := range l.apps { + go func(name string, app App) { + logf("--") + logf(fmt.Sprintf("Launcher: App \u001b[33m(%s)\u001b[0m starting\n", name)) + + if err := app.Run(l); err != nil { + logf(fmt.Sprintf("Launcher: App (%s) error:", name)) + logf(fmt.Sprintln("\u001b[31m", err, "\u001b[0m")) + } + + l.wg.Done() + + logf(fmt.Sprintf("Launcher: App (%s) finished\n", name)) + }(name, app) + } + + l.wg.Wait() + + logf("Launcher: Terminated") +} + +// NewLauncher create an instance of Launch. +func NewLauncher(opts ...LauncherOption) *Launcher { + l := &Launcher{ + apps: make(map[string]App), + wg: new(sync.WaitGroup), + Verbose: true, + } + + for _, opt := range opts { + opt(l) + } + + return l +} diff --git a/common/console/console.go b/common/console/console.go new file mode 100644 index 00000000..b6d38bf2 --- /dev/null +++ b/common/console/console.go @@ -0,0 +1,31 @@ +package console + +import ( + "fmt" + "strings" +) + +// DefaultLineSize is the line size used in Title function. +const DefaultLineSize = 80 + +// Line returns a single line. Eg: -------. +func Line(size int) string { + return strings.Repeat("-", size) +} + +// DoubleLine returns a doubled line. Eg: ========. +func DoubleLine(size int) string { + return strings.Repeat("=", size) +} + +// Title returns a title with double line. Eg: ====== title ======. +func Title(title string) string { + title = fmt.Sprintf(" %s ", title) + startIndex := (DefaultLineSize / 2) - (len(title) / 2) + delta := len(title) % 2 + + return fmt.Sprintf("%s%s%s", + DoubleLine(startIndex), + title, + DoubleLine((startIndex)+delta)) +} diff --git a/common/errors.go b/common/errors.go new file mode 100644 index 00000000..d18e905e --- /dev/null +++ b/common/errors.go @@ -0,0 +1,158 @@ +package common + +import ( + "fmt" + "strings" +) + +// EntityNotFoundError records an error indicating an entity was not found in any case that caused it. +// You can use it to representing a Database not found, cache not found or any other repository. +type EntityNotFoundError struct { + EntityType string + Title string + Message string + Code string + Err error +} + +// NewEntityNotFoundError creates an instance of EntityNotFoundError. +func NewEntityNotFoundError(entityType string) EntityNotFoundError { + return EntityNotFoundError{ + EntityType: entityType, + Title: "", + Message: "", + Err: nil, + } +} + +// WrapEntityNotFoundError creates an instance of EntityNotFoundError. +func WrapEntityNotFoundError(entityType string, err error) EntityNotFoundError { + return EntityNotFoundError{ + EntityType: entityType, + Title: "", + Message: "", + Err: err, + } +} + +// Error implements the error interface. +func (e EntityNotFoundError) Error() string { + if strings.TrimSpace(e.Message) == "" { + if strings.TrimSpace(e.EntityType) != "" { + return fmt.Sprintf("Entity %s not found", e.EntityType) + } + + if e.Err != nil && strings.TrimSpace(e.Message) == "" { + return e.Err.Error() + } + + return "entity not found" + } + + return e.Message +} + +// Unwrap implements the error interface introduced in Go 1.13 to unwrap the internal error. +func (e EntityNotFoundError) Unwrap() error { + return e.Err +} + +// ValidationError records an error indicating an entity was not found in any case that caused it. +// You can use it to representing a Database not found, cache not found or any other repository. +type ValidationError struct { + EntityType string + Title string + Message string + Code string + Err error +} + +// Error implements the error interface. +func (e ValidationError) Error() string { + if strings.TrimSpace(e.Code) != "" { + return fmt.Sprintf("%s - %s", e.Code, e.Message) + } + + return e.Message +} + +// Unwrap implements the error interface introduced in Go 1.13 to unwrap the internal error. +func (e ValidationError) Unwrap() error { + return e.Err +} + +// EntityConflictError records an error indicating an entity already exists in some repository +// You can use it to representing a Database conflict, cache or any other repository. +type EntityConflictError struct { + EntityType string + Title string + Message string + Code string + Err error +} + +// Error implements the error interface. +func (e EntityConflictError) Error() string { + if e.Err != nil && strings.TrimSpace(e.Message) == "" { + return e.Err.Error() + } + + return e.Message +} + +// Unwrap implements the error interface introduced in Go 1.13 to unwrap the internal error. +func (e EntityConflictError) Unwrap() error { + return e.Err +} + +// UnauthorizedError indicates an operation that couldn't be performant because there's no user authenticated. +type UnauthorizedError struct { + EntityType string + Title string + Message string + Code string + Err error +} + +func (e UnauthorizedError) Error() string { + return e.Message +} + +// ForbiddenError indicates an operation that couldn't be performant because the authenticated user has no sufficient privileges. +type ForbiddenError struct { + EntityType string + Title string + Message string + Code string + Err error +} + +func (e ForbiddenError) Error() string { + return e.Message +} + +// UnprocessableOperationError indicates an operation that couldn't be performant because it's invalid. +type UnprocessableOperationError struct { + EntityType string + Title string + Message string + Code string + Err error +} + +func (e UnprocessableOperationError) Error() string { + return e.Message +} + +// HTTPError indicates a http error raised in a http client. +type HTTPError struct { + EntityType string + Title string + Message string + Code string + Err error +} + +func (e HTTPError) Error() string { + return e.Message +} diff --git a/common/mlog/log.go b/common/mlog/log.go new file mode 100644 index 00000000..b63e5b38 --- /dev/null +++ b/common/mlog/log.go @@ -0,0 +1,222 @@ +package mlog + +import ( + "context" + "fmt" + "log" + "strings" +) + +// Logger is the common interface for log implementation. +type Logger interface { + Info(args ...any) + Infof(format string, args ...any) + Infoln(args ...any) + + Error(args ...any) + Errorf(format string, args ...any) + Errorln(args ...any) + + Warn(args ...any) + Warnf(format string, args ...any) + Warnln(args ...any) + + Debug(args ...any) + Debugf(format string, args ...any) + Debugln(args ...any) + + Fatal(args ...any) + Fatalf(format string, args ...any) + Fatalln(args ...any) + + WithFields(fields ...any) Logger +} + +// LogLevel represents the level of log system (fatal, error, warn, info and debug). +type LogLevel int8 + +// These are the different log levels. You can set the logging level to log. +const ( + // PanicLevel level, highest level of severity. Logs and then calls panic with the + // message passed to Debug, Info, ... + PanicLevel LogLevel = iota + // FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the + // logging level is set to Panic. + FatalLevel + // ErrorLevel level. Logs. Used for errors that should definitely be noted. + // Commonly used for hooks to send errors to an error tracking service. + ErrorLevel + // WarnLevel level. Non-critical entries that deserve eyes. + WarnLevel + // InfoLevel level. General operational entries about what's going on inside the + // application. + InfoLevel + // DebugLevel level. Usually only enabled when debugging. Very verbose logging. + DebugLevel +) + +// ParseLevel takes a string level and returns a LogLevel constant. +func ParseLevel(lvl string) (LogLevel, error) { + switch strings.ToLower(lvl) { + case "fatal": + return FatalLevel, nil + case "error": + return ErrorLevel, nil + case "warn", "warning": + return WarnLevel, nil + case "info": + return InfoLevel, nil + case "debug": + return DebugLevel, nil + } + + var l LogLevel + + return l, fmt.Errorf("not a valid LogLevel: %q", lvl) +} + +// GoLogger is the Go built-in (log) implementation of Logger interface. +type GoLogger struct { + fields []any + Level LogLevel +} + +// IsLevelEnabled checks if the given level is enabled. +func (l *GoLogger) IsLevelEnabled(level LogLevel) bool { + return l.Level >= level +} + +// Info implements Info Logger interface function. +func (l *GoLogger) Info(args ...any) { + if l.IsLevelEnabled(InfoLevel) { + log.Print(args...) + } +} + +// Infof implements Infof Logger interface function. +func (l *GoLogger) Infof(format string, args ...any) { + if l.IsLevelEnabled(InfoLevel) { + log.Printf(format, args...) + } +} + +// Infoln implements Infoln Logger interface function. +func (l *GoLogger) Infoln(args ...any) { + if l.IsLevelEnabled(InfoLevel) { + log.Println(args...) + } +} + +// Error implements Error Logger interface function. +func (l *GoLogger) Error(args ...any) { + if l.IsLevelEnabled(ErrorLevel) { + log.Print(args...) + } +} + +// Errorf implements Errorf Logger interface function. +func (l *GoLogger) Errorf(format string, args ...any) { + if l.IsLevelEnabled(ErrorLevel) { + log.Printf(format, args...) + } +} + +// Errorln implements Errorln Logger interface function. +func (l *GoLogger) Errorln(args ...any) { + if l.IsLevelEnabled(ErrorLevel) { + log.Println(args...) + } +} + +// Warn implements Warn Logger interface function. +func (l *GoLogger) Warn(args ...any) { + if l.IsLevelEnabled(WarnLevel) { + log.Print(args...) + } +} + +// Warnf implements Warnf Logger interface function. +func (l *GoLogger) Warnf(format string, args ...any) { + if l.IsLevelEnabled(WarnLevel) { + log.Printf(format, args...) + } +} + +// Warnln implements Warnln Logger interface function. +func (l *GoLogger) Warnln(args ...any) { + if l.IsLevelEnabled(WarnLevel) { + log.Println(args...) + } +} + +// Debug implements Debug Logger interface function. +func (l *GoLogger) Debug(args ...any) { + if l.IsLevelEnabled(DebugLevel) { + log.Print(args...) + } +} + +// Debugf implements Debugf Logger interface function. +func (l *GoLogger) Debugf(format string, args ...any) { + if l.IsLevelEnabled(DebugLevel) { + log.Printf(format, args...) + } +} + +// Debugln implements Debugln Logger interface function. +func (l *GoLogger) Debugln(args ...any) { + if l.IsLevelEnabled(DebugLevel) { + log.Println(args...) + } +} + +// Fatal implements Fatal Logger interface function. +func (l *GoLogger) Fatal(args ...any) { + if l.IsLevelEnabled(FatalLevel) { + log.Print(args...) + } +} + +// Fatalf implements Fatalf Logger interface function. +func (l *GoLogger) Fatalf(format string, args ...any) { + if l.IsLevelEnabled(FatalLevel) { + log.Printf(format, args...) + } +} + +// Fatalln implements Fatalln Logger interface function. +func (l *GoLogger) Fatalln(args ...any) { + if l.IsLevelEnabled(FatalLevel) { + log.Println(args...) + } +} + +// WithFields implements WithFields Logger interface function +// +//nolint:ireturn +func (l *GoLogger) WithFields(fields ...any) Logger { + return &GoLogger{ + Level: l.Level, + fields: fields, + } +} + +// NewLoggerFromContext extract the Logger from "logger" value inside context +// +//nolint:ireturn +func NewLoggerFromContext(ctx context.Context) Logger { + if logger := ctx.Value(loggerContextKey("logger")); logger != nil { + if l, ok := logger.(Logger); ok { + return l + } + } + + return &NoneLogger{} +} + +type loggerContextKey string + +// ContextWithLogger returns a context within a Logger in "logger" value. +func ContextWithLogger(ctx context.Context, logger Logger) context.Context { + return context.WithValue(ctx, loggerContextKey("logger"), logger) +} diff --git a/common/mlog/nil.go b/common/mlog/nil.go new file mode 100644 index 00000000..0ce0f507 --- /dev/null +++ b/common/mlog/nil.go @@ -0,0 +1,56 @@ +package mlog + +// NoneLogger is a wrapper for log nothing. +type NoneLogger struct{} + +// Info implements Info Logger interface function. +func (l *NoneLogger) Info(args ...any) {} + +// Infof implements Infof Logger interface function. +func (l *NoneLogger) Infof(format string, args ...any) {} + +// Infoln implements Infoln Logger interface function. +func (l *NoneLogger) Infoln(args ...any) {} + +// Error implements Error Logger interface function. +func (l *NoneLogger) Error(args ...any) {} + +// Errorf implements Errorf Logger interface function. +func (l *NoneLogger) Errorf(format string, args ...any) {} + +// Errorln implements Errorln Logger interface function. +func (l *NoneLogger) Errorln(args ...any) {} + +// Warn implements Warn Logger interface function. +func (l *NoneLogger) Warn(args ...any) {} + +// Warnf implements Warnf Logger interface function. +func (l *NoneLogger) Warnf(format string, args ...any) {} + +// Warnln implements Warnln Logger interface function. +func (l *NoneLogger) Warnln(args ...any) {} + +// Debug implements Debug Logger interface function. +func (l *NoneLogger) Debug(args ...any) {} + +// Debugf implements Debugf Logger interface function. +func (l *NoneLogger) Debugf(format string, args ...any) {} + +// Debugln implements Debugln Logger interface function. +func (l *NoneLogger) Debugln(args ...any) {} + +// Fatal implements Fatal Logger interface function. +func (l *NoneLogger) Fatal(args ...any) {} + +// Fatalf implements Fatalf Logger interface function. +func (l *NoneLogger) Fatalf(format string, args ...any) {} + +// Fatalln implements Fatalln Logger interface function. +func (l *NoneLogger) Fatalln(args ...any) {} + +// WithFields implements WithFields Logger interface function +// +//nolint:ireturn +func (l *NoneLogger) WithFields(fields ...any) Logger { + return l +} diff --git a/common/mmongo/mongo.go b/common/mmongo/mongo.go new file mode 100644 index 00000000..bdf75d3e --- /dev/null +++ b/common/mmongo/mongo.go @@ -0,0 +1,60 @@ +package mmongo + +import ( + "context" + "fmt" + "log" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.uber.org/zap" +) + +// MongoConnection is a hub which deal with mongodb connections. +type MongoConnection struct { + ConnectionStringSource string + DB *mongo.Client + Connected bool + Database string +} + +// Connect keeps a singleton connection with postgres. +func (mc *MongoConnection) Connect(ctx context.Context) error { + fmt.Println("Connecting to mongodb...") + + clientOptions := options.Client().ApplyURI(mc.ConnectionStringSource) + + noSQLDB, err := mongo.Connect(ctx, clientOptions) + if err != nil { + log.Fatal("failed to open connect to mongodb", zap.Error(err)) + return nil + } + + if err := noSQLDB.Ping(ctx, nil); err != nil { + log.Printf("MongoDBConnection.Ping %v", + zap.Error(err)) + + return err + } + + fmt.Println("Connected to mongodb โœ… ") + + mc.Connected = true + + mc.DB = noSQLDB + + return nil +} + +// GetDB returns a pointer to the mongodb connection, initializing it if necessary. +func (mc *MongoConnection) GetDB(ctx context.Context) (*mongo.Client, error) { + if mc.DB == nil { + err := mc.Connect(ctx) + if err != nil { + log.Printf("ERRCONECT %s", err) + return nil, err + } + } + + return mc.DB, nil +} diff --git a/common/mpointers/pointers.go b/common/mpointers/pointers.go new file mode 100644 index 00000000..0be07a79 --- /dev/null +++ b/common/mpointers/pointers.go @@ -0,0 +1,28 @@ +package mpointers + +import "time" + +// String just return given s as a pointer +func String(s string) *string { + return &s +} + +// Bool just return given b as a pointer +func Bool(b bool) *bool { + return &b +} + +// Time just return given t as a pointer +func Time(t time.Time) *time.Time { + return &t +} + +// Int64 just return given t as a pointer +func Int64(t int64) *int64 { + return &t +} + +// Int just return given t as a pointer +func Int(t int) *int { + return &t +} diff --git a/common/mpostgres/api.go b/common/mpostgres/api.go new file mode 100644 index 00000000..9c0e6e2a --- /dev/null +++ b/common/mpostgres/api.go @@ -0,0 +1,146 @@ +package mpostgres + +import ( + "context" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/LerianStudio/midaz/common" + "github.com/bxcodec/dbresolver/v2" +) + +type Table struct { + Name string + db dbresolver.DB + Columns []string +} + +// Create inserts a new record into the specified table. +func Create(ctx context.Context, t *Table, data map[string]any) (int64, error) { + if len(data) == 0 { + return 0, errors.New("no data provided for insertion") + } + + keys := make([]string, 0, len(data)) + placeholders := make([]string, 0, len(data)) + values := make([]any, 0, len(data)) + + i := 1 + + for k, v := range data { + if !common.Contains(t.Columns, k) { + return 0, errors.New("invalid column for table") + } + + keys = append(keys, k) + placeholders = append(placeholders, "$"+strconv.Itoa(i)) + values = append(values, v) + i++ + } + + sqlStatement := `INSERT INTO ` + t.Name + ` (` + strings.Join(keys, ", ") + `) VALUES (` + strings.Join(placeholders, ", ") + `) RETURNING id` + + var lastInsertID int64 + + if err := t.db.QueryRowContext(ctx, sqlStatement, values...).Scan(&lastInsertID); err != nil { + return 0, err + } + + return lastInsertID, nil +} + +// Update safely updates records in a specified table +func Update(ctx context.Context, t *Table, id int64, data map[string]any) error { + if len(data) == 0 { + return errors.New("no data provided to update") + } + + setClauses := make([]string, 0, len(data)) + values := make([]any, 0, len(data)+1) + + i := 1 + + for k, v := range data { + if !common.Contains(t.Columns, k) { + return fmt.Errorf("invalid column name: %s", k) + } + + setClauses = append(setClauses, fmt.Sprintf("%s = $%d", k, i)) + values = append(values, v) + i++ + } + + values = append(values, id) + + query := fmt.Sprintf("UPDATE %s SET %s WHERE id = $%d", t.Name, strings.Join(setClauses, ", "), i) + if _, err := t.db.ExecContext(ctx, query, values...); err != nil { + return err + } + + return nil +} + +// Delete removes a record identified by its ID. +func Delete(ctx context.Context, t *Table, id int64) error { + query := fmt.Sprintf("DELETE FROM %s WHERE id = $1", t.Name) + if _, err := t.db.ExecContext(ctx, query, id); err != nil { + return err + } + + return nil +} + +// FindAll fetches records from a PostgreSQL table +func FindAll(ctx context.Context, t *Table, dest any, conditions string, args ...any) error { + query := `SELECT * FROM ` + t.Name + if conditions != "" { + query += " WHERE " + conditions + } + + rows, err := t.db.QueryContext(ctx, query, args...) + if err != nil { + return err + } + defer rows.Close() + + sliceVal := reflect.ValueOf(dest).Elem() + elemType := sliceVal.Type().Elem() + + for rows.Next() { + elem := reflect.New(elemType).Interface() + if err := rows.Scan(elem); err != nil { + return err + } + + sliceVal.Set(reflect.Append(sliceVal, reflect.ValueOf(elem).Elem())) + } + + return rows.Err() +} + +// Count returns the number of rows in the table +func Count(ctx context.Context, t *Table, conditions string, args ...any) (int64, error) { + query := `SELECT COUNT(*) FROM ` + t.Name + if conditions != "" { + query += " WHERE " + conditions + } + + var count int64 + + err := t.db.QueryRowContext(ctx, query, args...).Scan(&count) + if err != nil { + return 0, err + } + + return count, nil +} + +// FindByID finds a row by ID +func FindByID(ctx context.Context, t *Table, id int64, dest any) error { + query := fmt.Sprintf("SELECT * FROM %s WHERE id = $1", t.Name) + + return t.db.QueryRowContext(ctx, query, id).Scan(dest) +} diff --git a/common/mpostgres/builder.go b/common/mpostgres/builder.go new file mode 100644 index 00000000..8069ec83 --- /dev/null +++ b/common/mpostgres/builder.go @@ -0,0 +1,31 @@ +package mpostgres + +// SQLQueryBuilderOption is a kind of interface for build options +type SQLQueryBuilderOption func(b *SQLQueryBuilder) + +// SQLQueryBuilder builds a query for PostgreSQL +type SQLQueryBuilder struct { + Params []any + Where []string + Sorts []string + Table string + Limit string + Offset string +} + +// NewSQLQueryBuilder creates an instance of SQLQueryBuilder +func NewSQLQueryBuilder(table string, opts ...SQLQueryBuilderOption) *SQLQueryBuilder { + builder := &SQLQueryBuilder{ + Table: table, + } + for _, opt := range opts { + opt(builder) + } + + return builder +} + +// With adds a new option to query builder +func (q *SQLQueryBuilder) With(opt SQLQueryBuilderOption) { + opt(q) +} diff --git a/common/mpostgres/postgres.go b/common/mpostgres/postgres.go new file mode 100644 index 00000000..a1457e1a --- /dev/null +++ b/common/mpostgres/postgres.go @@ -0,0 +1,120 @@ +package mpostgres + +import ( + "context" + "database/sql" + "errors" + "fmt" + "log" + "net/url" + "path/filepath" + + _ "github.com/jackc/pgx/v5/stdlib" + + "go.uber.org/zap" + + "github.com/bxcodec/dbresolver/v2" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + + // File system migration source. We need to import it to be able to use it as source in migrate.NewWithSourceInstance + _ "github.com/golang-migrate/migrate/v4/source/file" +) + +// PostgresConnection is a hub which deal with postgres connections. +type PostgresConnection struct { + ConnectionStringPrimary string + ConnectionStringReplica string + PrimaryDBName string + ReplicaDBName string + ConnectionDB *dbresolver.DB + Connected bool +} + +// Connect keeps a singleton connection with postgres. +func (pc *PostgresConnection) Connect() error { + fmt.Println("Connecting to primary and replica databases...") + + dbPrimary, err := sql.Open("pgx", pc.ConnectionStringPrimary) + if err != nil { + log.Fatal("failed to open connect to primary database", zap.Error(err)) + return nil + } + + dbReadOnlyReplica, err := sql.Open("pgx", pc.ConnectionStringReplica) + if err != nil { + log.Fatal("failed to open connect to replica database", zap.Error(err)) + return nil + } + + connectionDB := dbresolver.New( + dbresolver.WithPrimaryDBs(dbPrimary), + dbresolver.WithReplicaDBs(dbReadOnlyReplica), + dbresolver.WithLoadBalancer(dbresolver.RoundRobinLB)) + + migrationsPath, err := filepath.Abs(filepath.Join("components", "ledger", "migrations")) + if err != nil { + log.Fatal("failed get filepath", + zap.Error(err)) + + return err + } + + primaryURL, err := url.Parse(filepath.ToSlash(migrationsPath)) + if err != nil { + log.Fatal("failed parse url", + zap.Error(err)) + + return err + } + + primaryURL.Scheme = "file" + + primaryDriver, err := postgres.WithInstance(dbPrimary, &postgres.Config{ + MultiStatementEnabled: true, + DatabaseName: pc.PrimaryDBName, + SchemaName: "public", + }) + if err != nil { + log.Fatalf("failed to open connect to database %v", zap.Error(err)) + return nil + } + + m, err := migrate.NewWithDatabaseInstance(primaryURL.String(), pc.PrimaryDBName, primaryDriver) + if err != nil { + log.Fatal("failed to get migrations", + zap.Error(err)) + + return err + } + + if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return err + } + + if err := connectionDB.Ping(); err != nil { + log.Printf("PostgresConnection.Ping %v", + zap.Error(err)) + + return err + } + + pc.Connected = true + pc.ConnectionDB = &connectionDB + + fmt.Println("Connected to postgres โœ… ") + + return nil +} + +// GetDB returns a pointer to the postgres connection, initializing it if necessary. +func (pc *PostgresConnection) GetDB(ctx context.Context) (dbresolver.DB, error) { + if pc.ConnectionDB == nil { + if err := pc.Connect(); err != nil { + log.Printf("ERRCONECT %s", err) + return nil, err + } + } + + return *pc.ConnectionDB, nil +} diff --git a/common/mpostgres/query.options.go b/common/mpostgres/query.options.go new file mode 100644 index 00000000..a382f039 --- /dev/null +++ b/common/mpostgres/query.options.go @@ -0,0 +1,49 @@ +package mpostgres + +import ( + "fmt" + "strings" +) + +// DefaultMaxLimit is the default max limit for any query using db.LimitOffsetQuery +// It can be overrideable by LimitOffsetQuery.MaxLimit +const DefaultMaxLimit int64 = 50 + +// WithLimit adds limit to a SQL query builder +var WithLimit = func(limit int64) SQLQueryBuilderOption { + return func(b *SQLQueryBuilder) { + b.Limit = fmt.Sprintf("LIMIT %d", limit) + } +} + +// WithLimitOffset adds limit and offset pagination style to a SQL query builder +func WithLimitOffset(limit, offset int64) SQLQueryBuilderOption { + return func(b *SQLQueryBuilder) { + b.Limit = fmt.Sprintf("LIMIT %d", limit) + b.Offset = fmt.Sprintf("OFFSET %d", offset) + } +} + +// WithSort sorts the results by given sort argument +var WithSort = func(field string, order string) SQLQueryBuilderOption { + return func(b *SQLQueryBuilder) { + b.Sorts = append(b.Sorts, fmt.Sprintf("%s %s", field, strings.ToUpper(order))) + } +} + +// WithTextSearch searches for docs using PostgreSQL full-text search +var WithTextSearch = func(field, text string) SQLQueryBuilderOption { + return func(b *SQLQueryBuilder) { + if text != "" { + b.Where = append(b.Where, fmt.Sprintf("%s @@ to_tsquery('%s')", field, text)) + } + } +} + +// WithFilter adds a new filter condition to the query builder +func WithFilter(column string, value any) SQLQueryBuilderOption { + return func(b *SQLQueryBuilder) { + b.Params = append(b.Params, value) + b.Where = append(b.Where, fmt.Sprintf("%s = $%d", column, len(b.Params))) + } +} diff --git a/common/mpostgres/regex.go b/common/mpostgres/regex.go new file mode 100644 index 00000000..0b84af29 --- /dev/null +++ b/common/mpostgres/regex.go @@ -0,0 +1,102 @@ +package mpostgres + +// RegexIgnoreAccents receives a regex, than, for each char it's adds the accents variations to expression +// Ex: Given "a" -> "aรกร รฃรข" +// Ex: Given "c" -> "รง" +func RegexIgnoreAccents(regex string) string { + m1 := map[string]string{ + "a": "[aรกร รฃรข]", + "e": "[eรฉรจรช]", + "i": "[iรญรฌรฎ]", + "o": "[oรณรฒรตรด]", + "u": "[uรนรบรป]", + "c": "[cรง]", + "A": "[Aรร€รƒร‚]", + "E": "[Eร‰รˆรŠ]", + "I": "[IรรŒรŽ]", + "O": "[Oร“ร’ร•ร”]", + "U": "[Uร™รšร›]", + "C": "[Cร‡]", + } + m2 := map[string]string{ + "a": "a", + "รก": "a", + "ร ": "a", + "รฃ": "a", + "รข": "a", + "e": "e", + "รฉ": "e", + "รจ": "e", + "รช": "e", + "i": "i", + "รญ": "i", + "รฌ": "i", + "รฎ": "i", + "o": "o", + "รณ": "o", + "รฒ": "o", + "รต": "o", + "รด": "o", + "u": "u", + "รน": "u", + "รบ": "u", + "รป": "u", + "c": "c", + "รง": "c", + "A": "A", + "ร": "A", + "ร€": "A", + "รƒ": "A", + "ร‚": "A", + "E": "E", + "ร‰": "E", + "รˆ": "E", + "รŠ": "E", + "I": "I", + "ร": "I", + "รŒ": "I", + "รŽ": "I", + "O": "O", + "ร“": "O", + "ร’": "O", + "ร•": "O", + "ร”": "O", + "U": "U", + "ร™": "U", + "รš": "U", + "ร›": "U", + "C": "C", + "ร‡": "C", + } + s := "" + + for _, ch := range regex { + c := string(ch) + if v1, found := m2[c]; found { + if v2, found2 := m1[v1]; found2 { + s += v2 + continue + } + } + + s += string(ch) + } + + return s +} + +// RemoveChars from a string +func RemoveChars(str string, chars map[string]bool) string { + s := "" + + for _, ch := range str { + c := string(ch) + if _, found := chars[c]; found { + continue + } + + s += string(ch) + } + + return s +} diff --git a/common/mzap/injector.go b/common/mzap/injector.go new file mode 100644 index 00000000..f611dc6e --- /dev/null +++ b/common/mzap/injector.go @@ -0,0 +1,59 @@ +package mzap + +import ( + "fmt" + "log" + "os" + + "github.com/LerianStudio/midaz/common/console" + "github.com/LerianStudio/midaz/common/mlog" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// InitializeLogger initializes our log layer and returns it +// +//nolint:ireturn +func InitializeLogger() mlog.Logger { + fmt.Println(console.Title("InitializeLogger")) + + var zapCfg zap.Config + + if os.Getenv("ENV_NAME") == "local" { + zapCfg = zap.NewDevelopmentConfig() + } else { + zapCfg = zap.NewProductionConfig() + } + + if val, ok := os.LookupEnv("LOG_LEVEL"); ok { + var lvl zapcore.Level + if err := lvl.Set(val); err != nil { + log.Printf("Invalid LOG_LEVEL, fallback to InfoLevel: %v", err) + + lvl = zapcore.InfoLevel + } + + zapCfg.Level = zap.NewAtomicLevelAt(lvl) + } + + zapCfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + + zapCfg.DisableStacktrace = true + + logger, err := zapCfg.Build() + if err != nil { + log.Fatalf("can't initialize zap logger: %v", err) + } + + sugar := logger.Sugar() + + fmt.Printf("Log level is (%v)\n", zapCfg.Level) + fmt.Printf("Logger is (%T)\n", sugar) + + fmt.Println(console.Line(console.DefaultLineSize)) + + return &ZapLogger{ + Logger: sugar, + } +} diff --git a/common/mzap/zap.go b/common/mzap/zap.go new file mode 100644 index 00000000..55973e74 --- /dev/null +++ b/common/mzap/zap.go @@ -0,0 +1,67 @@ +package mzap + +import ( + "github.com/LerianStudio/midaz/common/mlog" + "go.uber.org/zap" +) + +// ZapLogger is a wrapper of zap.SugaredLogger. +type ZapLogger struct { + Logger *zap.SugaredLogger +} + +// Info implements Info Logger interface function. +func (l *ZapLogger) Info(args ...any) { l.Logger.Info(args...) } + +// Infof implements Infof Logger interface function. +func (l *ZapLogger) Infof(format string, args ...any) { l.Logger.Infof(format, args...) } + +// Infoln implements Infoln Logger interface function. +func (l *ZapLogger) Infoln(args ...any) { l.Logger.Infoln(args...) } + +// Error implements Error Logger interface function. +func (l *ZapLogger) Error(args ...any) { l.Logger.Error(args...) } + +// Errorf implements Errorf Logger interface function. +func (l *ZapLogger) Errorf(format string, args ...any) { l.Logger.Errorf(format, args...) } + +// Errorln implements Errorln Logger interface function +func (l *ZapLogger) Errorln(args ...any) { l.Logger.Errorln(args...) } + +// Warn implements Warn Logger interface function. +func (l *ZapLogger) Warn(args ...any) { l.Logger.Warn(args...) } + +// Warnf implements Warnf Logger interface function. +func (l *ZapLogger) Warnf(format string, args ...any) { l.Logger.Warnf(format, args...) } + +// Warnln implements Warnln Logger interface function +func (l *ZapLogger) Warnln(args ...any) { l.Logger.Warnln(args...) } + +// Debug implements Debug Logger interface function. +func (l *ZapLogger) Debug(args ...any) { l.Logger.Debug(args...) } + +// Debugf implements Debugf Logger interface function. +func (l *ZapLogger) Debugf(format string, args ...any) { l.Logger.Debugf(format, args...) } + +// Debugln implements Debugln Logger interface function +func (l *ZapLogger) Debugln(args ...any) { l.Logger.Debugln(args...) } + +// Fatal implements Fatal Logger interface function. +func (l *ZapLogger) Fatal(args ...any) { l.Logger.Fatal(args...) } + +// Fatalf implements Fatalf Logger interface function. +func (l *ZapLogger) Fatalf(format string, args ...any) { l.Logger.Fatalf(format, args...) } + +// Fatalln implements Fatalln Logger interface function +func (l *ZapLogger) Fatalln(args ...any) { l.Logger.Fatalln(args...) } + +// WithFields adds structured context to the logger. It returns a new logger and leaves the original unchanged. +// +//nolint:ireturn +func (l *ZapLogger) WithFields(fields ...any) mlog.Logger { + newLogger := l.Logger.With(fields...) + + return &ZapLogger{ + Logger: newLogger, + } +} diff --git a/common/net/http/doc.go b/common/net/http/doc.go new file mode 100644 index 00000000..2aa5f5b8 --- /dev/null +++ b/common/net/http/doc.go @@ -0,0 +1,25 @@ +package http + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/swagger" +) + +// DocAPI adds the default documentation route to the API. +// Ex: /{serviceName}/docs +// And adds the swagger route too. +// Ex: /{serviceName}/swagger.yaml +func DocAPI(serviceName, title string, app *fiber.App) { + docURL := fmt.Sprintf("/%s/docs", serviceName) + + app.Get(docURL, func(c *fiber.Ctx) error { + return c.SendFile("./components/ledger/api/v1.yml") + }) + + app.Get("/v1/swagger/*", swagger.New(swagger.Config{ + URL: docURL, + Title: title, + })) +} diff --git a/common/net/http/errors.go b/common/net/http/errors.go new file mode 100644 index 00000000..1bbf35b2 --- /dev/null +++ b/common/net/http/errors.go @@ -0,0 +1,70 @@ +package http + +import ( + "github.com/LerianStudio/midaz/common" + "github.com/gofiber/fiber/v2" +) + +// ResponseError is a struct used to return errors to the client. +type ResponseError struct { + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Origin *string `json:"origin,omitempty"` +} + +// Error returns the message of the ResponseError. +// +// No parameters. +// Returns a string. +func (r ResponseError) Error() string { + return r.Message +} + +// ValidationError records an error indicating an entity was not found in any case that caused it. +type ValidationError struct { + EntityType string `json:"entityType,omitempty"` + Title string `json:"title,omitempty"` + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Fields FieldValidations `json:"fields,omitempty"` +} + +// Error returns the error message for a ValidationError. +// +// No parameters. +// Returns a string. +func (r ValidationError) Error() string { + return r.Message +} + +// FieldValidations is a map of fields and their validation errors. +type FieldValidations map[string]string + +// WithError returns an error with the given status code and message. +func WithError(c *fiber.Ctx, err error) error { + switch e := err.(type) { + case common.EntityNotFoundError: + return NotFound(c, e.Code, e.Message) + case common.EntityConflictError: + return Conflict(c, e.Code, e.Message) + case common.ValidationError: + return BadRequest(c, ValidationError{ + Code: e.Code, + Message: e.Message, + Fields: nil, + }) + case common.UnprocessableOperationError: + return UnprocessableEntity(c, e.Code, e.Message) + case common.UnauthorizedError: + return Unauthorized(c, e.Code, e.Error()) + case common.ForbiddenError: + return Forbidden(c, e.Message) + case *ValidationError, ValidationError: + return BadRequest(c, e) + case ResponseError: + rErr, _ := err.(ResponseError) + return JSONResponseError(c, rErr) + default: + return InternalServerError(c, e.Error()) + } +} diff --git a/common/net/http/handler.go b/common/net/http/handler.go new file mode 100644 index 00000000..0cbdf874 --- /dev/null +++ b/common/net/http/handler.go @@ -0,0 +1,51 @@ +package http + +import ( + "log" + "os" + "time" + + "github.com/gofiber/fiber/v2" +) + +// Ping returns HTTP Status 200 with response "pong". +func Ping(c *fiber.Ctx) error { + if err := c.SendString("healthy"); err != nil { + log.Print(err.Error()) + } + + return nil +} + +// Version returns HTTP Status 200 with given version. +func Version(version string) fiber.Handler { + return func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "version": version, + "buildNumber": os.Getenv("BUILD_NUMBER"), + "requestDate": time.Now().UTC(), + }) + } +} + +// Welcome returns HTTP Status 200 with service info. +func Welcome(service string, description string) fiber.Handler { + return func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "service": service, + "description": description, + }) + } +} + +// NotImplementedEndpoint returns HTTP 501 with not implemented message. +func NotImplementedEndpoint(c *fiber.Ctx) error { + return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Not implemented yet"}) +} + +// File servers a specific file. +func File(filePath string) fiber.Handler { + return func(c *fiber.Ctx) error { + return c.SendFile(filePath) + } +} diff --git a/common/net/http/headers.go b/common/net/http/headers.go new file mode 100644 index 00000000..61eb8d1a --- /dev/null +++ b/common/net/http/headers.go @@ -0,0 +1,8 @@ +package http + +const ( + headerCorrelationID = "X-Correlation-ID" + headerUserAgent = "User-Agent" + headerRealIP = "X-Real-Ip" + headerForwardedFor = "X-Forwarded-For" +) diff --git a/common/net/http/httputils.go b/common/net/http/httputils.go new file mode 100644 index 00000000..66efc86d --- /dev/null +++ b/common/net/http/httputils.go @@ -0,0 +1,38 @@ +package http + +import ( + "net/http" + "strings" +) + +// IPAddrFromRemoteAddr removes port information from string. +func IPAddrFromRemoteAddr(s string) string { + idx := strings.LastIndex(s, ":") + if idx == -1 { + return s + } + + return s[:idx] +} + +// GetRemoteAddress returns IP address of the client making the request. +// It checks for X-Real-Ip or X-Forwarded-For headers which is used by Proxies. +func GetRemoteAddress(r *http.Request) string { + realIP := r.Header.Get(headerRealIP) + forwardedFor := r.Header.Get(headerForwardedFor) + + if realIP == "" && forwardedFor == "" { + return IPAddrFromRemoteAddr(r.RemoteAddr) + } + + if forwardedFor != "" { + parts := strings.Split(forwardedFor, ",") + for i, p := range parts { + parts[i] = strings.TrimSpace(p) + } + + return parts[0] + } + + return realIP +} diff --git a/common/net/http/proxy.go b/common/net/http/proxy.go new file mode 100644 index 00000000..76413458 --- /dev/null +++ b/common/net/http/proxy.go @@ -0,0 +1,26 @@ +package http + +import ( + "net/http" + "net/http/httputil" + "net/url" +) + +// ServeReverseProxy serves a reverse proxy for a given url. +func ServeReverseProxy(target string, res http.ResponseWriter, req *http.Request) { + targetURL, err := url.Parse(target) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + proxy := httputil.NewSingleHostReverseProxy(targetURL) + + // Update the headers to allow for SSL redirection + req.URL.Host = targetURL.Host + req.URL.Scheme = targetURL.Scheme + req.Header.Set("X-Forwarded-Host", req.Header.Get("Host")) + req.Host = targetURL.Host + + proxy.ServeHTTP(res, req) +} diff --git a/common/net/http/response.go b/common/net/http/response.go new file mode 100644 index 00000000..41109f4a --- /dev/null +++ b/common/net/http/response.go @@ -0,0 +1,108 @@ +package http + +import ( + "net/http" + + "github.com/gofiber/fiber/v2" +) + +// Unauthorized sends an HTTP 401 Unauthorized response with a custom code and message. +func Unauthorized(c *fiber.Ctx, code string, message string) error { + return c.Status(http.StatusUnauthorized).JSON(fiber.Map{ + "code": code, + "message": message, + }) +} + +// Forbidden sends an HTTP 403 Forbidden response with a custom message. +func Forbidden(c *fiber.Ctx, message string) error { + return c.Status(http.StatusForbidden).JSON(fiber.Map{ + "code": http.StatusForbidden, + "message": message, + }) +} + +// BadRequest sends an HTTP 400 Bad Request response with a custom body. +func BadRequest(c *fiber.Ctx, s any) error { + return c.Status(http.StatusBadRequest).JSON(s) +} + +// Created sends an HTTP 201 Created response with a custom body. +func Created(c *fiber.Ctx, s any) error { + return c.Status(http.StatusCreated).JSON(s) +} + +// OK sends an HTTP 200 OK response with a custom body. +func OK(c *fiber.Ctx, s any) error { + return c.Status(http.StatusOK).JSON(s) +} + +// NoContent sends an HTTP 204 No Content response without any body. +func NoContent(c *fiber.Ctx) error { + return c.SendStatus(http.StatusNoContent) +} + +// Accepted sends an HTTP 202 Accepted response with a custom body. +func Accepted(c *fiber.Ctx, s any) error { + return c.Status(http.StatusAccepted).JSON(s) +} + +// PartialContent sends an HTTP 206 Partial Content response with a custom body. +func PartialContent(c *fiber.Ctx, s any) error { + return c.Status(http.StatusPartialContent).JSON(s) +} + +// RangeNotSatisfiable sends an HTTP 416 Requested Range Not Satisfiable response. +func RangeNotSatisfiable(c *fiber.Ctx) error { + return c.SendStatus(http.StatusRequestedRangeNotSatisfiable) +} + +// NotFound sends an HTTP 404 Not Found response with a custom code and message. +func NotFound(c *fiber.Ctx, code, message string) error { + return c.Status(http.StatusNotFound).JSON(fiber.Map{ + "code": code, + "message": message, + }) +} + +// Conflict sends an HTTP 409 Conflict response with a custom code and message. +func Conflict(c *fiber.Ctx, code, message string) error { + return c.Status(http.StatusConflict).JSON(fiber.Map{ + "code": code, + "message": message, + }) +} + +// NotImplemented sends an HTTP 501 Not Implemented response with a custom message. +func NotImplemented(c *fiber.Ctx, message string) error { + return c.Status(http.StatusNotImplemented).JSON(fiber.Map{ + "code": http.StatusNotImplemented, + "message": message, + }) +} + +// UnprocessableEntity sends an HTTP 422 Unprocessable Entity response with a custom code and message. +func UnprocessableEntity(c *fiber.Ctx, code, message string) error { + return c.Status(http.StatusUnprocessableEntity).JSON(fiber.Map{ + "code": code, + "message": message, + }) +} + +// InternalServerError sends an HTTP 500 Internal Server Error response with a custom message. +func InternalServerError(c *fiber.Ctx, message string) error { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "code": "INTERNAL_SERVER_ERROR", + "message": message, + }) +} + +// JSONResponseError sends a JSON formatted error response with a custom error struct. +func JSONResponseError(c *fiber.Ctx, err ResponseError) error { + return c.Status(err.Code).JSON(err) +} + +// JSONResponse sends a custom status code and body as a JSON response. +func JSONResponse(c *fiber.Ctx, status int, s any) error { + return c.Status(status).JSON(s) +} diff --git a/common/net/http/withBasicAuth.go b/common/net/http/withBasicAuth.go new file mode 100644 index 00000000..377056e3 --- /dev/null +++ b/common/net/http/withBasicAuth.go @@ -0,0 +1,64 @@ +package http + +import ( + "crypto/subtle" + "encoding/base64" + "strings" + + "github.com/gofiber/fiber/v2" +) + +// BasicAuthFunc represents a func which returns if a username and password was authenticated or not. +// It returns true if authenticated, and false when not authenticated. +type BasicAuthFunc func(username, password string) bool + +// FixedBasicAuthFunc is a fixed username and password to use as BasicAuthFunc. +func FixedBasicAuthFunc(username, password string) BasicAuthFunc { + return func(user, pass string) bool { + if subtle.ConstantTimeCompare([]byte(user), []byte(username)) == 1 && subtle.ConstantTimeCompare([]byte(pass), []byte(password)) == 1 { + return true + } + + return false + } +} + +// WithBasicAuth creates a basic authentication middleware. +func WithBasicAuth(f BasicAuthFunc, realm string) fiber.Handler { + return func(c *fiber.Ctx) error { + auth := c.Get("Authorization") + if auth == "" { + return unauthorizedResponse(c, realm) + } + + parts := strings.SplitN(auth, " ", 2) + if len(parts) != 2 || parts[0] != "Basic" { + return unauthorizedResponse(c, realm) + } + + cred, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + return unauthorizedResponse(c, realm) + } + + pair := strings.SplitN(string(cred), ":", 2) + if len(pair) != 2 { + return unauthorizedResponse(c, realm) + } + + if f(pair[0], pair[1]) { + return c.Next() + } + + return unauthorizedResponse(c, realm) + } +} + +func unauthorizedResponse(c *fiber.Ctx, realm string) error { + c.Set("WWW-Authenticate", `Basic realm="`+realm+`"`) + + return c.Status(401).JSON(fiber.Map{ + "code": 401, + "message": "Unauthorized request", + }) +} diff --git a/common/net/http/withBody.go b/common/net/http/withBody.go new file mode 100644 index 00000000..f41bc7f6 --- /dev/null +++ b/common/net/http/withBody.go @@ -0,0 +1,228 @@ +package http + +import ( + "encoding/json" + "reflect" + "strings" + + "github.com/gofiber/fiber/v2" + + "github.com/go-playground/locales/en" + ut "github.com/go-playground/universal-translator" + en2 "github.com/go-playground/validator/translations/en" + + "gopkg.in/go-playground/validator.v9" +) + +// DecodeHandlerFunc is a handler which works with withBody decorator. +// It receives a struct which was decoded by withBody decorator before. +// Ex: json -> withBody -> DecodeHandlerFunc. +type DecodeHandlerFunc func(p any, c *fiber.Ctx) error + +// PayloadContextValue is a wrapper type used to keep Context.Locals safe. +type PayloadContextValue string + +// ConstructorFunc representing a constructor of any type. +type ConstructorFunc func() any + +// decoderHandler decodes payload coming from requests. +type decoderHandler struct { + handler DecodeHandlerFunc + constructor ConstructorFunc + structSource any +} + +func newOfType(s any) any { + t := reflect.TypeOf(s) + v := reflect.New(t.Elem()) + + return v.Interface() +} + +// FiberHandlerFunc is a method on the decoderHandler struct. It decodes the incoming request's body to a Go struct, +// validates it, checks for any extraneous fields not defined in the struct, and finally calls the wrapped handler function. +func (d *decoderHandler) FiberHandlerFunc(c *fiber.Ctx) error { + var s any + + if d.constructor != nil { + s = d.constructor() + } else { + s = newOfType(d.structSource) + } + + bodyBytes := c.Body() // Get the body bytes + + if err := json.Unmarshal(bodyBytes, s); err != nil { + return err + } + + marshaled, err := json.Marshal(s) + if err != nil { + return err + } + + var originalMap, marshaledMap map[string]any + + if err := json.Unmarshal(bodyBytes, &originalMap); err != nil { + return err + } + + if err := json.Unmarshal(marshaled, &marshaledMap); err != nil { + return err + } + + // Generate a map that only contains fields that are present in the original payload but not recognized by the Go struct. + diffFields := make(map[string]any) + + for key, value := range originalMap { + if _, ok := marshaledMap[key]; !ok { + diffFields[key] = value + } + } + + if len(diffFields) > 0 { + return BadRequest(c, fiber.Map{"code": "BAD_REQUEST", "message": "Incoming JSON fields do not match the request payload fields", "fields": diffFields}) + } + + if err := ValidateStruct(s); err != nil { + return BadRequest(c, err) + } + + c.Locals(string(PayloadContextValue("fields")), diffFields) + + return d.handler(s, c) +} + +// WithDecode wraps a handler function, providing it with a struct instance created using the provided constructor function. +func WithDecode(c ConstructorFunc, h DecodeHandlerFunc) fiber.Handler { + d := &decoderHandler{ + handler: h, + constructor: c, + } + + return d.FiberHandlerFunc +} + +// WithBody wraps a handler function, providing it with an instance of the specified struct. +func WithBody(s any, h DecodeHandlerFunc) fiber.Handler { + d := &decoderHandler{ + handler: h, + structSource: s, + } + + return d.FiberHandlerFunc +} + +// SetBodyInContext is a higher-order function that wraps a Fiber handler, injecting the decoded body into the request context. +func SetBodyInContext(handler fiber.Handler) DecodeHandlerFunc { + return func(s any, c *fiber.Ctx) error { + c.Locals(string(PayloadContextValue("payload")), s) + return handler(c) + } +} + +// GetPayloadFromContext retrieves the decoded request payload from the Fiber context. +func GetPayloadFromContext(c *fiber.Ctx) any { + return c.Locals(string(PayloadContextValue("payload"))) +} + +// ValidateStruct validates a struct against defined validation rules, using the validator package. +func ValidateStruct(s any) error { + v, trans := newValidator() + + k := reflect.ValueOf(s).Kind() + if k == reflect.Ptr { + k = reflect.ValueOf(s).Elem().Kind() + } + + if k != reflect.Struct { + return nil + } + + err := v.Struct(s) + if err != nil { + errPtr := malformedRequestErr(err.(validator.ValidationErrors), trans) + return &errPtr + } + + return nil +} + +//nolint:ireturn +func newValidator() (*validator.Validate, ut.Translator) { + locale := en.New() + uni := ut.New(locale, locale) + + trans, _ := uni.GetTranslator("en") + + v := validator.New() + + if err := en2.RegisterDefaultTranslations(v, trans); err != nil { + panic(err) + } + + v.RegisterTagNameFunc(func(fld reflect.StructField) string { + name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] + if name == "-" { + return "" + } + + return name + }) + + _ = v.RegisterTranslation("CPF", trans, func(ut ut.Translator) error { + return ut.Add("CPF", "{0} must be a valid Brazilian CPF", true) + }, func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T("CPF", fe.Field()) + + return t + }) + + _ = v.RegisterTranslation("localPhoneNumber", trans, func(ut ut.Translator) error { + return ut.Add("localPhoneNumber", "{0} must be a valid phone number without the country code", true) + }, func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T("localPhoneNumber", fe.Field()) + + return t + }) + + _ = v.RegisterTranslation("phoneNumber", trans, func(ut ut.Translator) error { + return ut.Add("phoneNumber", "{0} must be a valid phone number", true) + }, func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T("phoneNumber", fe.Field()) + + return t + }) + + _ = v.RegisterTranslation("countryCode", trans, func(ut ut.Translator) error { + return ut.Add("countryCode", "{0} must be a valid countryCode registered in https://countrycode.org", true) + }, func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T("countryCode", fe.Field()) + + return t + }) + + return v, trans +} + +func malformedRequestErr(err validator.ValidationErrors, trans ut.Translator) ValidationError { + return ValidationError{ + Code: "400", + Message: "Malformed request.", + Fields: fields(err, trans), + } +} + +func fields(errors validator.ValidationErrors, trans ut.Translator) FieldValidations { + l := len(errors) + if l > 0 { + fields := make(FieldValidations, l) + for _, e := range errors { + fields[e.Field()] = e.Translate(trans) + } + + return fields + } + + return nil +} diff --git a/common/net/http/withBody_test.go b/common/net/http/withBody_test.go new file mode 100644 index 00000000..6f262254 --- /dev/null +++ b/common/net/http/withBody_test.go @@ -0,0 +1,44 @@ +package http + +import ( + "encoding/json" + "testing" +) + +type SimpleStruct struct { + Name string + Age int +} + +type ComplexStruct struct { + Enable bool + Simple SimpleStruct +} + +func TestNewOfTypeWithSimpleStruct(t *testing.T) { + s := newOfType(new(SimpleStruct)) + + if err := json.Unmarshal([]byte("{\"Name\":\"Bruce\", \"Age\": 18}"), s); err != nil { + t.Error(err) + } + + sPrt := s.(*SimpleStruct) + + if sPrt.Name != "Bruce" || sPrt.Age != 18 { + t.Error("Wrong data.") + } +} + +func TestNewOfTypeWithComplexStruct(t *testing.T) { + s := newOfType(new(ComplexStruct)) + + if err := json.Unmarshal([]byte("{\"Simple\": {\"Name\":\"Bruce\", \"Age\": 18}}"), s); err != nil { + t.Error(err) + } + + sPrt := s.(*ComplexStruct) + + if sPrt.Simple.Name != "Bruce" || sPrt.Simple.Age != 18 { + t.Error("Wrong data.") + } +} diff --git a/common/net/http/withCORS.go b/common/net/http/withCORS.go new file mode 100644 index 00000000..d7f98b30 --- /dev/null +++ b/common/net/http/withCORS.go @@ -0,0 +1,35 @@ +package http + +import ( + "github.com/LerianStudio/midaz/common" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +const ( + defaultAccessControlAllowOrigin = "*" + defaultAccessControlAllowMethods = "POST, GET, OPTIONS, PUT, DELETE, PATCH" + defaultAccessControlAllowHeaders = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization" + defaultAccessControlExposeHeaders = "" +) + +// WithCORS is a middleware that enables CORS. +// Replace it with a real CORS middleware implementation. +func WithCORS() fiber.Handler { + return cors.New(cors.Config{ + AllowOrigins: common.GetenvOrDefault("ACCESS_CONTROL_ALLOW_ORIGIN", defaultAccessControlAllowOrigin), + AllowMethods: common.GetenvOrDefault("ACCESS_CONTROL_ALLOW_METHODS", defaultAccessControlAllowMethods), + AllowHeaders: common.GetenvOrDefault("ACCESS_CONTROL_ALLOW_HEADERS", defaultAccessControlAllowHeaders), + ExposeHeaders: common.GetenvOrDefault("ACCESS_CONTROL_EXPOSE_HEADERS", defaultAccessControlExposeHeaders), + AllowCredentials: true, + }) +} + +// AllowFullOptionsWithCORS set r.Use(WithCORS) and allow every request to use OPTION method. +func AllowFullOptionsWithCORS(app *fiber.App) { + app.Use(WithCORS()) + + app.Options("/*", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusNoContent) + }) +} diff --git a/common/net/http/withCorrelationID.go b/common/net/http/withCorrelationID.go new file mode 100644 index 00000000..f62915a0 --- /dev/null +++ b/common/net/http/withCorrelationID.go @@ -0,0 +1,18 @@ +package http + +import ( + "github.com/gofiber/fiber/v2" + gid "github.com/google/uuid" +) + +// WithCorrelationID creates a correlation id. +func WithCorrelationID() fiber.Handler { + return func(c *fiber.Ctx) error { + cid := gid.New().String() + + c.Set(headerCorrelationID, cid) + c.Request().Header.Add(headerCorrelationID, cid) + + return c.Next() + } +} diff --git a/common/net/http/withJWT.go b/common/net/http/withJWT.go new file mode 100644 index 00000000..a570a1a3 --- /dev/null +++ b/common/net/http/withJWT.go @@ -0,0 +1,232 @@ +package http + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/LerianStudio/midaz/common/mlog" + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt" + "github.com/lestrrat-go/jwx/jwk" + "github.com/patrickmn/go-cache" +) + +const ( + jwkDefaultDuration = time.Hour * 1 + superOrgScope = "*" +) + +// TokenContextValue is a wrapper type used to keep Context.Locals safe. +type TokenContextValue string + +// ProfileID is the profileID type of a member. +type ProfileID string + +// OAuth2JWTToken represents a self-contained way for securely transmitting information between parties as a JSON object +// https://tools.ietf.org/html/rfc7519 +type OAuth2JWTToken struct { + Token *jwt.Token + Claims jwt.MapClaims + Groups []string + Sub string + Username *string + Scope string + ScopeSet map[string]bool +} + +// TokenFromContext extracts a JWT token from context. +func TokenFromContext(c *fiber.Ctx) (*OAuth2JWTToken, error) { + if tokenValue := c.Locals(string(TokenContextValue("token"))); tokenValue != nil { + if token, ok := tokenValue.(*jwt.Token); ok { + if claims, ok := token.Claims.(jwt.MapClaims); ok { + t := &OAuth2JWTToken{ + Token: token, + Claims: claims, + Sub: claims["sub"].(string), + } + if username, found := claims["username"].(string); found { + t.Username = &username + } + + if scope, found := claims["scope"].(string); found { + t.Scope = scope + t.ScopeSet = make(map[string]bool) + + for _, s := range strings.Split(scope, " ") { + t.ScopeSet[s] = true + } + } + + if groups, found := claims["cognito:groups"].([]any); found { + t.Groups = convertGroups(groups) + } + + return t, nil + } + } + } + + return nil, errors.New("invalid JWT token") +} + +func convertGroups(groups []any) []string { + newGroups := make([]string, 0) + + for _, g := range groups { + if v, ok := g.(string); ok { + newGroups = append(newGroups, v) + } + } + + return newGroups +} + +func getTokenHeader(c *fiber.Ctx) string { + splitToken := strings.Split(c.Get(fiber.HeaderAuthorization), "Bearer") + if len(splitToken) == 2 { + return strings.TrimSpace(splitToken[1]) + } + + return "" +} + +// JWKProvider manages cryptographic public keys issued by an authorization server +// See https://tools.ietf.org/html/rfc7517 +// It's used to verify JSON Web Tokens which was signed using RS256 signing algorithm. +type JWKProvider struct { + URI string + CacheDuration time.Duration + cache *cache.Cache + once sync.Once +} + +// Fetch fetches (JWKS) JSON Web Key Set from authorization server and cache it +// +//nolint:ireturn +func (p *JWKProvider) Fetch(ctx context.Context) (jwk.Set, error) { + p.once.Do(func() { + p.cache = cache.New(p.CacheDuration, p.CacheDuration) + }) + + if set, found := p.cache.Get(p.URI); found { + return set.(jwk.Set), nil + } + + set, err := jwk.Fetch(ctx, p.URI) + if err != nil { + return nil, err + } + + p.cache.Set(p.URI, set, p.CacheDuration) + + return set, nil +} + +// JWTMiddleware represents a middleware which protects endpoint using JWT tokens. +type JWTMiddleware struct { + JWK *JWKProvider +} + +// NewJWTMiddleware create an instance of JWTMiddleware +// It uses JWK cache duration of 1 hour. +func NewJWTMiddleware(jwkURI string) *JWTMiddleware { + return &JWTMiddleware{ + JWK: &JWKProvider{ + URI: jwkURI, + CacheDuration: jwkDefaultDuration, + }, + } +} + +// Protect protects any endpoint using JWT tokens. +func (m *JWTMiddleware) Protect() fiber.Handler { + return func(c *fiber.Ctx) error { + l := mlog.NewLoggerFromContext(c.UserContext()) + l.Debug("JWTMiddleware:Protect") + + l.Debug("Read token from header") + + tokenString := getTokenHeader(c) + + if len(tokenString) == 0 { + return Unauthorized(c, "INVALID_REQUEST", "Must provide a token") + } + + // TODO: Need to be cached + l.Debugf("Get JWK keys using %s", m.JWK.URI) + + keySet, err := m.JWK.Fetch(context.Background()) + if err != nil { + msg := fmt.Sprint("Couldn't now load JWK keys from source: ", err.Error()) + l.Error(msg) + + return InternalServerError(c, msg) + } + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + kid, ok := token.Header["kid"].(string) + if !ok { + return nil, errors.New("kid header not found") + } + + key, ok := keySet.LookupKeyID(kid) + if !ok { + return nil, errors.New("the provided token doesn't belongs to the required trusted issuer, check the identity server you logged in") + } + + var raw any + + if err := key.Raw(&raw); err != nil { + return nil, err + } + + return raw, nil + }) + if err != nil { + l.Error(err.Error()) + return Unauthorized(c, "AUTH_SERVER_ERROR", err.Error()) + } + + if token.Valid { + l.Debug("Token ok") + c.Locals(string(TokenContextValue("token")), token) + + return c.Next() + } + + return Unauthorized(c, "INVALID_TOKEN", "Invalid token") + } +} + +// WithScope verify if a requester has the required scope to access an endpoint. +func (m *JWTMiddleware) WithScope(scopes []string) fiber.Handler { + return func(c *fiber.Ctx) error { + t, err := TokenFromContext(c) + if err != nil { + return Unauthorized(c, "INVALID_SCOPE", "Unauthorized") + } + + authorized := false + + for _, s := range scopes { + if _, found := t.ScopeSet[s]; found { + authorized = true + break + } + } + + if authorized || len(scopes) == 0 { + return c.Next() + } + + return Forbidden(c, "Insufficient privileges") + } +} diff --git a/common/net/http/withLogging.go b/common/net/http/withLogging.go new file mode 100644 index 00000000..da977483 --- /dev/null +++ b/common/net/http/withLogging.go @@ -0,0 +1,192 @@ +package http + +import ( + "net/url" + "strconv" + "strings" + "time" + + "github.com/LerianStudio/midaz/common/mlog" + "github.com/gofiber/fiber/v2" +) + +// RequestInfo is a struct design to store http access log data. +type RequestInfo struct { + Method string + Username string + URI string + Referer string + RemoteAddress string + Status int + Date time.Time + Duration time.Duration + UserAgent string + CorrelationID string + Protocol string + Size int + Body string +} + +// ResponseMetricsWrapper is a Wrapper responsible for collect the response data such as status code and size +// It implements built-in ResponseWriter interface. +type ResponseMetricsWrapper struct { + Context *fiber.Ctx + StatusCode int + Size int + Body string +} + +// NewRequestInfo creates an instance of RequestInfo. +func NewRequestInfo(c *fiber.Ctx) *RequestInfo { + username, referer := "-", "-" + rawURL := string(c.Request().URI().FullURI()) + + parsedURL, err := url.Parse(rawURL) + if err == nil && parsedURL.User != nil { + if name := parsedURL.User.Username(); name != "" { + username = name + } + } + + if c.Get("Referer") != "" { + referer = c.Get("Referer") + } + + body := "" + + if c.Request().Header.ContentLength() > 0 { + body = string(c.Body()) + } + + return &RequestInfo{ + Method: c.Method(), + URI: c.OriginalURL(), + Username: username, + Referer: referer, + UserAgent: c.Get(headerUserAgent), + CorrelationID: c.Get(headerCorrelationID), + RemoteAddress: c.IP(), + Protocol: c.Protocol(), + Date: time.Now().UTC(), + Body: body, + } +} + +// CLFString produces a log entry format similar to Common Log Format (CLF) +// Ref: https://httpd.apache.org/docs/trunk/logs.html#common +func (r *RequestInfo) CLFString() string { + return strings.Join([]string{ + r.RemoteAddress, + "-", + r.Username, + `"` + r.Method, + r.URI, + `"` + r.Protocol, + strconv.Itoa(r.Status), + strconv.Itoa(r.Size), + r.Referer, + r.UserAgent, + }, " ") +} + +// String implements fmt.Stringer interface and produces a log entry using RequestInfo.CLFExtendedString. +func (r *RequestInfo) String() string { + return r.CLFString() +} + +func (r *RequestInfo) debugRequestString() string { + return strings.Join([]string{ + r.CLFString(), + r.Referer, + r.UserAgent, + r.CorrelationID, + r.Body, + }, " ") +} + +func (r *RequestInfo) debugResponseString(w *ResponseMetricsWrapper) string { + return strings.Join([]string{ + r.CLFString(), + r.Referer, + r.UserAgent, + r.CorrelationID, + w.Body, + }, " ") +} + +// FinishRequestInfo calculates the duration of RequestInfo automatically using time.Now() +// It also set StatusCode and Size of RequestInfo passed by ResponseMetricsWrapper. +func (r *RequestInfo) FinishRequestInfo(rw *ResponseMetricsWrapper) { + r.Duration = time.Now().UTC().Sub(r.Date) + r.Status = rw.StatusCode + r.Size = rw.Size +} + +type logMiddleware struct { + Logger mlog.Logger +} + +// LogMiddlewareOption represents the log middleware function as an implementation. +type LogMiddlewareOption func(l *logMiddleware) + +// WithLogger is a functional option for logMiddleware. +func WithLogger(logger mlog.Logger) LogMiddlewareOption { + return func(l *logMiddleware) { + l.Logger = logger + } +} + +// buildOpts creates an instance of logMiddleware with options. +func buildOpts(opts ...LogMiddlewareOption) *logMiddleware { + mid := &logMiddleware{ + Logger: &mlog.GoLogger{}, + } + + for _, opt := range opts { + opt(mid) + } + + return mid +} + +// WithLog is a middleware to log access to http server. +// It logs access log according to Apache Standard Logs which uses Common Log Format (CLF) +// Ref: https://httpd.apache.org/docs/trunk/logs.html#common +func WithLog(opts ...LogMiddlewareOption) fiber.Handler { + return func(c *fiber.Ctx) error { + if c.Path() == "/health" { + return c.Next() + } + + info := NewRequestInfo(c) + + mid := buildOpts(opts...) + logger := mid.Logger.WithFields( + headerCorrelationID, info.CorrelationID, + ) + + rw := ResponseMetricsWrapper{ + Context: c, + StatusCode: 200, + Size: 0, + Body: "", + } + + logger.Debug(info.debugRequestString()) + + ctx := mlog.ContextWithLogger(c.Context(), logger) + + c.SetUserContext(mlog.ContextWithLogger(ctx, logger)) + + info.FinishRequestInfo(&rw) + + logger.Debug(info.debugResponseString(&rw)) + logger.Infoln(info) + + if err := c.Next(); err != nil { + return err + } + + return nil + } +} diff --git a/common/os.go b/common/os.go new file mode 100644 index 00000000..6cc6e3b9 --- /dev/null +++ b/common/os.go @@ -0,0 +1,140 @@ +package common + +import ( + "fmt" + "os" + "reflect" + "strconv" + "strings" + "sync" + + "github.com/LerianStudio/midaz/common/console" + "github.com/joho/godotenv" + "github.com/pkg/errors" +) + +// GetenvOrDefault encapsulate built-in os.Getenv behavior but if key is not present it returns the defaultValue. +func GetenvOrDefault(key string, defaultValue string) string { + str := os.Getenv(key) + if strings.TrimSpace(str) == "" { + return defaultValue + } + + return str +} + +// GetenvBoolOrDefault returns the value of os.Getenv(key string) value as bool or defaultValue if error +// Is the environment variable (key) is not defined, it returns the given defaultValue +// If the environment variable (key) is not a valid bool format, it returns the given defaultValue +// If any error occurring during bool parse, it returns the given defaultValue. +func GetenvBoolOrDefault(key string, defaultValue bool) bool { + str := os.Getenv(key) + + val, err := strconv.ParseBool(str) + if err != nil { + return defaultValue + } + + return val +} + +// GetenvIntOrDefault returns the value of os.Getenv(key string) value as int or defaultValue if error +// If the environment variable (key) is not defined, it returns the given defaultValue +// If the environment variable (key) is not a valid int format, it returns the given defaultValue +// If any error occurring during int parse, it returns the given defaultValue. +func GetenvIntOrDefault(key string, defaultValue int64) int64 { + str := os.Getenv(key) + + val, err := strconv.ParseInt(str, 10, 64) + if err != nil { + return defaultValue + } + + return val +} + +// LocalEnvConfig is used to automatically call the InitLocalEnvConfig method using Dependency Injection +// So, if a func parameter or a struct field depends on LocalEnvConfig, when DI starts, it will call InitLocalEnvConfig as the LocalEnvConfig provider. +type LocalEnvConfig struct { + Initialized bool +} + +var ( + localEnvConfig *LocalEnvConfig + localEnvConfigOnce sync.Once +) + +// InitLocalEnvConfig load a .env file to set up local environment vars +// It's called once per application process. +func InitLocalEnvConfig() *LocalEnvConfig { + fmt.Println(console.Title("InitLocalEnvConfig")) + + envName := GetenvOrDefault("ENV_NAME", "local") + + if envName == "local" { + fmt.Printf("ENVIRONMENT NAME \u001B[31m(%s)\u001B[0m\n", envName) + localEnvConfigOnce.Do(func() { + if err := godotenv.Load(); err != nil { + fmt.Println("Skipping .env file. Current env ", envName) + + localEnvConfig = &LocalEnvConfig{ + Initialized: false, + } + } else { + fmt.Println("Env vars loaded from .env file on process", os.Getpid()) + + localEnvConfig = &LocalEnvConfig{ + Initialized: true, + } + } + }) + } + + fmt.Println(console.Line(console.DefaultLineSize)) + + return localEnvConfig +} + +// SetConfigFromEnvVars builds a struct by setting it fields values using the "var" tag +// Constraints: s any - must be an initialized pointer +// Supported types: String, Boolean, Int, Int8, Int16, Int32 and Int64. +func SetConfigFromEnvVars(s any) error { + v := reflect.ValueOf(s) + + t := v.Type() + if t.Kind() != reflect.Ptr { + return errors.New("s must be an pointer") + } + + e := t.Elem() + for i := 0; i < e.NumField(); i++ { + f := e.Field(i) + if tag, ok := f.Tag.Lookup("env"); ok { + values := strings.Split(tag, ",") + if len(values) > 0 { + fv := v.Elem().FieldByName(f.Name) + if fv.CanSet() { + switch k := fv.Kind(); k { + case reflect.Bool: + fv.SetBool(GetenvBoolOrDefault(values[0], false)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + fv.SetInt(GetenvIntOrDefault(values[0], 0)) + default: + fv.SetString(os.Getenv(values[0])) + } + } + } + } + } + + return nil +} + +// EnsureConfigFromEnvVars ensures that an interface will be settled using SetConfigFromEnvVars anyway. +func EnsureConfigFromEnvVars(s any) any { + if err := SetConfigFromEnvVars(s); err != nil { + panic(err) + } + + return s +} diff --git a/common/shell/ascii.sh b/common/shell/ascii.sh new file mode 100644 index 00000000..e50624fe --- /dev/null +++ b/common/shell/ascii.sh @@ -0,0 +1,32 @@ +source $PWD/common/shell/colors.sh + +border() { + local str="$*" # Put all arguments into single string + local len=${#str} + local i + for ((i = 0; i < len + 4; ++i)); do + printf '-' + done + printf "\n $str \n" + for ((i = 0; i < len + 4; ++i)); do + printf '-' + done + echo +} + +lineOk() { + local str="$1${green} โœ”๏ธ${normal}" + printf "${green}${bold}[ok]${normal} $str\n" +} + +lineError() { + local str="${red}$* โœ—${normal}" + printf "$str\n" +} + +title1() { + local str="$*" + printf "\n" + border "๐Ÿ“ ${bold}$str${normal}" + printf "\n" +} diff --git a/common/shell/colors.sh b/common/shell/colors.sh new file mode 100644 index 00000000..ab69818b --- /dev/null +++ b/common/shell/colors.sh @@ -0,0 +1,21 @@ +# check if stdout is a terminal... +if test -t 1; then + + # see if it supports colors... + ncolors=$(tput colors) + + if test -n "$ncolors" && test $ncolors -ge 8; then + bold="$(tput bold)" + underline="$(tput smul)" + standout="$(tput smso)" + normal="$(tput sgr0)" + black="$(tput setaf 0)" + red="$(tput setaf 1)" + green="$(tput setaf 2)" + yellow="$(tput setaf 3)" + blue="$(tput setaf 4)" + magenta="$(tput setaf 5)" + cyan="$(tput setaf 6)" + white="$(tput setaf 7)" + fi +fi \ No newline at end of file diff --git a/common/shell/logo.txt b/common/shell/logo.txt new file mode 100644 index 00000000..d1714cdc --- /dev/null +++ b/common/shell/logo.txt @@ -0,0 +1,7 @@ + __ __ _ _ + | \/ (_) __| | __ _ ____ + | |\/| | |/ _` |/ _` |_ / + | | | | | (_| | (_| |/ / + |_| |_|_|\__,_|\__,_/___| + + MIDAZ.IO ENGINEERING TEAM ๐Ÿš€ \ No newline at end of file diff --git a/common/stringUtils.go b/common/stringUtils.go new file mode 100644 index 00000000..00e0b5fb --- /dev/null +++ b/common/stringUtils.go @@ -0,0 +1,61 @@ +package common + +import ( + "bytes" + "strings" + "unicode" + + "golang.org/x/text/runes" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" +) + +// RemoveAccents removes accents of a given word and returns it +func RemoveAccents(word string) (string, error) { + t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) + + s, _, err := transform.String(t, word) + if err != nil { + return "", err + } + + return s, nil +} + +// RemoveSpaces removes spaces of a given word and returns it +func RemoveSpaces(word string) string { + rr := make([]rune, 0, len(word)) + + for _, r := range word { + if !unicode.IsSpace(r) { + rr = append(rr, r) + } + } + + return string(rr) +} + +// IsNilOrEmpty returns a boolean indicating if a *string is nil or empty. +// It's use TrimSpace so, a string " " and "" will be considered empty +func IsNilOrEmpty(s *string) bool { + return s == nil || strings.TrimSpace(*s) == "" +} + +// CamelToSnakeCase converts a given camelCase string to snake_case format. +func CamelToSnakeCase(str string) string { + var buffer bytes.Buffer + + for i, character := range str { + if unicode.IsUpper(character) { + if i > 0 { + buffer.WriteString("_") + } + + buffer.WriteRune(unicode.ToLower(character)) + } else { + buffer.WriteString(string(character)) + } + } + + return buffer.String() +} diff --git a/common/stringUtils_test.go b/common/stringUtils_test.go new file mode 100644 index 00000000..0ee6e8b8 --- /dev/null +++ b/common/stringUtils_test.go @@ -0,0 +1,91 @@ +package common + +import ( + "testing" + + "github.com/LerianStudio/midaz/common/mpointers" +) + +func Test_RemoveAccents(t *testing.T) { + want := "aaaaeeeiiioooouuu" + got, err := RemoveAccents("ร รกรฃรขรจรฉรชรฌรญรฎรฒรณรดรตรนรบรป") + if err != nil { + t.Error(err) + return + } + if got != want { + t.Errorf("Want: %s, got: %s", want, got) + } +} + +func Test_RemoveSpaces(t *testing.T) { + want := "foobar" + got := RemoveSpaces("foo bar") + if got != want { + t.Errorf("Want: %s, got: %s", want, got) + } +} + +func Test_IsEmpty(t *testing.T) { + m := map[*string]bool{ + mpointers.String("foo"): false, + mpointers.String(""): true, + mpointers.String(" "): true, + mpointers.String(" "): true, + mpointers.String(" bar "): false, + nil: true, + } + for str, want := range m { + got := IsNilOrEmpty(str) + if want != got { + value := "nil" + if str != nil { + value = *str + } + t.Errorf("Want: %v, got: %v to value \"%v\"", want, IsNilOrEmpty(str), value) + } + } +} + +func TestCamelToSnakeCase(t *testing.T) { + cases := []struct { + name string + input string + expected string + }{ + { + name: "EmptyString", + input: "", + expected: "", + }, + { + name: "AllLowerCase", + input: "goland", + expected: "goland", + }, + { + name: "AllUpperCase", + input: "GOLAND", + expected: "g_o_l_a_n_d", + }, + { + name: "LeadingUpperCase", + input: "GoLand", + expected: "go_land", + }, + { + name: "MixedUpperLowerCase", + input: "GoLand2023", + expected: "go_land2023", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := CamelToSnakeCase(tc.input) + if result != tc.expected { + t.Errorf("Expected %s but got %s", tc.expected, result) + } + }) + } +} diff --git a/common/utils.go b/common/utils.go new file mode 100644 index 00000000..fe185347 --- /dev/null +++ b/common/utils.go @@ -0,0 +1,47 @@ +package common + +import ( + "strconv" +) + +// Contains checks if an item is in a slice. This function uses type parameters to work with any slice type. +func Contains[T comparable](slice []T, item T) bool { + for _, s := range slice { + if s == item { + return true + } + } + + return false +} + +// CheckMetadataKeyAndValueLength check the length of key and value to a limit pass by on field limit +func CheckMetadataKeyAndValueLength(limit int, metadata map[string]any) error { + for k, v := range metadata { + if len(k) > limit { + return ValidationError{ + Message: "Error the key: " + k + " must be less than 100 characters", + } + } + + var value string + switch t := v.(type) { + case int: + value = strconv.Itoa(t) + case float64: + value = strconv.FormatFloat(t, 'f', -1, 64) + case string: + value = t + case bool: + value = strconv.FormatBool(t) + } + + if len(value) > limit { + return ValidationError{ + Message: "Error the value: " + value + " must be less than 100 characters", + } + } + } + + return nil +} diff --git a/components/auth/.env.example b/components/auth/.env.example new file mode 100644 index 00000000..c20ce805 --- /dev/null +++ b/components/auth/.env.example @@ -0,0 +1,29 @@ +# AUTH +# Kratos +ENV_NAME=production +KRATOS_DB_USER=kratos +KRATOS_DB_PASSWORD=kratos +KRATOS_DB_NAME=kratos +KRATOS_DB_PORT=5432 +KRATOS_ADMIN_URL=http://kratos:4434/ +KRATOS_PUBLIC_URL=http://kratos:4433/ +# generate a random secret to replace this example +KRATOS_COOKIE_SECRET= #< to create a secreat you may run this command to generate a new secret: pwgen -s -n 30 7 > +# generate a random secret to replace this example +KRATOS_CIPHER_SECRET= #< to create a secreat you may run this command to generate a new secret: pwgen -s -n 30 7 > +SMTP_USER=test +SMTP_PASSWORD=test +SMTP_ADDRESS=mailslurper +SMTP_PORT=1025 +# Hydra +HYDRA_ADDRESS=http://hydra:4445/ +HYDRA_DB_USER=hydra +HYDRA_DB_PASSWORD=hydra +HYDRA_DB_NAME=hydra +HYDRA_DB_PORT=5432 +# generate a random secret to replace this example +HYDRA_SYSTEM_SECRET= #< to create a secreat you may run this command to generate a new secret: pwgen -s -n 30 7 > +# generate a random secret to replace this example +HYDRA_COOKIE_SECRET= #< to create a secreat you may run this command to generate a new secret: pwgen -s -n 30 7 > +# generate a random secret to replace this example +HYDRA_PAIRWISE_SALT= #< to create a secreat you may run this command to generate a new secret: pwgen -s -n 30 7 > \ No newline at end of file diff --git a/components/auth/Makefile b/components/auth/Makefile new file mode 100644 index 00000000..f20e8350 --- /dev/null +++ b/components/auth/Makefile @@ -0,0 +1,62 @@ +AUTH_DIR := ./components/auth + +.PHONY: auth + +build: + ./make.sh "build" + +help: + @echo "Management commands" + @echo "" + @echo "Usage:" + @echo " ## Root Commands" + @echo " make build Build all project services." + @echo " make test Run tests on all projects." + @echo " make clean Clean the directory tree of produced artifacts." + @echo " make lint Run static code analysis (lint)." + @echo " make format Run code formatter." + @echo " make checkEnvs Check if github hooks are instaled and secret env on files are not exposed." + @echo " make gen Generates all project code to connect its components using Wire." + @echo "" + @echo " ## Utility Commands" + @echo " make setup-git-hooks Setup git hooks." + @echo "" + +test: + go test -v ./... ./... + +cover: + go test -cover ./... + +clean: + ./make.sh "clean" + +lint: + ./make.sh "lint" + +format: + ./make.sh "format" + +check-logs: + ./make.sh "checkLogs" + +check-tests: + ./make.sh "checkTests" + +setup-git-hooks: + ./make.sh "setupGitHooks" + +goreleaser: + goreleaser release --snapshot --skip-publish --rm-dist + +tidy: + go mod tidy + +sec: + gosec ./... + +ledger: + $(MAKE) -C $(AUTH_DIR) restart + +gen: + $(MAKE) -C $(AUTH_DIR) gen \ No newline at end of file diff --git a/components/auth/docker-compose.yml b/components/auth/docker-compose.yml new file mode 100644 index 00000000..81aaa317 --- /dev/null +++ b/components/auth/docker-compose.yml @@ -0,0 +1,136 @@ +x-postgres-common: + &postgres-common + image: postgres:16-alpine + restart: always + env_file: + - .env + networks: + - app-tier + +services: + kratos-db: + <<: *postgres-common + container_name: kratos-db + ports: + - "5435:5432" + environment: + - POSTGRES_USER=${KRATOS_DB_USER} + - POSTGRES_PASSWORD=${KRATOS_DB_PASSWORD} + - POSTGRES_DB=${KRATOS_DB_NAME} + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ${KRATOS_DB_USER} -d ${KRATOS_DB_NAME}" ] + interval: 10s + timeout: 5s + retries: 5 + + hydra-db: + <<: *postgres-common + container_name: hydra-db + ports: + - "5434:5432" + environment: + - POSTGRES_USER=${HYDRA_DB_USER} + - POSTGRES_PASSWORD=${HYDRA_DB_PASSWORD} + - POSTGRES_DB=${HYDRA_DB_NAME} + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ${HYDRA_DB_USER} -d ${HYDRA_DB_NAME}" ] + interval: 10s + timeout: 5s + retries: 5 + + mailslurper: #simulate email verification steps + image: oryd/mailslurper:latest-smtps + ports: + - "4436:4436" + - "4437:4437" + networks: + - app-tier + + kratos-migrate: + depends_on: + - kratos-db + container_name: kratos-migrate + image: oryd/kratos:v1.1.0 + env_file: + - .env + environment: + - DSN=postgres://${KRATOS_DB_USER}:${KRATOS_DB_PASSWORD}@kratos-db:${KRATOS_DB_PORT}/${KRATOS_DB_NAME}?sslmode=disable&max_conns=20&max_idle_conns=4 + command: migrate sql -e --yes + networks: + - app-tier + + hydra-migrate: + depends_on: + - hydra-db + - kratos-migrate + container_name: hydra-migrate + image: oryd/hydra:v2.2.0 + env_file: + - .env + environment: + - DSN=postgres://${HYDRA_DB_USER}:${HYDRA_DB_PASSWORD}@hydra-db:${HYDRA_DB_PORT}/${HYDRA_DB_NAME}?sslmode=disable&max_conns=20&max_idle_conns=4 + - SECRETS_SYSTEM=${HYDRA_SYSTEM_SECRET} + restart: on-failure + command: migrate sql -e --yes + networks: + - app-tier + + hydra: + container_name: hydra + image: oryd/hydra:v2.2.0 + env_file: + - .env + environment: + - DSN=postgres://${HYDRA_DB_USER}:${HYDRA_DB_PASSWORD}@hydra-db:${HYDRA_DB_PORT}/${HYDRA_DB_NAME}?sslmode=disable&max_conns=20&max_idle_conns=4 + - OIDC_SUBJECT_IDENTIFIERS_PAIRWISE_SALT=${HYDRA_PAIRWISE_SALT} + - URLS_IDENTITY_PROVIDER_PUBLICURL=${KRATOS_PUBLIC_URL} + - URLS_IDENTITY_PROVIDER_URL=${KRATOS_ADMIN_URL} + - SECRETS_COOKIE=${HYDRA_COOKIE_SECRET} + - SECRETS_SYSTEM=${HYDRA_SYSTEM_SECRET} + command: serve -c /etc/hydra/config/hydra.yml all --dev + depends_on: + - hydra-db + - hydra-migrate + ports: + - "4444:4444" #public port + - "4445:4445" #admin port + - "5555:5555" #hydra token user port + restart: on-failure + volumes: + - type: bind + source: ../../config/auth + target: /etc/hydra/config + networks: + - app-tier + + kratos: + container_name: kratos + image: oryd/kratos:v1.1.0 + env_file: + - .env + environment: + - DSN=postgres://${KRATOS_DB_USER}:${KRATOS_DB_PASSWORD}@kratos-db:${KRATOS_DB_PORT}/${KRATOS_DB_NAME}?sslmode=disable&max_conns=20&max_idle_conns=4 + - OAUTH2_PROVIDER_URL=${HYDRA_ADDRESS} + - OAUTH2_PROVIDER_OVERRIDE_RETURN_TO=true + - SECRETS_COOKIE=${KRATOS_COOKIE_SECRET} + - SECRETS_CIPHER=${KRATOS_CIPHER_SECRET} + - COURIER_SMTP_CONNECTION_URI=smtps://${SMTP_USER}:${SMTP_PASSWORD}@${SMTP_ADDRESS}:${SMTP_PORT}/?skip_ssl_verify=true + command: serve -c /etc/kratos/config/kratos.yml --dev --watch-courier + depends_on: + - kratos-db + - kratos-migrate + ports: + - "4433:4433" #public port + - "4434:4434" #admin port + volumes: + - type: bind + source: ../../config/auth + target: /etc/kratos/config + - type: bind + source: ../../config/identity-schemas + target: /etc/kratos/identity-schemas + networks: + - app-tier + +networks: + app-tier: \ No newline at end of file diff --git a/components/ledger/.env.example b/components/ledger/.env.example new file mode 100644 index 00000000..cd4e9407 --- /dev/null +++ b/components/ledger/.env.example @@ -0,0 +1,23 @@ +ENV_NAME=production +SERVER_PORT=3000 +SERVER_ADDRESS=:${SERVER_PORT} +MONGO_HOST=mongodb +MONGO_NAME=ledger +MONGO_USER=midaz +MONGO_PASSWORD=leriand +MONGO_PORT=27017 +REDIS_PORT=6379 +DB_HOST=primary-ledger +DB_USER=midaz +DB_NAME=ledger +DB_PASSWORD=leriand +DB_PORT=5432 +DB_REPLICA_HOST=replica-ledger +DB_REPLICA_USER=midaz +DB_REPLICA_NAME=ledger +DB_REPLICA_PASSWORD=leriand +DB_REPLICA_PORT=5433 +REPLICATION_USER=replicator +REPLICATION_PASSWORD=replicator_password +USER_EXECUTE_COMMAND=postgres +LOG_LEVEL=debug \ No newline at end of file diff --git a/components/ledger/Dockerfile b/components/ledger/Dockerfile new file mode 100644 index 00000000..4f1c4997 --- /dev/null +++ b/components/ledger/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.21-alpine AS builder + +WORKDIR /ledger-app + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w -extldflags "-static"' -o /app components/ledger/internal/main.go + +FROM gcr.io/distroless/static-debian12 + +COPY --chown=nonroot:nonroot --from=builder /app /app + +# Copy the migrations directory. +COPY --chown=nonroot:nonroot --from=builder /ledger-app/components/ledger/migrations /components/ledger/migrations + +USER nonroot + +EXPOSE 3000 + +ENTRYPOINT ["/app"] \ No newline at end of file diff --git a/components/ledger/Makefile b/components/ledger/Makefile new file mode 100644 index 00000000..5997a042 --- /dev/null +++ b/components/ledger/Makefile @@ -0,0 +1,73 @@ +service_name := ledger-service +bin_dir := ./.bin +artifacts_dir := ./artifacts + +$(shell mkdir -p $(artifacts_dir)) + +.PHONY: info gen run test cover-html tidy help build up start down destroy stop restart logs logs-api ps login-timescale login-api db-shell + +# Display available commands +info: + @echo "Available commands:" + @echo " make wire" + @echo " make run" + @echo " make test" + +gen: + @go generate ./... + +run: + @go run internal/main.go .env + +test: + @go test -v ./... + +cover-html: + @go tool cover -html=$(artifacts_dir)/coverage.out -o $(artifacts_dir)/coverage.html + +tidy: + @go mod tidy + +# Help command to list other commands +help: + @make -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | \ + awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | \ + sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' + +# Docker Compose Commands +build: + @docker-compose -f docker-compose.yml build $(c) + +up: + @docker-compose -f docker-compose.yml up $(c) + +start: + @docker-compose -f docker-compose.yml start $(c) + +down: + @docker-compose -f docker-compose.yml down $(c) + +destroy: + @docker-compose -f docker-compose.yml down -v $(c) + +stop: + @docker-compose -f docker-compose.yml stop $(c) + +restart: + docker-compose -f docker-compose.yml down $(c) && \ + docker-compose -f docker-compose.yml up -d $(c) + +logs: + @docker-compose -f docker-compose.yml logs --tail=100 -f $(c) + +logs-api: + @docker-compose -f docker-compose.yml logs --tail=100 -f ledger + +ps: + @docker-compose -f docker-compose.yml ps + +ledger-api: + @docker-compose -f docker-compose.yml exec ledger /bin/bash + +db-shell: + @docker-compose -f docker-compose.yml exec ledger psql -Upostgres diff --git a/components/ledger/api/v1.yml b/components/ledger/api/v1.yml new file mode 100644 index 00000000..34e2df36 --- /dev/null +++ b/components/ledger/api/v1.yml @@ -0,0 +1,834 @@ +openapi: 3.0.0 +info: + title: Ledger API + description: API to manage ledger, accounts, instruments, portfolios, organizations, and products. + version: "1.0" +servers: + - url: "http://localhost:8080/v1" +tags: + - name: Organizations + - name: Ledgers + - name: Accounts + - name: Instruments + - name: Portfolios + - name: Products + +paths: + # Organizations Paths + /organizations: + get: + tags: + - Organizations + summary: Retrieve all organizations + responses: + "200": + description: List of organizations + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Organization" + post: + tags: + - Organizations + summary: Create an organization + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/OrganizationInput" + responses: + "201": + description: Organization created + content: + application/json: + schema: + $ref: "#/components/schemas/Organization" + "/organizations/{id}": + get: + tags: + - Organizations + summary: Get an organization by ID + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Detailed organization data + content: + application/json: + schema: + $ref: "#/components/schemas/Organization" + patch: + tags: + - Organizations + summary: Update an organization + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/OrganizationInput" + responses: + "200": + description: Organization updated + delete: + tags: + - Organizations + summary: Delete an organization + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "204": + description: Organization deleted + + # Ledgers Paths + /ledgers: + get: + tags: + - Ledgers + summary: Retrieve all ledgers + responses: + "200": + description: List of ledgers + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Ledger" + post: + tags: + - Ledgers + summary: Create a ledger + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/LedgerInput" + responses: + "201": + description: Ledger created + content: + application/json: + schema: + $ref: "#/components/schemas/Ledger" + "/ledgers/{id}": + get: + tags: + - Ledgers + summary: Get a ledger by ID + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Detailed ledger data + content: + application/json: + schema: + $ref: "#/components/schemas/Ledger" + patch: + tags: + - Ledgers + summary: Update a ledger + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/LedgerInput" + responses: + "200": + description: Ledger updated + delete: + tags: + - Ledgers + summary: Delete a ledger + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "204": + description: Ledger deleted + + # Accounts Paths + /accounts: + get: + tags: + - Accounts + summary: Retrieve all accounts + responses: + "200": + description: List of accounts + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Account" + post: + tags: + - Accounts + summary: Create an account + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AccountInput" + responses: + "201": + description: Account created + content: + application/json: + schema: + $ref: "#/components/schemas/Account" + "/accounts/{id}": + get: + tags: + - Accounts + summary: Get an account by ID + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Detailed account data + content: + application/json: + schema: + $ref: "#/components/schemas/Account" + patch: + tags: + - Accounts + summary: Update an account + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AccountInput" + responses: + "200": + description: Account updated + delete: + tags: + - Accounts + summary: Delete an account + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "204": + description: Account deleted + + # Instruments Paths + /instruments: + get: + tags: + - Instruments + summary: Retrieve all instruments + responses: + "200": + description: List of instruments + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Instrument" + post: + tags: + - Instruments + summary: Create an instrument + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InstrumentInput" + responses: + "201": + description: Instrument created + content: + application/json: + schema: + $ref: "#/components/schemas/Instrument" + "/instruments/{id}": + get: + tags: + - Instruments + summary: Get an instrument by ID + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Detailed instrument data + content: + application/json: + schema: + $ref: "#/components/schemas/Instrument" + patch: + tags: + - Instruments + summary: Update an instrument + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InstrumentInput" + responses: + "200": + description: Instrument updated + delete: + tags: + - Instruments + summary: Delete an instrument + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "204": + description: Instrument deleted + + # Portfolios Paths + /portfolios: + get: + tags: + - Portfolios + summary: Retrieve all portfolios + responses: + "200": + description: List of portfolios + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Portfolio" + post: + tags: + - Portfolios + summary: Create a portfolio + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PortfolioInput" + responses: + "201": + description: Portfolio created + content: + application/json: + schema: + $ref: "#/components/schemas/Portfolio" + "/portfolios/{id}": + get: + tags: + - Portfolios + summary: Get a portfolio by ID + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Detailed portfolio data + content: + application/json: + schema: + $ref: "#/components/schemas/Portfolio" + patch: + tags: + - Portfolios + summary: Update a portfolio + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PortfolioInput" + responses: + "200": + description: Portfolio updated + delete: + tags: + - Portfolios + summary: Delete a portfolio + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "204": + description: Portfolio deleted + + # Products Paths + /products: + get: + tags: + - Products + summary: Retrieve all products + responses: + "200": + description: List of products + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Product" + post: + tags: + - Products + summary: Create a product + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ProductInput" + responses: + "201": + description: Product created + content: + application/json: + schema: + $ref: "#/components/schemas/Product" + "/products/{id}": + get: + tags: + - Products + summary: Get a product by ID + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Detailed product data + content: + application/json: + schema: + $ref: "#/components/schemas/Product" + patch: + tags: + - Products + summary: Update a product + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ProductInput" + responses: + "200": + description: Product updated + delete: + tags: + - Products + summary: Delete a product + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "204": + description: Product deleted + +components: + schemas: + # Organization Schema + Organization: + type: object + properties: + id: + type: string + legalName: + type: string + doingBusinessAs: + type: string + nullable: true + legalDocument: + type: string + address: + $ref: "#/components/schemas/Address" + statusCode: + type: string + statusDescription: + type: string + nullable: true + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + deletedAt: + type: string + format: date-time + nullable: true + metadata: + type: object + additionalProperties: true + # Address Schema + Address: + type: object + properties: + line1: + type: string + line2: + type: string + nullable: true + neighborhood: + type: string + zipCode: + type: string + city: + type: string + state: + type: string + country: + type: string + # Organization Input Schema + OrganizationInput: + type: object + properties: + legalName: + type: string + doingBusinessAs: + type: string + nullable: true + legalDocument: + type: string + address: + $ref: "#/components/schemas/Address" + metadata: + type: object + additionalProperties: true + # Ledger Schema + Ledger: + type: object + properties: + id: + type: string + name: + type: string + statusCode: + type: string + statusDescription: + type: string + nullable: true + organizationId: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + deletedAt: + type: string + format: date-time + nullable: true + metadata: + type: object + additionalProperties: true + # Ledger Input Schema + LedgerInput: + type: object + properties: + name: + type: string + statusCode: + type: string + statusDescription: + type: string + organizationId: + type: string + metadata: + type: object + additionalProperties: true + # Account Schema + Account: + type: object + properties: + id: + type: string + name: + type: string + nullable: true + portfolioId: + type: string + instrumentCode: + type: string + availableBalance: + type: number + nullable: true + onHoldBalance: + type: number + nullable: true + statusCode: + type: string + statusDescription: + type: string + nullable: true + allowSending: + type: boolean + allowReceiving: + type: boolean + ledgerId: + type: string + alias: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + deletedAt: + type: string + format: date-time + nullable: true + metadata: + type: object + additionalProperties: true + # Account Input Schema + AccountInput: + type: object + properties: + name: + type: string + nullable: true + instrumentCode: + type: string + statusCode: + type: string + allowSending: + type: boolean + allowReceiving: + type: boolean + metadata: + type: object + additionalProperties: true + # Instrument Schema + Instrument: + type: object + properties: + id: + type: string + name: + type: string + types: + type: string + code: + type: string + statusCode: + type: string + statusDescription: + type: string + nullable: true + ledgerId: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + deletedAt: + type: string + format: date-time + nullable: true + metadata: + type: object + additionalProperties: true + # Instrument Input Schema + InstrumentInput: + type: object + properties: + name: + type: string + types: + type: string + code: + type: string + statusCode: + type: string + statusDescription: + type: string + nullable: true + metadata: + type: object + additionalProperties: true + # Portfolio Schema + Portfolio: + type: object + properties: + id: + type: string + name: + type: string + statusCode: + type: string + statusDescription: + type: string + nullable: true + accounts: + type: array + items: + $ref: "#/components/schemas/Account" + entityId: + type: string + nullable: true + ledgerID: + type: string + productId: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + deletedAt: + type: string + format: date-time + nullable: true + metadata: + type: object + additionalProperties: true + # Portfolio Input Schema + PortfolioInput: + type: object + properties: + name: + type: string + statusCode: + type: string + statusDescription: + type: string + entityId: + type: string + ledgerID: + type: string + productId: + type: string + metadata: + type: object + additionalProperties: true + # Product Schema + Product: + type: object + properties: + id: + type: string + name: + type: string + status: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + deletedAt: + type: string + format: date-time + nullable: true + ledgerID: + type: string + metadata: + type: object + additionalProperties: true + # Product Input Schema + ProductInput: + type: object + properties: + name: + type: string + status: + type: string + ledgerID: + type: string + metadata: + type: object + additionalProperties: true diff --git a/components/ledger/diagram.png b/components/ledger/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..f064b33864a3facbe1a95d4a6b372f090f6b962f GIT binary patch literal 183422 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 D*Sdk* literal 0 HcmV?d00001 diff --git a/components/ledger/docker-compose.yml b/components/ledger/docker-compose.yml new file mode 100644 index 00000000..984fc63b --- /dev/null +++ b/components/ledger/docker-compose.yml @@ -0,0 +1,134 @@ +x-postgres-ledger-common: + &postgres-ledger-common + image: postgres:16-alpine + user: ${USER_EXECUTE_COMMAND} + restart: always + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - app-tier + +x-mongodb-common: + &mongodb-common + image: mongo:latest + restart: always + healthcheck: + test: echo 'db.runCommand("ping").ok' + interval: 10s + timeout: 5s + retries: 5 + networks: + - app-tier + +x-redis-common: + &redis-common + image: redis:latest + env_file: + - .env + networks: + - app-tier + +services: + ledger: + build: + context: ../../ + dockerfile: ./components/ledger/Dockerfile + env_file: + - .env + links: + - redis + - mongodb + - primary-ledger + - replica-ledger + ports: + - ${SERVER_PORT}:${SERVER_PORT} + volumes: + - .:/usr/src/app + depends_on: + redis: + condition: service_started + mongodb: + condition: service_healthy + primary-ledger: + condition: service_healthy + replica-ledger: + condition: service_healthy + networks: + - app-tier + + mongodb: + <<: *mongodb-common + container_name: mongodb_ledger + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} + ports: + - ${MONGO_PORT}:${MONGO_PORT} + volumes: + - mongodb_data_container:/data/db + + redis: + <<: *redis-common + container_name: redis_ledger + ports: + - ${REDIS_PORT}:${REDIS_PORT} + + primary-ledger: + <<: *postgres-ledger-common + container_name: primary-ledger + ports: + - ${DB_PORT}:${DB_PORT} + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_DB: ${DB_NAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_HOST_AUTH_METHOD: "scram-sha-256\nhost replication all 0.0.0.0/0 md5" + POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256" + command: | + postgres + -c wal_level=replica + -c hot_standby=on + -c max_wal_senders=10 + -c max_replication_slots=10 + -c hot_standby_feedback=on + volumes: + - ./setup/00_init.sql:/docker-entrypoint-initdb.d/00_init.sql + + replica-ledger: + <<: *postgres-ledger-common + container_name: replica-ledger + ports: + - ${DB_REPLICA_PORT}:${DB_REPLICA_PORT} + environment: + PGUSER: ${REPLICATION_USER} + PGPASSWORD: ${REPLICATION_PASSWORD} + command: | + bash -c " + until pg_basebackup --pgdata=/var/lib/postgresql/data -R --slot=replication_slot --host=primary-ledger --port=${DB_PORT} + do + echo 'Waiting for primary-ledger to connect...' + sleep 1s + done + echo 'Backup done..., starting replica-ledger...' + chmod 0700 /var/lib/postgresql/data + # Ensure the port is set to 5433 for the replica + sed -i 's/^#port.*/port = ${DB_REPLICA_PORT}/' /var/lib/postgresql/data/postgresql.conf + exec postgres -c config_file=/var/lib/postgresql/data/postgresql.conf + " + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ${DB_REPLICA_USER} -d ${DB_REPLICA_NAME} -p ${DB_REPLICA_PORT}" ] + interval: 10s + timeout: 5s + retries: 5 + depends_on: + primary-ledger: + condition: service_healthy + +volumes: + mongodb_data_container: + +networks: + app-tier: \ No newline at end of file diff --git a/components/ledger/internal/adapters/database/mongodb/metadata.mongodb.go b/components/ledger/internal/adapters/database/mongodb/metadata.mongodb.go new file mode 100644 index 00000000..005931a0 --- /dev/null +++ b/components/ledger/internal/adapters/database/mongodb/metadata.mongodb.go @@ -0,0 +1,177 @@ +package mongodb + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mmongo" + m "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// MetadataMongoDBRepository is a MongoDD-specific implementation of the MetadataRepository. +type MetadataMongoDBRepository struct { + connection *mmongo.MongoConnection + Database string +} + +// NewMetadataMongoDBRepository returns a new instance of MetadataMongoDBLRepository using the given MongoDB connection. +func NewMetadataMongoDBRepository(mc *mmongo.MongoConnection) *MetadataMongoDBRepository { + r := &MetadataMongoDBRepository{ + connection: mc, + Database: mc.Database, + } + if _, err := r.connection.GetDB(context.Background()); err != nil { + panic("Failed to connect mongodb") + } + + return r +} + +// Create inserts a new metadata entity into mongodb. +func (mmr *MetadataMongoDBRepository) Create(ctx context.Context, collection string, metadata *m.Metadata) error { + db, err := mmr.connection.GetDB(ctx) + if err != nil { + return err + } + + coll := db.Database(strings.ToLower(mmr.Database)).Collection(strings.ToLower(collection)) + record := &m.MetadataMongoDBModel{} + + if err := record.FromEntity(metadata); err != nil { + return err + } + + insertResult, err := coll.InsertOne(ctx, record) + if err != nil { + return err + } + + fmt.Println("Inserted a document: ", insertResult.InsertedID) + + return nil +} + +// FindList retrieves metadata from the mongodb all metadata or a list by specify metadata. +func (mmr *MetadataMongoDBRepository) FindList(ctx context.Context, collection string, filter any) ([]*m.Metadata, error) { + db, err := mmr.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + coll := db.Database(strings.ToLower(mmr.Database)).Collection(strings.ToLower(collection)) + + opts := options.Find() + + cur, err := coll.Find(ctx, filter, opts) + if err != nil { + return nil, err + } + + var meta []*m.MetadataMongoDBModel + + for cur.Next(ctx) { + var record m.MetadataMongoDBModel + if err := cur.Decode(&record); err != nil { + return nil, err + } + + meta = append(meta, &record) + } + + if err := cur.Err(); err != nil { + return nil, err + } + + if err := cur.Close(ctx); err != nil { + return nil, err + } + + metadata := make([]*m.Metadata, 0, len(meta)) + for i := range meta { + metadata = append(metadata, meta[i].ToEntity()) + } + + return metadata, nil +} + +// FindByEntity retrieves a metadata from the mongodb using the provided entity_id. +func (mmr *MetadataMongoDBRepository) FindByEntity(ctx context.Context, collection, id string) (*m.Metadata, error) { + db, err := mmr.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + coll := db.Database(strings.ToLower(mmr.Database)).Collection(strings.ToLower(collection)) + + var record m.MetadataMongoDBModel + if err = coll.FindOne(ctx, bson.M{"entity_id": id}).Decode(&record); err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, nil + } + + return nil, err + } + + return record.ToEntity(), nil +} + +// Update an metadata entity into mongodb. +func (mmr *MetadataMongoDBRepository) Update(ctx context.Context, collection, id string, metadata map[string]any) error { + db, err := mmr.connection.GetDB(ctx) + if err != nil { + return err + } + + coll := db.Database(strings.ToLower(mmr.Database)).Collection(strings.ToLower(collection)) + opts := options.Update().SetUpsert(true) + filter := bson.M{"entity_id": id} + update := bson.D{{Key: "$set", Value: bson.D{{Key: "metadata", Value: metadata}, {Key: "updated_at", Value: time.Now()}}}} + + updated, err := coll.UpdateOne(ctx, filter, update, opts) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return common.EntityNotFoundError{ + EntityType: collection, + Err: err, + } + } + + return err + } + + if updated.ModifiedCount > 0 { + fmt.Println("updated a document with entity_id: ", id) + } + + return nil +} + +// Delete an metadata entity into mongodb. +func (mmr *MetadataMongoDBRepository) Delete(ctx context.Context, collection, id string) error { + db, err := mmr.connection.GetDB(ctx) + if err != nil { + return err + } + + opts := options.Delete() + + coll := db.Database(strings.ToLower(mmr.Database)).Collection(strings.ToLower(collection)) + + deleted, err := coll.DeleteOne(ctx, bson.D{{Key: "entity_id", Value: id}}, opts) + if err != nil { + return err + } + + if deleted.DeletedCount > 0 { + fmt.Println("deleted a document with entity_id: ", id) + } + + return nil +} diff --git a/components/ledger/internal/adapters/database/postgres/account.postgresql.go b/components/ledger/internal/adapters/database/postgres/account.postgresql.go new file mode 100644 index 00000000..3f3dce4a --- /dev/null +++ b/components/ledger/internal/adapters/database/postgres/account.postgresql.go @@ -0,0 +1,353 @@ +package postgres + +import ( + "context" + "database/sql" + "errors" + "reflect" + "strconv" + "strings" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mpostgres" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + "github.com/google/uuid" + "github.com/lib/pq" +) + +// AccountPostgreSQLRepository is a Postgresql-specific implementation of the AccountRepository. +type AccountPostgreSQLRepository struct { + connection *mpostgres.PostgresConnection +} + +// NewAccountPostgreSQLRepository returns a new instance of AccountPostgreSQLRepository using the given Postgres connection. +func NewAccountPostgreSQLRepository(pc *mpostgres.PostgresConnection) *AccountPostgreSQLRepository { + c := &AccountPostgreSQLRepository{ + connection: pc, + } + + _, err := c.connection.GetDB(context.Background()) + if err != nil { + panic("Failed to connect database") + } + + return c +} + +// Create a new account entity into Postgresql and returns it. +func (r *AccountPostgreSQLRepository) Create(ctx context.Context, account *a.Account) (*a.Account, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &a.AccountPostgreSQLModel{} + record.FromEntity(account) + + 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 + ) + RETURNING *`, + record.ID, + record.Name, + record.ParentAccountID, + record.EntityID, + record.InstrumentCode, + record.OrganizationID, + record.LedgerID, + record.PortfolioID, + record.ProductID, + record.AvailableBalance, + record.OnHoldBalance, + record.BalanceScale, + record.Status, + record.StatusDescription, + record.AllowSending, + record.AllowReceiving, + record.Alias, + record.Type, + record.CreatedAt, + record.UpdatedAt, + record.DeletedAt, + ) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(a.Account{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return record.ToEntity(), nil +} + +// FindAll retrieves an Account entities from the database. +func (r *AccountPostgreSQLRepository) FindAll(ctx context.Context, organizationID, ledgerID, portfolioID uuid.UUID) ([]*a.Account, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var accounts []*a.Account + + rows, err := db.QueryContext(ctx, "SELECT * FROM account WHERE organization_id = $1 AND ledger_id = $2 AND portfolio_id = $3 AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID, portfolioID) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var acc a.AccountPostgreSQLModel + if err := rows.Scan( + &acc.ID, + &acc.Name, + &acc.ParentAccountID, + &acc.EntityID, + &acc.InstrumentCode, + &acc.OrganizationID, + &acc.LedgerID, + &acc.PortfolioID, + &acc.ProductID, + &acc.AvailableBalance, + &acc.OnHoldBalance, + &acc.BalanceScale, + &acc.Status, + &acc.StatusDescription, + &acc.AllowSending, + &acc.AllowReceiving, + &acc.Alias, + &acc.Type, + &acc.CreatedAt, + &acc.UpdatedAt, + &acc.DeletedAt, + ); err != nil { + return nil, err + } + + accounts = append(accounts, acc.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return accounts, nil +} + +// Find retrieves an Account entity from the database using the provided ID. +func (r *AccountPostgreSQLRepository) Find(ctx context.Context, organizationID, ledgerID, portfolioID, id uuid.UUID) (*a.Account, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + account := &a.AccountPostgreSQLModel{} + + row := db.QueryRowContext(ctx, "SELECT * FROM account WHERE organization_id = $1 AND ledger_id = $2 AND portfolio_id = $3 AND id = $4 AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID, portfolioID, id) + if err := row.Scan( + &account.ID, + &account.Name, + &account.ParentAccountID, + &account.EntityID, + &account.InstrumentCode, + &account.OrganizationID, + &account.LedgerID, + &account.PortfolioID, + &account.ProductID, + &account.AvailableBalance, + &account.OnHoldBalance, + &account.BalanceScale, + &account.Status, + &account.StatusDescription, + &account.AllowSending, + &account.AllowReceiving, + &account.Alias, + &account.Type, + &account.CreatedAt, + &account.UpdatedAt, + &account.DeletedAt, + ); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(a.Account{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil, err + } + + return account.ToEntity(), nil +} + +// ListByIDs retrieves Accounts entities from the database using the provided IDs. +func (r *AccountPostgreSQLRepository) ListByIDs(ctx context.Context, organizationID, ledgerID, portfolioID uuid.UUID, ids []uuid.UUID) ([]*a.Account, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var accounts []*a.Account + + rows, err := db.QueryContext(ctx, "SELECT * FROM account WHERE organization_id = $1 AND ledger_id = $2 AND portfolio_id = $3 AND id = ANY($4) AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID, portfolioID, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var acc a.AccountPostgreSQLModel + if err := rows.Scan( + &acc.ID, + &acc.Name, + &acc.ParentAccountID, + &acc.EntityID, + &acc.InstrumentCode, + &acc.OrganizationID, + &acc.LedgerID, + &acc.PortfolioID, + &acc.ProductID, + &acc.AvailableBalance, + &acc.OnHoldBalance, + &acc.BalanceScale, + &acc.Status, + &acc.StatusDescription, + &acc.AllowSending, + &acc.AllowReceiving, + &acc.Alias, + &acc.Type, + &acc.CreatedAt, + &acc.UpdatedAt, + &acc.DeletedAt, + ); err != nil { + return nil, err + } + + accounts = append(accounts, acc.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return accounts, nil +} + +// Update an Account entity into Postgresql and returns the Account updated. +func (r *AccountPostgreSQLRepository) Update(ctx context.Context, organizationID, ledgerID, portfolioID, id uuid.UUID, account *a.Account) (*a.Account, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &a.AccountPostgreSQLModel{} + record.FromEntity(account) + + var updates []string + + var args []any + + if account.Name != "" { + updates = append(updates, "name = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Name) + } + + if !account.Status.IsEmpty() { + updates = append(updates, "status = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Status) + + updates = append(updates, "status_description = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.StatusDescription) + } + + if account.Alias != "" { + updates = append(updates, "alias = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Alias) + } + + if account.AllowSending != record.AllowSending { + updates = append(updates, "allow_sending = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.AllowSending) + } + + if account.AllowReceiving != record.AllowReceiving { + updates = append(updates, "allow_receiving = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.AllowReceiving) + } + + if account.ProductID != "" { + updates = append(updates, "product_id = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.ProductID) + } + + record.UpdatedAt = time.Now() + + updates = append(updates, "updated_at = $"+strconv.Itoa(len(args)+1)) + + args = append(args, record.UpdatedAt, organizationID, ledgerID, portfolioID, id) + + query := `UPDATE account SET ` + strings.Join(updates, ", ") + + ` WHERE organization_id = $` + strconv.Itoa(len(args)-3) + + ` AND ledger_id = $` + strconv.Itoa(len(args)-2) + + ` AND portfolio_id = $` + strconv.Itoa(len(args)-1) + + ` AND id = $` + strconv.Itoa(len(args)) + + ` AND deleted_at IS NULL` + + result, err := db.ExecContext(ctx, query, args...) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(a.Account{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return record.ToEntity(), nil +} + +// Delete removes an Account entity from the database using the provided IDs. +func (r *AccountPostgreSQLRepository) Delete(ctx context.Context, organizationID, ledgerID, portfolioID, id uuid.UUID) error { + db, err := r.connection.GetDB(ctx) + if err != nil { + return err + } + + if _, err := db.ExecContext(ctx, `UPDATE account SET deleted_at = now() WHERE organization_id = $1 AND ledger_id = $2 AND portfolio_id = $3 AND id = $4 AND deleted_at IS NULL`, + organizationID, ledgerID, portfolioID, id); err != nil { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(a.Account{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil +} diff --git a/components/ledger/internal/adapters/database/postgres/instrument.postgresql.go b/components/ledger/internal/adapters/database/postgres/instrument.postgresql.go new file mode 100644 index 00000000..de19c33d --- /dev/null +++ b/components/ledger/internal/adapters/database/postgres/instrument.postgresql.go @@ -0,0 +1,257 @@ +package postgres + +import ( + "context" + "database/sql" + "errors" + "reflect" + "strconv" + "strings" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mpostgres" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + "github.com/google/uuid" + "github.com/lib/pq" +) + +// InstrumentPostgreSQLRepository is a Postgresql-specific implementation of the InstrumentRepository. +type InstrumentPostgreSQLRepository struct { + connection *mpostgres.PostgresConnection +} + +// NewInstrumentPostgreSQLRepository returns a new instance of InstrumentPostgreSQLRepository using the given Postgres connection. +func NewInstrumentPostgreSQLRepository(pc *mpostgres.PostgresConnection) *InstrumentPostgreSQLRepository { + c := &InstrumentPostgreSQLRepository{ + connection: pc, + } + + _, err := c.connection.GetDB(context.Background()) + if err != nil { + panic("Failed to connect database") + } + + return c +} + +// Create a new instrument entity into Postgresql and returns it. +func (r *InstrumentPostgreSQLRepository) Create(ctx context.Context, instrument *i.Instrument) (*i.Instrument, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &i.InstrumentPostgreSQLModel{} + record.FromEntity(instrument) + + result, err := db.ExecContext(ctx, `INSERT INTO instrument + (id, name, type, code, status, status_description, ledger_id, organization_id, created_at, updated_at, deleted_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`, + record.ID, record.Name, record.Type, record.Code, record.Status, record.StatusDescription, + record.LedgerID, record.OrganizationID, record.CreatedAt, record.UpdatedAt, record.DeletedAt) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(i.Instrument{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return record.ToEntity(), nil +} + +// FindAll retrieves Instrument entities from the database. +func (r *InstrumentPostgreSQLRepository) FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID) ([]*i.Instrument, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var instruments []*i.Instrument + + rows, err := db.QueryContext(ctx, "SELECT * FROM instrument WHERE organization_id = $1 AND ledger_id = $2 AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var instrument i.InstrumentPostgreSQLModel + if err := rows.Scan(&instrument.ID, &instrument.Name, &instrument.Type, &instrument.Code, &instrument.Status, &instrument.StatusDescription, + &instrument.LedgerID, &instrument.OrganizationID, &instrument.CreatedAt, &instrument.UpdatedAt, &instrument.DeletedAt); err != nil { + return nil, err + } + + instruments = append(instruments, instrument.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return instruments, nil +} + +// ListByIDs retrieves Instruments entities from the database using the provided IDs. +func (r *InstrumentPostgreSQLRepository) ListByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*i.Instrument, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var instruments []*i.Instrument + + rows, err := db.QueryContext(ctx, "SELECT * FROM instrument WHERE organization_id = $1 AND ledger_id = $2 AND id = ANY($3) AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var instrument i.InstrumentPostgreSQLModel + if err := rows.Scan(&instrument.ID, &instrument.Name, &instrument.Type, &instrument.Code, &instrument.Status, &instrument.StatusDescription, + &instrument.LedgerID, &instrument.OrganizationID, &instrument.CreatedAt, &instrument.UpdatedAt, &instrument.DeletedAt); err != nil { + return nil, err + } + + instruments = append(instruments, instrument.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return instruments, nil +} + +// Find retrieves an Instrument entity from the database using the provided ID. +func (r *InstrumentPostgreSQLRepository) Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*i.Instrument, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + instrument := &i.InstrumentPostgreSQLModel{} + + row := db.QueryRowContext(ctx, "SELECT * FROM instrument WHERE organization_id = $1 AND ledger_id = $2 AND id = $3 AND deleted_at IS NULL", + organizationID, ledgerID, id) + if err := row.Scan(&instrument.ID, &instrument.Name, &instrument.Type, &instrument.Code, &instrument.Status, &instrument.StatusDescription, + &instrument.LedgerID, &instrument.OrganizationID, &instrument.CreatedAt, &instrument.UpdatedAt, &instrument.DeletedAt); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(i.Instrument{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil, err + } + + return instrument.ToEntity(), nil +} + +// Update an Instrument entity into Postgresql and returns the Instrument updated. +func (r *InstrumentPostgreSQLRepository) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, instrument *i.Instrument) (*i.Instrument, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &i.InstrumentPostgreSQLModel{} + record.FromEntity(instrument) + + var updates []string + + var args []any + + if instrument.Name != "" { + updates = append(updates, "name = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Name) + } + + if !instrument.Status.IsEmpty() { + updates = append(updates, "status = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Status) + + updates = append(updates, "status_description = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.StatusDescription) + } + + record.UpdatedAt = time.Now() + + updates = append(updates, "updated_at = $"+strconv.Itoa(len(args)+1)) + + args = append(args, record.UpdatedAt, organizationID, ledgerID, id) + + query := `UPDATE instrument SET ` + strings.Join(updates, ", ") + + ` WHERE organization_id = $` + strconv.Itoa(len(args)-2) + + ` AND ledger_id = $` + strconv.Itoa(len(args)-1) + + ` AND id = $` + strconv.Itoa(len(args)) + + ` AND deleted_at IS NULL` + + result, err := db.ExecContext(ctx, query, args...) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(i.Instrument{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return record.ToEntity(), nil +} + +// Delete removes an Instrument entity from the database using the provided IDs. +func (r *InstrumentPostgreSQLRepository) Delete(ctx context.Context, organizationID, ledgerID, id uuid.UUID) error { + db, err := r.connection.GetDB(ctx) + if err != nil { + return err + } + + result, err := db.ExecContext(ctx, `UPDATE instrument SET deleted_at = now() WHERE organization_id = $1 AND ledger_id = $2 AND id = $3 AND deleted_at IS NULL`, + organizationID, ledgerID, id) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(i.Instrument{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil +} diff --git a/components/ledger/internal/adapters/database/postgres/ledger.postgresql.go b/components/ledger/internal/adapters/database/postgres/ledger.postgresql.go new file mode 100644 index 00000000..f3d6c036 --- /dev/null +++ b/components/ledger/internal/adapters/database/postgres/ledger.postgresql.go @@ -0,0 +1,276 @@ +package postgres + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "reflect" + "strconv" + "strings" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mpostgres" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + "github.com/google/uuid" + "github.com/lib/pq" +) + +// LedgerPostgreSQLRepository is a Postgresql-specific implementation of the LedgerRepository. +type LedgerPostgreSQLRepository struct { + connection *mpostgres.PostgresConnection +} + +// NewLedgerPostgreSQLRepository returns a new instance of LedgerPostgresRepository using the given Postgres connection. +func NewLedgerPostgreSQLRepository(pc *mpostgres.PostgresConnection) *LedgerPostgreSQLRepository { + c := &LedgerPostgreSQLRepository{ + connection: pc, + } + + _, err := c.connection.GetDB(context.Background()) + if err != nil { + panic("Failed to connect database") + } + + return c +} + +// Create a new Ledger entity into Postgresql and returns it. +func (r *LedgerPostgreSQLRepository) Create(ctx context.Context, ledger *l.Ledger) (*l.Ledger, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &l.LedgerPostgreSQLModel{} + record.FromEntity(ledger) + + result, err := db.ExecContext(ctx, `INSERT INTO ledger (id, name, organization_id, status, status_description, created_at, updated_at, deleted_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`, + record.ID, record.Name, record.OrganizationID, record.Status, record.StatusDescription, record.CreatedAt, record.UpdatedAt, record.DeletedAt) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(l.Ledger{}).Name(), + } + } + + return record.ToEntity(), nil +} + +// Find retrieves a Ledger entity from the database using the provided ID. +func (r *LedgerPostgreSQLRepository) Find(ctx context.Context, organizationID, id uuid.UUID) (*l.Ledger, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + ledger := &l.LedgerPostgreSQLModel{} + + var status string + + row := db.QueryRowContext(ctx, "SELECT * FROM ledger WHERE organization_id = $1 AND id = $2 AND deleted_at IS NULL", organizationID, id) + if err := row.Scan(&ledger.ID, &ledger.Name, &ledger.OrganizationID, &ledger.Status, &ledger.StatusDescription, + &ledger.CreatedAt, &ledger.UpdatedAt, &ledger.DeletedAt); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(l.Ledger{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil, err + } + + err = json.Unmarshal([]byte(status), &ledger.Status) + if err != nil { + return nil, err + } + + return ledger.ToEntity(), nil +} + +// FindAll retrieves Ledgers entities from the database. +func (r *LedgerPostgreSQLRepository) FindAll(ctx context.Context, organizationID uuid.UUID) ([]*l.Ledger, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var ledgers []*l.Ledger + + rows, err := db.QueryContext(ctx, "SELECT * FROM ledger WHERE organization_id = $1 AND deleted_at IS NULL ORDER BY created_at DESC", organizationID) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var ledger l.LedgerPostgreSQLModel + + var status string + + if err := rows.Scan(&ledger.ID, &ledger.Name, &ledger.OrganizationID, &ledger.Status, &ledger.StatusDescription, + &ledger.CreatedAt, &ledger.UpdatedAt, &ledger.DeletedAt); err != nil { + return nil, err + } + + err = json.Unmarshal([]byte(status), &ledger.Status) + if err != nil { + return nil, err + } + + ledgers = append(ledgers, ledger.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return ledgers, nil +} + +// ListByIDs retrieves Ledgers entities from the database using the provided IDs. +func (r *LedgerPostgreSQLRepository) ListByIDs(ctx context.Context, organizationID uuid.UUID, ids []uuid.UUID) ([]*l.Ledger, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var ledgers []*l.Ledger + + rows, err := db.QueryContext(ctx, "SELECT * FROM ledger WHERE organization_id = $1 AND id = ANY($2) AND deleted_at IS NULL ORDER BY created_at DESC", organizationID, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var ledger l.LedgerPostgreSQLModel + + var status string + + if err := rows.Scan(&ledger.ID, &ledger.Name, &ledger.OrganizationID, &ledger.Status, &ledger.StatusDescription, + &ledger.CreatedAt, &ledger.UpdatedAt, &ledger.DeletedAt); err != nil { + return nil, err + } + + err = json.Unmarshal([]byte(status), &ledger.Status) + if err != nil { + return nil, err + } + + ledgers = append(ledgers, ledger.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return ledgers, nil +} + +// Update a Ledger entity into Postgresql and returns the Ledger updated. +func (r *LedgerPostgreSQLRepository) Update(ctx context.Context, organizationID, id uuid.UUID, ledger *l.Ledger) (*l.Ledger, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &l.LedgerPostgreSQLModel{} + record.FromEntity(ledger) + + var updates []string + + var args []any + + if ledger.Name != "" { + updates = append(updates, "name = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Name) + } + + if ledger.OrganizationID != "" { + updates = append(updates, "organization_id = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.OrganizationID) + } + + if !ledger.Status.IsEmpty() { + updates = append(updates, "status = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Status) + + updates = append(updates, "status_description = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.StatusDescription) + } + + record.UpdatedAt = time.Now() + + updates = append(updates, "updated_at = $"+strconv.Itoa(len(args)+1)) + + args = append(args, record.UpdatedAt, organizationID, id) + + query := `UPDATE ledger SET ` + strings.Join(updates, ", ") + + ` WHERE organization_id = $` + strconv.Itoa(len(args)-1) + + ` AND id = $` + strconv.Itoa(len(args)) + + ` AND deleted_at IS NULL` + + result, err := db.ExecContext(ctx, query, args...) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(l.Ledger{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return record.ToEntity(), nil +} + +// Delete removes a Ledger entity from the database using the provided ID. +func (r *LedgerPostgreSQLRepository) Delete(ctx context.Context, organizationID, id uuid.UUID) error { + db, err := r.connection.GetDB(ctx) + if err != nil { + return err + } + + result, err := db.ExecContext(ctx, `UPDATE ledger SET deleted_at = now() WHERE organization_id = $1 AND id = $2 AND deleted_at IS NULL`, organizationID, id) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(l.Ledger{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil +} diff --git a/components/ledger/internal/adapters/database/postgres/organization.postgresql.go b/components/ledger/internal/adapters/database/postgres/organization.postgresql.go new file mode 100644 index 00000000..53b3330b --- /dev/null +++ b/components/ledger/internal/adapters/database/postgres/organization.postgresql.go @@ -0,0 +1,305 @@ +package postgres + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "reflect" + "strconv" + "strings" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mpostgres" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + "github.com/google/uuid" + "github.com/lib/pq" +) + +// OrganizationPostgreSQLRepository is a Postgresql-specific implementation of the OrganizationRepository. +type OrganizationPostgreSQLRepository struct { + connection *mpostgres.PostgresConnection +} + +// NewOrganizationPostgreSQLRepository returns a new instance of OrganizationPostgresRepository using the given Postgres connection. +func NewOrganizationPostgreSQLRepository(pc *mpostgres.PostgresConnection) *OrganizationPostgreSQLRepository { + c := &OrganizationPostgreSQLRepository{ + connection: pc, + } + + _, err := c.connection.GetDB(context.Background()) + if err != nil { + panic("Failed to connect database") + } + + return c +} + +// Create inserts a new Organization entity into Postgresql and returns the created Organization. +func (r *OrganizationPostgreSQLRepository) Create(ctx context.Context, organization *o.Organization) (*o.Organization, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &o.OrganizationPostgreSQLModel{} + record.FromEntity(organization) + + address, err := json.Marshal(record.Address) + if err != nil { + return nil, err + } + + result, err := db.ExecContext(ctx, `INSERT INTO organization ( + id, parent_organization_id, legal_name, doing_business_as, legal_document, address, status, status_description, created_at, updated_at, deleted_at + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 + ) RETURNING *`, + record.ID, + record.ParentOrganizationID, + record.LegalName, + record.DoingBusinessAs, + record.LegalDocument, + address, + record.Status, + record.StatusDescription, + record.CreatedAt, + record.UpdatedAt, + record.DeletedAt) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(o.Organization{}).Name(), + } + } + + return record.ToEntity(), nil +} + +// Update an Organization entity into Postgresql and returns the Organization updated. +func (r *OrganizationPostgreSQLRepository) Update(ctx context.Context, id uuid.UUID, organization *o.Organization) (*o.Organization, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &o.OrganizationPostgreSQLModel{} + record.FromEntity(organization) + + var updates []string + + var args []any + + if organization.LegalName != "" { + updates = append(updates, "legal_name = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.LegalName) + } + + if organization.DoingBusinessAs != nil { + updates = append(updates, "doing_business_as = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.DoingBusinessAs) + } + + if !organization.Address.IsEmpty() { + address, err := json.Marshal(record.Address) + if err != nil { + return nil, err + } + + updates = append(updates, "address = $"+strconv.Itoa(len(args)+1)) + args = append(args, address) + } + + if !organization.Status.IsEmpty() { + updates = append(updates, "status = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Status) + + updates = append(updates, "status_description = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.StatusDescription) + } + + record.UpdatedAt = time.Now() + + updates = append(updates, "updated_at = $"+strconv.Itoa(len(args)+1)) + + args = append(args, record.UpdatedAt, id) + query := `UPDATE organization SET ` + strings.Join(updates, ", ") + + ` WHERE id = $` + strconv.Itoa(len(args)) + + ` AND deleted_at IS NULL` + + result, err := db.ExecContext(ctx, query, args...) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(o.Organization{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return record.ToEntity(), nil +} + +// Find retrieves an Organization entity from the database using the provided ID. +func (r *OrganizationPostgreSQLRepository) Find(ctx context.Context, id uuid.UUID) (*o.Organization, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + organization := &o.OrganizationPostgreSQLModel{} + + var address string + + row := db.QueryRowContext(ctx, "SELECT * FROM organization WHERE id = $1 AND deleted_at IS NULL", id) + if err := row.Scan(&organization.ID, &organization.ParentOrganizationID, &organization.LegalName, + &organization.DoingBusinessAs, &organization.LegalDocument, &address, &organization.Status, &organization.StatusDescription, + &organization.CreatedAt, &organization.UpdatedAt, &organization.DeletedAt); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(o.Organization{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil, err + } + + err = json.Unmarshal([]byte(address), &organization.Address) + if err != nil { + return nil, err + } + + return organization.ToEntity(), nil +} + +// FindAll retrieves Organizations entities from the database. +func (r *OrganizationPostgreSQLRepository) FindAll(ctx context.Context) ([]*o.Organization, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var organizations []*o.Organization + + rows, err := db.QueryContext(ctx, "SELECT * FROM organization WHERE deleted_at IS NULL ORDER BY created_at DESC") + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var organization o.OrganizationPostgreSQLModel + + var address string + + if err := rows.Scan(&organization.ID, &organization.ParentOrganizationID, &organization.LegalName, + &organization.DoingBusinessAs, &organization.LegalDocument, &address, &organization.Status, &organization.StatusDescription, + &organization.CreatedAt, &organization.UpdatedAt, &organization.DeletedAt); err != nil { + return nil, err + } + + err = json.Unmarshal([]byte(address), &organization.Address) + if err != nil { + return nil, err + } + + organizations = append(organizations, organization.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return organizations, nil +} + +// ListByIDs retrieves Organizations entities from the database using the provided IDs. +func (r *OrganizationPostgreSQLRepository) ListByIDs(ctx context.Context, ids []uuid.UUID) ([]*o.Organization, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var organizations []*o.Organization + + rows, err := db.QueryContext(ctx, "SELECT * FROM organization WHERE id = ANY($1) AND deleted_at IS NULL ORDER BY created_at DESC", pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var organization o.OrganizationPostgreSQLModel + + var address string + + if err := rows.Scan(&organization.ID, &organization.ParentOrganizationID, &organization.LegalName, + &organization.DoingBusinessAs, &organization.LegalDocument, &address, &organization.Status, &organization.StatusDescription, + &organization.CreatedAt, &organization.UpdatedAt, &organization.DeletedAt); err != nil { + return nil, err + } + + err = json.Unmarshal([]byte(address), &organization.Address) + if err != nil { + return nil, err + } + + organizations = append(organizations, organization.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return organizations, nil +} + +// Delete removes an Organization entity from the database using the provided ID. +func (r *OrganizationPostgreSQLRepository) Delete(ctx context.Context, id uuid.UUID) error { + db, err := r.connection.GetDB(ctx) + if err != nil { + return err + } + + result, err := db.ExecContext(ctx, `UPDATE organization SET deleted_at = now() WHERE id = $1 AND deleted_at IS NULL`, id) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(o.Organization{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil +} diff --git a/components/ledger/internal/adapters/database/postgres/portfolio.postgresql.go b/components/ledger/internal/adapters/database/postgres/portfolio.postgresql.go new file mode 100644 index 00000000..8f86f0fb --- /dev/null +++ b/components/ledger/internal/adapters/database/postgres/portfolio.postgresql.go @@ -0,0 +1,289 @@ +package postgres + +import ( + "context" + "database/sql" + "errors" + "reflect" + "strconv" + "strings" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mpostgres" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + "github.com/google/uuid" + "github.com/lib/pq" +) + +// PortfolioPostgreSQLRepository is a Postgresql-specific implementation of the PortfolioRepository. +type PortfolioPostgreSQLRepository struct { + connection *mpostgres.PostgresConnection +} + +// NewPortfolioPostgreSQLRepository returns a new instance of PortfolioPostgreSQLRepository using the given Postgres connection. +func NewPortfolioPostgreSQLRepository(pc *mpostgres.PostgresConnection) *PortfolioPostgreSQLRepository { + c := &PortfolioPostgreSQLRepository{ + connection: pc, + } + + _, err := c.connection.GetDB(context.Background()) + if err != nil { + panic("Failed to connect database") + } + + return c +} + +// Create a new portfolio entity into Postgresql and returns it. +func (r *PortfolioPostgreSQLRepository) Create(ctx context.Context, portfolio *p.Portfolio) (*p.Portfolio, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &p.PortfolioPostgreSQLModel{} + record.FromEntity(portfolio) + + result, err := db.ExecContext(ctx, `INSERT INTO portfolio (id, name, entity_id, ledger_id, organization_id, + status, status_description, created_at, updated_at, deleted_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *`, + record.ID, record.Name, record.EntityID, record.LedgerID, record.OrganizationID, + record.Status, record.StatusDescription, record.CreatedAt, record.UpdatedAt, record.DeletedAt) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return record.ToEntity(), nil +} + +// FindByIDEntity find portfolio from the database using the Entity id. +func (r *PortfolioPostgreSQLRepository) FindByIDEntity(ctx context.Context, organizationID, ledgerID, entityID uuid.UUID) (*p.Portfolio, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + portfolio := &p.PortfolioPostgreSQLModel{} + + row := db.QueryRowContext(ctx, "SELECT * FROM portfolio WHERE organization_id = $1 AND ledger_id = $2 AND entity_id = $3 AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID, entityID) + if err := row.Scan(&portfolio.ID, &portfolio.Name, &portfolio.EntityID, &portfolio.LedgerID, &portfolio.OrganizationID, + &portfolio.Status, &portfolio.StatusDescription, &portfolio.CreatedAt, &portfolio.UpdatedAt, &portfolio.DeletedAt); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil, err + } + + return portfolio.ToEntity(), nil +} + +// FindAll retrieves Portfolio entities from the database. +func (r *PortfolioPostgreSQLRepository) FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID) ([]*p.Portfolio, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var portfolios []*p.Portfolio + + rows, err := db.QueryContext(ctx, "SELECT * FROM portfolio WHERE organization_id = $1 AND ledger_id = $2 AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID) + if err != nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + defer rows.Close() + + for rows.Next() { + var portfolio p.PortfolioPostgreSQLModel + if err := rows.Scan(&portfolio.ID, &portfolio.Name, &portfolio.EntityID, &portfolio.LedgerID, &portfolio.OrganizationID, + &portfolio.Status, &portfolio.StatusDescription, &portfolio.CreatedAt, &portfolio.UpdatedAt, &portfolio.DeletedAt); err != nil { + return nil, err + } + + portfolios = append(portfolios, portfolio.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return portfolios, nil +} + +// Find retrieves a Portfolio entity from the database using the provided ID. +func (r *PortfolioPostgreSQLRepository) Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*p.Portfolio, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + portfolio := &p.PortfolioPostgreSQLModel{} + + row := db.QueryRowContext(ctx, "SELECT * FROM portfolio WHERE organization_id = $1 AND ledger_id = $2 AND id = $3 AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID, id) + if err := row.Scan(&portfolio.ID, &portfolio.Name, &portfolio.EntityID, &portfolio.LedgerID, &portfolio.OrganizationID, + &portfolio.Status, &portfolio.StatusDescription, &portfolio.CreatedAt, &portfolio.UpdatedAt, &portfolio.DeletedAt); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil, err + } + + return portfolio.ToEntity(), nil +} + +// ListByIDs retrieves Portfolios entities from the database using the provided IDs. +func (r *PortfolioPostgreSQLRepository) ListByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*p.Portfolio, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var portfolios []*p.Portfolio + + rows, err := db.QueryContext(ctx, "SELECT * FROM portfolio WHERE organization_id = $1 AND ledger_id = $2 AND id = ANY($3) AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var portfolio p.PortfolioPostgreSQLModel + if err := rows.Scan(&portfolio.ID, &portfolio.Name, &portfolio.EntityID, &portfolio.LedgerID, &portfolio.OrganizationID, + &portfolio.Status, &portfolio.StatusDescription, &portfolio.CreatedAt, &portfolio.UpdatedAt, &portfolio.DeletedAt); err != nil { + return nil, err + } + + portfolios = append(portfolios, portfolio.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return portfolios, nil +} + +// Update a Portfolio entity into Postgresql and returns the Portfolio updated. +func (r *PortfolioPostgreSQLRepository) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, portfolio *p.Portfolio) (*p.Portfolio, error) { + db, err := r.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &p.PortfolioPostgreSQLModel{} + record.FromEntity(portfolio) + + var updates []string + + var args []any + + if portfolio.Name != "" { + updates = append(updates, "name = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Name) + } + + if !portfolio.Status.IsEmpty() { + updates = append(updates, "status = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Status) + + updates = append(updates, "status_description = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.StatusDescription) + } + + record.UpdatedAt = time.Now() + + updates = append(updates, "updated_at = $"+strconv.Itoa(len(args)+1)) + + args = append(args, record.UpdatedAt, organizationID, ledgerID, id) + + query := `UPDATE portfolio SET ` + strings.Join(updates, ", ") + + ` WHERE organization_id = $` + strconv.Itoa(len(args)-2) + + ` AND ledger_id = $` + strconv.Itoa(len(args)-1) + + ` AND id = $` + strconv.Itoa(len(args)) + + ` AND deleted_at IS NULL` + + result, err := db.ExecContext(ctx, query, args...) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return record.ToEntity(), nil +} + +// Delete removes a Portfolio entity from the database using the provided IDs. +func (r *PortfolioPostgreSQLRepository) Delete(ctx context.Context, organizationID, ledgerID, id uuid.UUID) error { + db, err := r.connection.GetDB(ctx) + if err != nil { + return err + } + + result, err := db.ExecContext(ctx, `UPDATE portfolio SET deleted_at = now() WHERE organization_id = $1 AND ledger_id = $2 AND id = $3 AND deleted_at IS NULL`, + organizationID, ledgerID, id) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil +} diff --git a/components/ledger/internal/adapters/database/postgres/product.postgresql.go b/components/ledger/internal/adapters/database/postgres/product.postgresql.go new file mode 100644 index 00000000..8b69249a --- /dev/null +++ b/components/ledger/internal/adapters/database/postgres/product.postgresql.go @@ -0,0 +1,294 @@ +package postgres + +import ( + "context" + "database/sql" + "errors" + "reflect" + "strconv" + "strings" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mpostgres" + r "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + "github.com/google/uuid" + "github.com/lib/pq" +) + +// ProductPostgreSQLRepository is a Postgresql-specific implementation of the Repository. +type ProductPostgreSQLRepository struct { + connection *mpostgres.PostgresConnection +} + +// NewProductPostgreSQLRepository returns a new instance of ProductPostgreSQLRepository using the given Postgres connection. +func NewProductPostgreSQLRepository(pc *mpostgres.PostgresConnection) *ProductPostgreSQLRepository { + c := &ProductPostgreSQLRepository{ + connection: pc, + } + + _, err := c.connection.GetDB(context.Background()) + if err != nil { + panic("Failed to connect database") + } + + return c +} + +// Create a new product entity into Postgresql and returns it. +func (p *ProductPostgreSQLRepository) Create(ctx context.Context, product *r.Product) (*r.Product, error) { + db, err := p.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &r.ProductPostgreSQLModel{} + record.FromEntity(product) + + result, err := db.ExecContext(ctx, `INSERT INTO product VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`, + record.ID, + record.Name, + record.LedgerID, + record.OrganizationID, + record.Status, + record.StatusDescription, + record.CreatedAt, + record.UpdatedAt, + record.DeletedAt, + ) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return record.ToEntity(), nil +} + +// FindByName find product from the database using Organization and Ledger id and Name. +func (p *ProductPostgreSQLRepository) FindByName(ctx context.Context, organizationID, ledgerID uuid.UUID, name string) (bool, error) { + db, err := p.connection.GetDB(ctx) + if err != nil { + return false, err + } + + rows, err := db.QueryContext(ctx, "SELECT * FROM product WHERE organization_id = $1 AND ledger_id = $2 AND name LIKE $3 AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID, name) + if err != nil { + return false, err + } + defer rows.Close() + + if rows.Next() { + return true, common.EntityConflictError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Title: "Entity found.", + Code: "0008", + Message: "Entity was found matching by the provided Name. Ensure the another Name is being used for the entity you are attempting to manage.", + } + } + + return false, nil +} + +// FindAll retrieves Product entities from the database. +func (p *ProductPostgreSQLRepository) FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID) ([]*r.Product, error) { + db, err := p.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var products []*r.Product + + rows, err := db.QueryContext(ctx, "SELECT * FROM product WHERE organization_id = $1 AND ledger_id = $2 AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID) + if err != nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + defer rows.Close() + + for rows.Next() { + var product r.ProductPostgreSQLModel + if err := rows.Scan(&product.ID, &product.Name, &product.LedgerID, &product.OrganizationID, + &product.Status, &product.StatusDescription, &product.CreatedAt, &product.UpdatedAt, &product.DeletedAt); err != nil { + return nil, err + } + + products = append(products, product.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return products, nil +} + +// FindByIDs retrieves Products entities from the database using the provided IDs. +func (p *ProductPostgreSQLRepository) FindByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*r.Product, error) { + db, err := p.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + var products []*r.Product + + rows, err := db.QueryContext(ctx, "SELECT * FROM product WHERE organization_id = $1 AND ledger_id = $2 AND id = ANY($3) AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var product r.ProductPostgreSQLModel + if err := rows.Scan(&product.ID, &product.Name, &product.LedgerID, &product.OrganizationID, + &product.Status, &product.StatusDescription, &product.CreatedAt, &product.UpdatedAt, &product.DeletedAt); err != nil { + return nil, err + } + + products = append(products, product.ToEntity()) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return products, nil +} + +// Find retrieves a Product entity from the database using the provided ID. +func (p *ProductPostgreSQLRepository) Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*r.Product, error) { + db, err := p.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + product := &r.ProductPostgreSQLModel{} + + row := db.QueryRowContext(ctx, "SELECT * FROM product WHERE organization_id = $1 AND ledger_id = $2 AND id = $3 AND deleted_at IS NULL ORDER BY created_at DESC", + organizationID, ledgerID, id) + if err := row.Scan(&product.ID, &product.Name, &product.LedgerID, &product.OrganizationID, + &product.Status, &product.StatusDescription, &product.CreatedAt, &product.UpdatedAt, &product.DeletedAt); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil, err + } + + return product.ToEntity(), nil +} + +// Update a Product entity into Postgresql and returns the Product updated. +func (p *ProductPostgreSQLRepository) Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, prd *r.Product) (*r.Product, error) { + db, err := p.connection.GetDB(ctx) + if err != nil { + return nil, err + } + + record := &r.ProductPostgreSQLModel{} + record.FromEntity(prd) + + var updates []string + + var args []any + + if prd.Name != "" { + updates = append(updates, "name = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Name) + } + + if !prd.Status.IsEmpty() { + updates = append(updates, "status = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.Status) + + updates = append(updates, "status_description = $"+strconv.Itoa(len(args)+1)) + args = append(args, record.StatusDescription) + } + + record.UpdatedAt = time.Now() + + updates = append(updates, "updated_at = $"+strconv.Itoa(len(args)+1)) + + args = append(args, record.UpdatedAt, organizationID, ledgerID, id) + + query := `UPDATE product SET ` + strings.Join(updates, ", ") + + ` WHERE organization_id = $` + strconv.Itoa(len(args)-2) + + ` AND ledger_id = $` + strconv.Itoa(len(args)-1) + + ` AND id = $` + strconv.Itoa(len(args)) + + ` AND deleted_at IS NULL` + + result, err := db.ExecContext(ctx, query, args...) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return record.ToEntity(), nil +} + +// Delete removes a Product entity from the database using the provided IDs. +func (p *ProductPostgreSQLRepository) Delete(ctx context.Context, organizationID, ledgerID, id uuid.UUID) error { + db, err := p.connection.GetDB(ctx) + if err != nil { + return err + } + + result, err := db.ExecContext(ctx, `UPDATE product SET deleted_at = now() WHERE organization_id = $1 AND ledger_id = $2 AND id = $3 AND deleted_at IS NULL`, + organizationID, ledgerID, id) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Title: "Entity not found.", + Code: "0007", + Message: "No entity was found matching the provided ID. Ensure the correct ID is being used for the entity you are attempting to manage.", + } + } + + return nil +} diff --git a/components/ledger/internal/app/command/command.go b/components/ledger/internal/app/command/command.go new file mode 100644 index 00000000..d417ca8e --- /dev/null +++ b/components/ledger/internal/app/command/command.go @@ -0,0 +1,35 @@ +package command + +import ( + m "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + r "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" +) + +// UseCase is a struct that aggregates various repositories for simplified access in use case implementations. +type UseCase struct { + // OrganizationRepo provides an abstraction on top of the organization data source. + OrganizationRepo o.Repository + + // LedgerRepo provides an abstraction on top of the ledger data source. + LedgerRepo l.Repository + + // ProductRepo provides an abstraction on top of the product data source. + ProductRepo r.Repository + + // PortfolioRepo provides an abstraction on top of the portfolio data source. + PortfolioRepo p.Repository + + // AccountRepo provides an abstraction on top of the account data source. + AccountRepo a.Repository + + // InstrumentRepo provides an abstraction on top of the instrument data source. + InstrumentRepo i.Repository + + // MetadataRepo provides an abstraction on top of the metadata data source. + MetadataRepo m.Repository +} diff --git a/components/ledger/internal/app/command/create-account.go b/components/ledger/internal/app/command/create-account.go new file mode 100644 index 00000000..aa8269ec --- /dev/null +++ b/components/ledger/internal/app/command/create-account.go @@ -0,0 +1,104 @@ +package command + +import ( + "context" + "reflect" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + m "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + "github.com/google/uuid" +) + +// CreateAccount creates a new account persists data in the repository. +func (uc *UseCase) CreateAccount(ctx context.Context, organizationID, ledgerID, portfolioID string, cai *a.CreateAccountInput) (*a.Account, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to create account: %v", cai) + + var status a.Status + if cai.Status.IsEmpty() { + status = a.Status{ + Code: "ACTIVE", + } + } else { + status = cai.Status + } + + balanceValue := float64(0) + + var balance a.Balance + if cai.Balance.IsEmpty() { + balance = a.Balance{ + Available: &balanceValue, + OnHold: &balanceValue, + Scale: &balanceValue, + } + } else { + balance = cai.Balance + } + + var entityID string + + if cai.EntityID == nil { + portfolio, err := uc.PortfolioRepo.Find(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(portfolioID)) + if err != nil { + logger.Errorf("Error find portfolio to get Entity ID: %v", err) + return nil, err + } + + entityID = portfolio.EntityID + } else { + entityID = *cai.EntityID + } + + account := &a.Account{ + ID: uuid.New().String(), + InstrumentCode: cai.InstrumentCode, + Alias: cai.Alias, + Name: cai.Name, + Type: cai.Type, + ParentAccountID: cai.ParentAccountID, + ProductID: cai.ProductID, + OrganizationID: organizationID, + PortfolioID: portfolioID, + LedgerID: ledgerID, + EntityID: entityID, + Balance: balance, + Status: status, + AllowSending: true, + AllowReceiving: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + port, err := uc.AccountRepo.Create(ctx, account) + if err != nil { + logger.Errorf("Error creating account: %v", err) + return nil, err + } + + if cai.Metadata != nil { + if err := common.CheckMetadataKeyAndValueLength(100, cai.Metadata); err != nil { + return nil, err + } + + meta := m.Metadata{ + EntityID: port.ID, + EntityName: reflect.TypeOf(a.Account{}).Name(), + Data: cai.Metadata, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := uc.MetadataRepo.Create(ctx, reflect.TypeOf(a.Account{}).Name(), &meta); err != nil { + logger.Errorf("Error into creating account metadata: %v", err) + return nil, err + } + + port.Metadata = cai.Metadata + } + + return port, nil +} diff --git a/components/ledger/internal/app/command/create-account_test.go b/components/ledger/internal/app/command/create-account_test.go new file mode 100644 index 00000000..d32b8f4a --- /dev/null +++ b/components/ledger/internal/app/command/create-account_test.go @@ -0,0 +1,63 @@ +package command + +import ( + "context" + "errors" + "testing" + + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/account" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestCreateAccountSuccess is responsible to test CreateAccount with success +func TestCreateAccountSuccess(t *testing.T) { + account := &a.Account{ + ID: uuid.New().String(), + OrganizationID: uuid.New().String(), + LedgerID: uuid.New().String(), + PortfolioID: uuid.New().String(), + } + + uc := UseCase{ + AccountRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.AccountRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), account). + Return(account, nil). + Times(1) + res, err := uc.AccountRepo.Create(context.TODO(), account) + + assert.Equal(t, account, res) + assert.Nil(t, err) +} + +// TestCreateAccountError is responsible to test CreateAccount with error +func TestCreateAccountError(t *testing.T) { + errMSG := "err to create account on database" + account := &a.Account{ + ID: uuid.New().String(), + OrganizationID: uuid.New().String(), + LedgerID: uuid.New().String(), + PortfolioID: uuid.New().String(), + } + + uc := UseCase{ + AccountRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.AccountRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), account). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.AccountRepo.Create(context.TODO(), account) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/command/create-instrument.go b/components/ledger/internal/app/command/create-instrument.go new file mode 100644 index 00000000..280e2463 --- /dev/null +++ b/components/ledger/internal/app/command/create-instrument.go @@ -0,0 +1,67 @@ +package command + +import ( + "context" + "reflect" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + m "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + "github.com/google/uuid" +) + +// CreateInstrument creates a new instrument persists data in the repository. +func (uc *UseCase) CreateInstrument(ctx context.Context, organizationID, ledgerID uuid.UUID, cii *i.CreateInstrumentInput) (*i.Instrument, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to create instrument: %v", cii) + + var status i.Status + if cii.Status.IsEmpty() { + status = i.Status{ + Code: "ACTIVE", + } + } else { + status = cii.Status + } + + instrument := &i.Instrument{ + Name: cii.Name, + Type: cii.Type, + Code: cii.Code, + Status: status, + LedgerID: ledgerID.String(), + OrganizationID: organizationID.String(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + inst, err := uc.InstrumentRepo.Create(ctx, instrument) + if err != nil { + logger.Errorf("Error creating instrument: %v", err) + return nil, err + } + + if cii.Metadata != nil { + if err := common.CheckMetadataKeyAndValueLength(100, cii.Metadata); err != nil { + return nil, err + } + + meta := m.Metadata{ + EntityID: inst.ID, + EntityName: reflect.TypeOf(i.Instrument{}).Name(), + Data: cii.Metadata, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if err := uc.MetadataRepo.Create(ctx, reflect.TypeOf(i.Instrument{}).Name(), &meta); err != nil { + logger.Errorf("Error into creating instrument metadata: %v", err) + return nil, err + } + + inst.Metadata = cii.Metadata + } + + return inst, nil +} diff --git a/components/ledger/internal/app/command/create-instrument_test.go b/components/ledger/internal/app/command/create-instrument_test.go new file mode 100644 index 00000000..2b35e2a0 --- /dev/null +++ b/components/ledger/internal/app/command/create-instrument_test.go @@ -0,0 +1,60 @@ +package command + +import ( + "context" + "errors" + "testing" + + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/instrument" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestCreateInstrumentSuccess is responsible to test CreateInstrument with success +func TestCreateInstrumentSuccess(t *testing.T) { + instrument := &i.Instrument{ + ID: uuid.New().String(), + LedgerID: uuid.New().String(), + OrganizationID: uuid.New().String(), + } + + uc := UseCase{ + InstrumentRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.InstrumentRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), instrument). + Return(instrument, nil). + Times(1) + res, err := uc.InstrumentRepo.Create(context.TODO(), instrument) + + assert.Equal(t, instrument, res) + assert.Nil(t, err) +} + +// TestCreateInstrumentError is responsible to test CreateInstrument with error +func TestCreateInstrumentError(t *testing.T) { + errMSG := "err to create instrument on database" + instrument := &i.Instrument{ + ID: uuid.New().String(), + LedgerID: uuid.New().String(), + } + + uc := UseCase{ + InstrumentRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.InstrumentRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), instrument). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.InstrumentRepo.Create(context.TODO(), instrument) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/command/create-ledger.go b/components/ledger/internal/app/command/create-ledger.go new file mode 100644 index 00000000..019c7c19 --- /dev/null +++ b/components/ledger/internal/app/command/create-ledger.go @@ -0,0 +1,62 @@ +package command + +import ( + "context" + "reflect" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + m "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" +) + +// CreateLedger creates a new ledger persists data in the repository. +func (uc *UseCase) CreateLedger(ctx context.Context, organizationID string, cli *l.CreateLedgerInput) (*l.Ledger, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to create ledger: %v", cli) + + var status l.Status + if cli.Status.IsEmpty() { + status = l.Status{ + Code: "ACTIVE", + } + } else { + status = cli.Status + } + + ledger := &l.Ledger{ + OrganizationID: organizationID, + Name: cli.Name, + Status: status, + } + + led, err := uc.LedgerRepo.Create(ctx, ledger) + if err != nil { + logger.Errorf("Error creating ledger: %v", err) + return nil, err + } + + if cli.Metadata != nil { + if err := common.CheckMetadataKeyAndValueLength(100, cli.Metadata); err != nil { + return nil, err + } + + meta := m.Metadata{ + EntityID: led.ID, + EntityName: reflect.TypeOf(l.Ledger{}).Name(), + Data: cli.Metadata, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := uc.MetadataRepo.Create(ctx, reflect.TypeOf(l.Ledger{}).Name(), &meta); err != nil { + logger.Errorf("Error into creating ledger metadata: %v", err) + return nil, err + } + + led.Metadata = cli.Metadata + } + + return led, nil +} diff --git a/components/ledger/internal/app/command/create-ledger_test.go b/components/ledger/internal/app/command/create-ledger_test.go new file mode 100644 index 00000000..584431d5 --- /dev/null +++ b/components/ledger/internal/app/command/create-ledger_test.go @@ -0,0 +1,60 @@ +package command + +import ( + "context" + "errors" + "testing" + + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/ledger" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestCreateLedgerSuccess is responsible to test CreateLedger with success +func TestCreateLedgerSuccess(t *testing.T) { + ledger := &l.Ledger{ + ID: uuid.New().String(), + OrganizationID: uuid.New().String(), + } + + uc := UseCase{ + LedgerRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.LedgerRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), ledger). + Return(ledger, nil). + Times(1) + res, err := uc.LedgerRepo.Create(context.TODO(), ledger) + + assert.Equal(t, ledger, res) + assert.Nil(t, err) +} + +// TestCreateLedgerError is responsible to test CreateLedger with error +func TestCreateLedgerError(t *testing.T) { + errMSG := "err to create ledger on database" + + ledger := &l.Ledger{ + ID: uuid.New().String(), + OrganizationID: uuid.New().String(), + } + + uc := UseCase{ + LedgerRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.LedgerRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), ledger). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.LedgerRepo.Create(context.TODO(), ledger) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/command/create-matadata_test.go b/components/ledger/internal/app/command/create-matadata_test.go new file mode 100644 index 00000000..47d9bfd3 --- /dev/null +++ b/components/ledger/internal/app/command/create-matadata_test.go @@ -0,0 +1,53 @@ +package command + +import ( + "context" + "errors" + "reflect" + "testing" + + meta "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/metadata" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +// TestMetadataCreateSuccess is responsible to test MetadataCreate with success +func TestMetadataCreateSuccess(t *testing.T) { + metadata := meta.Metadata{ID: primitive.NewObjectID()} + collection := reflect.TypeOf(o.Organization{}).Name() + uc := UseCase{ + MetadataRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.MetadataRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), collection, &metadata). + Return(nil). + Times(1) + + err := uc.MetadataRepo.Create(context.TODO(), collection, &metadata) + assert.Nil(t, err) +} + +// TestMetadataCreateError is responsible to test MetadataCreate with error +func TestMetadataCreateError(t *testing.T) { + errMSG := "err to create metadata on mongodb" + metadata := meta.Metadata{ID: primitive.NewObjectID()} + collection := reflect.TypeOf(o.Organization{}).Name() + uc := UseCase{ + MetadataRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.MetadataRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), collection, &metadata). + Return(errors.New(errMSG)). + Times(1) + + err := uc.MetadataRepo.Create(context.TODO(), collection, &metadata) + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) +} diff --git a/components/ledger/internal/app/command/create-organization.go b/components/ledger/internal/app/command/create-organization.go new file mode 100644 index 00000000..b363615f --- /dev/null +++ b/components/ledger/internal/app/command/create-organization.go @@ -0,0 +1,67 @@ +package command + +import ( + "context" + "reflect" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + m "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" +) + +// CreateOrganization creates a new organization persists data in the repository. +func (uc *UseCase) CreateOrganization(ctx context.Context, coi *o.CreateOrganizationInput) (*o.Organization, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to create organization: %v", coi) + + var status o.Status + if coi.Status.IsEmpty() { + status = o.Status{ + Code: "ACTIVE", + } + } else { + status = coi.Status + } + + organization := &o.Organization{ + ParentOrganizationID: coi.ParentOrganizationID, + LegalName: coi.LegalName, + DoingBusinessAs: coi.DoingBusinessAs, + LegalDocument: coi.LegalDocument, + Address: coi.Address, + Status: status, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + org, err := uc.OrganizationRepo.Create(ctx, organization) + if err != nil { + logger.Errorf("Error creating organization: %v", err) + return nil, err + } + + if coi.Metadata != nil { + if err := common.CheckMetadataKeyAndValueLength(100, coi.Metadata); err != nil { + return nil, err + } + + meta := m.Metadata{ + EntityID: org.ID, + EntityName: reflect.TypeOf(o.Organization{}).Name(), + Data: coi.Metadata, + CreatedAt: organization.CreatedAt, + UpdatedAt: organization.UpdatedAt, + } + + if err := uc.MetadataRepo.Create(ctx, reflect.TypeOf(o.Organization{}).Name(), &meta); err != nil { + logger.Errorf("Error into creating organization metadata: %v", err) + return nil, err + } + + org.Metadata = coi.Metadata + } + + return org, nil +} diff --git a/components/ledger/internal/app/command/create-organization_test.go b/components/ledger/internal/app/command/create-organization_test.go new file mode 100644 index 00000000..34d5a03b --- /dev/null +++ b/components/ledger/internal/app/command/create-organization_test.go @@ -0,0 +1,53 @@ +package command + +import ( + "context" + "errors" + "testing" + + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/organization" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestCreateOrganizationSuccess is responsible to test CreateOrganization with success +func TestCreateOrganizationSuccess(t *testing.T) { + id := uuid.New().String() + organization := &o.Organization{ID: id} + + uc := UseCase{ + OrganizationRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.OrganizationRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), organization). + Return(organization, nil). + Times(1) + res, err := uc.OrganizationRepo.Create(context.TODO(), organization) + + assert.Equal(t, organization, res) + assert.Nil(t, err) +} + +// TestCreateOrganizationError is responsible to test CreateOrganization with error +func TestCreateOrganizationError(t *testing.T) { + organization := &o.Organization{} + errMSG := "err to create organization on database" + + uc := UseCase{ + OrganizationRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.OrganizationRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), organization). + Return(nil, errors.New(errMSG)) + res, err := uc.OrganizationRepo.Create(context.TODO(), organization) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/command/create-portfolio.go b/components/ledger/internal/app/command/create-portfolio.go new file mode 100644 index 00000000..93665ad3 --- /dev/null +++ b/components/ledger/internal/app/command/create-portfolio.go @@ -0,0 +1,67 @@ +package command + +import ( + "context" + "reflect" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + m "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + "github.com/google/uuid" +) + +// CreatePortfolio creates a new portfolio persists data in the repository. +func (uc *UseCase) CreatePortfolio(ctx context.Context, organizationID, ledgerID string, cpi *p.CreatePortfolioInput) (*p.Portfolio, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to create portfolio: %v", cpi) + + var status p.Status + if cpi.Status.IsEmpty() { + status = p.Status{ + Code: "ACTIVE", + } + } else { + status = cpi.Status + } + + portfolio := &p.Portfolio{ + ID: uuid.New().String(), + EntityID: cpi.EntityID, + LedgerID: ledgerID, + OrganizationID: organizationID, + Name: cpi.Name, + Status: status, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + port, err := uc.PortfolioRepo.Create(ctx, portfolio) + if err != nil { + logger.Errorf("Error creating portfolio: %v", err) + return nil, err + } + + if cpi.Metadata != nil { + if err := common.CheckMetadataKeyAndValueLength(100, cpi.Metadata); err != nil { + return nil, err + } + + meta := m.Metadata{ + EntityID: port.ID, + EntityName: reflect.TypeOf(p.Portfolio{}).Name(), + Data: cpi.Metadata, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if err := uc.MetadataRepo.Create(ctx, reflect.TypeOf(p.Portfolio{}).Name(), &meta); err != nil { + logger.Errorf("Error into creating portfolio metadata: %v", err) + return nil, err + } + + port.Metadata = cpi.Metadata + } + + return port, nil +} diff --git a/components/ledger/internal/app/command/create-portfolio_test.go b/components/ledger/internal/app/command/create-portfolio_test.go new file mode 100644 index 00000000..47e1c88e --- /dev/null +++ b/components/ledger/internal/app/command/create-portfolio_test.go @@ -0,0 +1,63 @@ +package command + +import ( + "context" + "errors" + "testing" + + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/portfolio" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestCreatePortfolioSuccess is responsible to test CreatePortfolio with success +func TestCreatePortfolioSuccess(t *testing.T) { + portfolio := &p.Portfolio{ + ID: uuid.New().String(), + OrganizationID: uuid.New().String(), + EntityID: uuid.New().String(), + LedgerID: uuid.New().String(), + } + + uc := UseCase{ + PortfolioRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.PortfolioRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), portfolio). + Return(portfolio, nil). + Times(1) + res, err := uc.PortfolioRepo.Create(context.TODO(), portfolio) + + assert.Equal(t, portfolio, res) + assert.Nil(t, err) +} + +// TestCreatePortfolioError is responsible to test CreatePortfolio with error +func TestCreatePortfolioError(t *testing.T) { + errMSG := "err to create portfolio on database" + portfolio := &p.Portfolio{ + ID: uuid.New().String(), + OrganizationID: uuid.New().String(), + EntityID: uuid.New().String(), + LedgerID: uuid.New().String(), + } + + uc := UseCase{ + PortfolioRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.PortfolioRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), portfolio). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.PortfolioRepo.Create(context.TODO(), portfolio) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/command/create-product.go b/components/ledger/internal/app/command/create-product.go new file mode 100644 index 00000000..cee4c7b2 --- /dev/null +++ b/components/ledger/internal/app/command/create-product.go @@ -0,0 +1,71 @@ +package command + +import ( + "context" + "reflect" + "time" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + m "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + r "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + "github.com/google/uuid" +) + +// CreateProduct creates a new product persists data in the repository. +func (uc *UseCase) CreateProduct(ctx context.Context, organizationID, ledgerID string, cpi *r.CreateProductInput) (*r.Product, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to create product: %v", cpi) + + var status r.Status + if cpi.Status.IsEmpty() { + status = r.Status{ + Code: "ACTIVE", + } + } else { + status = cpi.Status + } + + product := &r.Product{ + ID: uuid.New().String(), + LedgerID: ledgerID, + OrganizationID: organizationID, + Name: cpi.Name, + Status: status, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + _, err := uc.ProductRepo.FindByName(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), cpi.Name) + if err != nil { + return nil, err + } + + prod, err := uc.ProductRepo.Create(ctx, product) + if err != nil { + logger.Errorf("Error creating product: %v", err) + return nil, err + } + + if cpi.Metadata != nil { + if err := common.CheckMetadataKeyAndValueLength(100, cpi.Metadata); err != nil { + return nil, err + } + + meta := m.Metadata{ + EntityID: prod.ID, + EntityName: reflect.TypeOf(r.Product{}).Name(), + Data: cpi.Metadata, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if err := uc.MetadataRepo.Create(ctx, reflect.TypeOf(r.Product{}).Name(), &meta); err != nil { + logger.Errorf("Error into creating product metadata: %v", err) + return nil, err + } + + prod.Metadata = cpi.Metadata + } + + return prod, nil +} diff --git a/components/ledger/internal/app/command/create-product_test.go b/components/ledger/internal/app/command/create-product_test.go new file mode 100644 index 00000000..14a88f8c --- /dev/null +++ b/components/ledger/internal/app/command/create-product_test.go @@ -0,0 +1,61 @@ +package command + +import ( + "context" + "errors" + "testing" + + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/product" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestCreateProductSuccess is responsible to test CreateProduct with success +func TestCreateProductSuccess(t *testing.T) { + product := &p.Product{ + ID: uuid.New().String(), + OrganizationID: uuid.New().String(), + LedgerID: uuid.New().String(), + } + + uc := UseCase{ + ProductRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.ProductRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), product). + Return(product, nil). + Times(1) + res, err := uc.ProductRepo.Create(context.TODO(), product) + + assert.Equal(t, product, res) + assert.Nil(t, err) +} + +// TestCreateProductError is responsible to test CreateProduct with error +func TestCreateProductError(t *testing.T) { + errMSG := "err to create product on database" + product := &p.Product{ + ID: uuid.New().String(), + OrganizationID: uuid.New().String(), + LedgerID: uuid.New().String(), + } + + uc := UseCase{ + ProductRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.ProductRepo.(*mock.MockRepository). + EXPECT(). + Create(gomock.Any(), product). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.ProductRepo.Create(context.TODO(), product) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/command/delete-account.go b/components/ledger/internal/app/command/delete-account.go new file mode 100644 index 00000000..b5b25ed4 --- /dev/null +++ b/components/ledger/internal/app/command/delete-account.go @@ -0,0 +1,37 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + "github.com/google/uuid" +) + +// DeleteAccountByID delete an account from the repository by ids. +func (uc *UseCase) DeleteAccountByID(ctx context.Context, organizationID, ledgerID, portfolioID, id string) error { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Remove account for id: %s", id) + + if err := uc.AccountRepo.Delete(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(portfolioID), uuid.MustParse(id)); err != nil { + logger.Errorf("Error deleting account on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(a.Account{}).Name(), + Message: fmt.Sprintf("Account with id %s was not found", id), + Code: "ACCOUNT_NOT_FOUND", + Err: err, + } + } + + return err + } + + return nil +} diff --git a/components/ledger/internal/app/command/delete-account_test.go b/components/ledger/internal/app/command/delete-account_test.go new file mode 100644 index 00000000..5c40b4d0 --- /dev/null +++ b/components/ledger/internal/app/command/delete-account_test.go @@ -0,0 +1,55 @@ +package command + +import ( + "context" + "errors" + "testing" + + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/account" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestDeleteAccountByIDSuccess is responsible to test DeleteAccountByID with success +func TestDeleteAccountByIDSuccess(t *testing.T) { + organizationID := uuid.New() + ledgerID := uuid.New() + portfolioID := uuid.New() + id := uuid.New() + uc := UseCase{ + AccountRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.AccountRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), organizationID, ledgerID, portfolioID, id). + Return(nil). + Times(1) + err := uc.AccountRepo.Delete(context.TODO(), organizationID, ledgerID, portfolioID, id) + + assert.Nil(t, err) +} + +// TestDeleteAccountByIDError is responsible to test DeleteAccountByID with error +func TestDeleteAccountByIDError(t *testing.T) { + organizationID := uuid.New() + ledgerID := uuid.New() + portfolioID := uuid.New() + id := uuid.New() + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + AccountRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.AccountRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), organizationID, ledgerID, portfolioID, id). + Return(errors.New(errMSG)). + Times(1) + err := uc.AccountRepo.Delete(context.TODO(), organizationID, ledgerID, portfolioID, id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) +} diff --git a/components/ledger/internal/app/command/delete-instrument.go b/components/ledger/internal/app/command/delete-instrument.go new file mode 100644 index 00000000..f641da68 --- /dev/null +++ b/components/ledger/internal/app/command/delete-instrument.go @@ -0,0 +1,37 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + "github.com/google/uuid" +) + +// DeleteInstrumentByID delete an instrument from the repository by ids. +func (uc *UseCase) DeleteInstrumentByID(ctx context.Context, organizationID, ledgerID, id uuid.UUID) error { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Remove instrument for id: %s", id) + + if err := uc.InstrumentRepo.Delete(ctx, organizationID, ledgerID, id); err != nil { + logger.Errorf("Error deleting instrument on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(i.Instrument{}).Name(), + Message: fmt.Sprintf("Instrument with id %s was not found", id), + Code: "INSTRUMENT_NOT_FOUND", + Err: err, + } + } + + return err + } + + return nil +} diff --git a/components/ledger/internal/app/command/delete-instrument_test.go b/components/ledger/internal/app/command/delete-instrument_test.go new file mode 100644 index 00000000..84fc5aec --- /dev/null +++ b/components/ledger/internal/app/command/delete-instrument_test.go @@ -0,0 +1,54 @@ +package command + +import ( + "context" + "errors" + "testing" + + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/instrument" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestDeleteInstrumentByIDSuccess is responsible to test DeleteInstrumentByID with success +func TestDeleteInstrumentByIDSuccess(t *testing.T) { + id := uuid.New() + ledgerID := uuid.New() + organizationID := uuid.New() + + uc := UseCase{ + InstrumentRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.InstrumentRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), organizationID, ledgerID, id). + Return(nil). + Times(1) + err := uc.InstrumentRepo.Delete(context.TODO(), organizationID, ledgerID, id) + + assert.Nil(t, err) +} + +// TestDeleteInstrumentByIDError is responsible to test DeleteInstrumentByID with error +func TestDeleteInstrumentByIDError(t *testing.T) { + id := uuid.New() + ledgerID := uuid.New() + organizationID := uuid.New() + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + InstrumentRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.InstrumentRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), organizationID, ledgerID, id). + Return(errors.New(errMSG)). + Times(1) + err := uc.InstrumentRepo.Delete(context.TODO(), organizationID, ledgerID, id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) +} diff --git a/components/ledger/internal/app/command/delete-ledger.go b/components/ledger/internal/app/command/delete-ledger.go new file mode 100644 index 00000000..c92bb363 --- /dev/null +++ b/components/ledger/internal/app/command/delete-ledger.go @@ -0,0 +1,37 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + "github.com/google/uuid" +) + +// DeleteLedgerByID deletes a ledger from the repository +func (uc *UseCase) DeleteLedgerByID(ctx context.Context, organizationID, id string) error { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Remove ledger for id: %s", id) + + if err := uc.LedgerRepo.Delete(ctx, uuid.MustParse(organizationID), uuid.MustParse(id)); err != nil { + logger.Errorf("Error deleting ledger on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(l.Ledger{}).Name(), + Message: fmt.Sprintf("Ledger with id %s was not found", id), + Code: "LEDGER_NOT_FOUND", + Err: err, + } + } + + return err + } + + return nil +} diff --git a/components/ledger/internal/app/command/delete-ledger_test.go b/components/ledger/internal/app/command/delete-ledger_test.go new file mode 100644 index 00000000..a0958e64 --- /dev/null +++ b/components/ledger/internal/app/command/delete-ledger_test.go @@ -0,0 +1,52 @@ +package command + +import ( + "context" + "errors" + "testing" + + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/ledger" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestDeleteLedgerByIDSuccess is responsible to test DeleteLedgerByID with success +func TestDeleteLedgerByIDSuccess(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + + uc := UseCase{ + LedgerRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.LedgerRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), organizationID, id). + Return(nil). + Times(1) + err := uc.LedgerRepo.Delete(context.TODO(), organizationID, id) + + assert.Nil(t, err) +} + +// TestDeleteLedgerByIDError is responsible to test DeleteLedgerByID with error +func TestDeleteLedgerByIDError(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + LedgerRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.LedgerRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), organizationID, id). + Return(errors.New(errMSG)). + Times(1) + err := uc.LedgerRepo.Delete(context.TODO(), organizationID, id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) +} diff --git a/components/ledger/internal/app/command/delete-matadata_test.go b/components/ledger/internal/app/command/delete-matadata_test.go new file mode 100644 index 00000000..3cf2c59d --- /dev/null +++ b/components/ledger/internal/app/command/delete-matadata_test.go @@ -0,0 +1,52 @@ +package command + +import ( + "context" + "errors" + "reflect" + "testing" + + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/metadata" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestMetadataDeleteSuccess is responsible to test MetadataDelete with success +func TestMetadataDeleteSuccess(t *testing.T) { + id := uuid.New().String() + collection := reflect.TypeOf(o.Organization{}).Name() + uc := UseCase{ + MetadataRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.MetadataRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), collection, id). + Return(nil). + Times(1) + + err := uc.MetadataRepo.Delete(context.TODO(), collection, id) + assert.Nil(t, err) +} + +// TestMetadataDeleteError is responsible to test MetadataDelete with error +func TestMetadataDeleteError(t *testing.T) { + errMSG := "err to delete metadata on mongodb" + id := uuid.New().String() + collection := reflect.TypeOf(o.Organization{}).Name() + uc := UseCase{ + MetadataRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.MetadataRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), collection, id). + Return(errors.New(errMSG)). + Times(1) + + err := uc.MetadataRepo.Delete(context.TODO(), collection, id) + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) +} diff --git a/components/ledger/internal/app/command/delete-organization.go b/components/ledger/internal/app/command/delete-organization.go new file mode 100644 index 00000000..bf8b359f --- /dev/null +++ b/components/ledger/internal/app/command/delete-organization.go @@ -0,0 +1,37 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + "github.com/google/uuid" +) + +// DeleteOrganizationByID fetch a new organization from the repository +func (uc *UseCase) DeleteOrganizationByID(ctx context.Context, id string) error { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Remove organization for id: %s", id) + + if err := uc.OrganizationRepo.Delete(ctx, uuid.MustParse(id)); err != nil { + logger.Errorf("Error deleting organization on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(o.Organization{}).Name(), + Message: fmt.Sprintf("Organization with id %s was not found", id), + Code: "ORGANIZATION_NOT_FOUND", + Err: err, + } + } + + return err + } + + return nil +} diff --git a/components/ledger/internal/app/command/delete-organization_test.go b/components/ledger/internal/app/command/delete-organization_test.go new file mode 100644 index 00000000..2a49c91c --- /dev/null +++ b/components/ledger/internal/app/command/delete-organization_test.go @@ -0,0 +1,50 @@ +package command + +import ( + "context" + "errors" + "testing" + + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/organization" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestDeleteOrganizationByIDSuccess is responsible to test DeleteOrganizationByID with success +func TestDeleteOrganizationByIDSuccess(t *testing.T) { + id := uuid.New() + + uc := UseCase{ + OrganizationRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.OrganizationRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), id). + Return(nil). + Times(1) + err := uc.OrganizationRepo.Delete(context.TODO(), id) + + assert.Nil(t, err) +} + +// TestDeleteOrganizationByIDError is responsible to test DeleteOrganizationByID with error +func TestDeleteOrganizationByIDError(t *testing.T) { + id := uuid.New() + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + OrganizationRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.OrganizationRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), id). + Return(errors.New(errMSG)). + Times(1) + err := uc.OrganizationRepo.Delete(context.TODO(), id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) +} diff --git a/components/ledger/internal/app/command/delete-portfolio.go b/components/ledger/internal/app/command/delete-portfolio.go new file mode 100644 index 00000000..68a6d2d9 --- /dev/null +++ b/components/ledger/internal/app/command/delete-portfolio.go @@ -0,0 +1,37 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + "github.com/google/uuid" +) + +// DeletePortfolioByID deletes a portfolio from the repository by ids. +func (uc *UseCase) DeletePortfolioByID(ctx context.Context, organizationID, ledgerID, id string) error { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Remove portfolio for id: %s", id) + + if err := uc.PortfolioRepo.Delete(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(id)); err != nil { + logger.Errorf("Error deleting portfolio on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Message: fmt.Sprintf("Portfolio with id %s was not found", id), + Code: "PORTFOLIO_NOT_FOUND", + Err: err, + } + } + + return err + } + + return nil +} diff --git a/components/ledger/internal/app/command/delete-portfolio_test.go b/components/ledger/internal/app/command/delete-portfolio_test.go new file mode 100644 index 00000000..113aee9c --- /dev/null +++ b/components/ledger/internal/app/command/delete-portfolio_test.go @@ -0,0 +1,54 @@ +package command + +import ( + "context" + "errors" + "testing" + + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/portfolio" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestDeletePortfolioByIDSuccess is responsible to test DeletePortfolioByID with success +func TestDeletePortfolioByIDSuccess(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + + uc := UseCase{ + PortfolioRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.PortfolioRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), organizationID, ledgerID, id). + Return(nil). + Times(1) + err := uc.PortfolioRepo.Delete(context.TODO(), organizationID, ledgerID, id) + + assert.Nil(t, err) +} + +// TestDeletePortfolioByIDError is responsible to test DeletePortfolioByID with error +func TestDeletePortfolioByIDError(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + PortfolioRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.PortfolioRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), organizationID, ledgerID, id). + Return(errors.New(errMSG)). + Times(1) + err := uc.PortfolioRepo.Delete(context.TODO(), organizationID, ledgerID, id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) +} diff --git a/components/ledger/internal/app/command/delete-product.go b/components/ledger/internal/app/command/delete-product.go new file mode 100644 index 00000000..6e8f15c8 --- /dev/null +++ b/components/ledger/internal/app/command/delete-product.go @@ -0,0 +1,37 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + r "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + "github.com/google/uuid" +) + +// DeleteProductByID delete a product from the repository by ids. +func (uc *UseCase) DeleteProductByID(ctx context.Context, organizationID, ledgerID, id string) error { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Remove product for id: %s", id) + + if err := uc.ProductRepo.Delete(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(id)); err != nil { + logger.Errorf("Error deleting product on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Message: fmt.Sprintf("Product with id %s was not found", id), + Code: "PRODUCT_NOT_FOUND", + Err: err, + } + } + + return err + } + + return nil +} diff --git a/components/ledger/internal/app/command/delete-product_test.go b/components/ledger/internal/app/command/delete-product_test.go new file mode 100644 index 00000000..5a3a264a --- /dev/null +++ b/components/ledger/internal/app/command/delete-product_test.go @@ -0,0 +1,54 @@ +package command + +import ( + "context" + "errors" + "testing" + + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/product" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestDeleteProductByIDSuccess is responsible to test DeleteProductByID with success +func TestDeleteProductByIDSuccess(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + + uc := UseCase{ + ProductRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.ProductRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), organizationID, ledgerID, id). + Return(nil). + Times(1) + err := uc.ProductRepo.Delete(context.TODO(), organizationID, ledgerID, id) + + assert.Nil(t, err) +} + +// TestDeleteProductByIDError is responsible to test DeleteProductByID with error +func TestDeleteProductByIDError(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + ProductRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.ProductRepo.(*mock.MockRepository). + EXPECT(). + Delete(gomock.Any(), organizationID, ledgerID, id). + Return(errors.New(errMSG)). + Times(1) + err := uc.ProductRepo.Delete(context.TODO(), organizationID, ledgerID, id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) +} diff --git a/components/ledger/internal/app/command/update-account.go b/components/ledger/internal/app/command/update-account.go new file mode 100644 index 00000000..fcecfe00 --- /dev/null +++ b/components/ledger/internal/app/command/update-account.go @@ -0,0 +1,66 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + "github.com/google/uuid" +) + +// UpdateAccountByID update an account from the repository by given id. +func (uc *UseCase) UpdateAccountByID(ctx context.Context, organizationID, ledgerID, portfolioID, id string, uai *a.UpdateAccountInput) (*a.Account, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to update account: %v", uai) + + account := &a.Account{ + Name: uai.Name, + Status: uai.Status, + Alias: uai.Alias, + AllowSending: uai.AllowSending, + AllowReceiving: uai.AllowReceiving, + ProductID: uai.ProductID, + Metadata: uai.Metadata, + } + + accountUpdated, err := uc.AccountRepo.Update(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(portfolioID), uuid.MustParse(id), account) + if err != nil { + logger.Errorf("Error updating account on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(a.Account{}).Name(), + Message: fmt.Sprintf("Account with id %s was not found", id), + Code: "ACCOUNT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if len(uai.Metadata) > 0 { + if err := common.CheckMetadataKeyAndValueLength(100, uai.Metadata); err != nil { + return nil, err + } + + err := uc.MetadataRepo.Update(ctx, reflect.TypeOf(a.Account{}).Name(), id, uai.Metadata) + if err != nil { + return nil, err + } + + accountUpdated.Metadata = uai.Metadata + } else { + err := uc.MetadataRepo.Delete(ctx, reflect.TypeOf(a.Account{}).Name(), id) + if err != nil { + return nil, err + } + } + + return accountUpdated, nil +} diff --git a/components/ledger/internal/app/command/update-account_test.go b/components/ledger/internal/app/command/update-account_test.go new file mode 100644 index 00000000..52686b6a --- /dev/null +++ b/components/ledger/internal/app/command/update-account_test.go @@ -0,0 +1,67 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/account" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestUpdateAccountByIDSuccess is responsible to test UpdateAccountByID with success +func TestUpdateAccountByIDSuccess(t *testing.T) { + organizationID := uuid.New() + ledgerID := uuid.New() + portfolioID := uuid.New() + id := uuid.New() + account := &a.Account{ + ID: id.String(), + UpdatedAt: time.Now(), + } + + uc := UseCase{ + AccountRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.AccountRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), organizationID, ledgerID, portfolioID, id, account). + Return(account, nil). + Times(1) + res, err := uc.AccountRepo.Update(context.TODO(), organizationID, ledgerID, portfolioID, id, account) + + assert.Equal(t, account, res) + assert.Nil(t, err) +} + +// TestUpdateAccountByIDError is responsible to test UpdateAccountByID with error +func TestUpdateAccountByIDError(t *testing.T) { + errMSG := "errDatabaseItemNotFound" + organizationID := uuid.New() + ledgerID := uuid.New() + portfolioID := uuid.New() + id := uuid.New() + account := &a.Account{ + ID: id.String(), + UpdatedAt: time.Now(), + } + + uc := UseCase{ + AccountRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.AccountRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), organizationID, ledgerID, portfolioID, id, account). + Return(nil, errors.New(errMSG)) + res, err := uc.AccountRepo.Update(context.TODO(), organizationID, ledgerID, portfolioID, id, account) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/command/update-instrument.go b/components/ledger/internal/app/command/update-instrument.go new file mode 100644 index 00000000..1dc1db77 --- /dev/null +++ b/components/ledger/internal/app/command/update-instrument.go @@ -0,0 +1,72 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + "github.com/google/uuid" +) + +// UpdateInstrumentByID update an instrument from the repository by given id. +func (uc *UseCase) UpdateInstrumentByID(ctx context.Context, organizationID, ledgerID uuid.UUID, id uuid.UUID, uii *i.UpdateInstrumentInput) (*i.Instrument, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to update instrument: %v", uii) + + if uii.Name == "" && uii.Status.IsEmpty() && uii.Metadata == nil { + return nil, common.UnprocessableOperationError{ + Message: "at least one of the allowed fields must be sent with a valid value [name, status.code, status.description, metadata]", + Code: "0006", + Err: nil, + } + } + + instrument := &i.Instrument{ + Name: uii.Name, + } + + if !uii.Status.IsEmpty() { + instrument.Status = uii.Status + } + + instrumentUpdated, err := uc.InstrumentRepo.Update(ctx, organizationID, ledgerID, id, instrument) + if err != nil { + logger.Errorf("Error updating instrument on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(i.Instrument{}).Name(), + Message: fmt.Sprintf("Instrument with ledger id %s and instrument id %s was not found", ledgerID, id), + Code: "INSTRUMENT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if len(uii.Metadata) > 0 { + if err := common.CheckMetadataKeyAndValueLength(100, uii.Metadata); err != nil { + return nil, err + } + + if err := uc.MetadataRepo.Update(ctx, reflect.TypeOf(i.Instrument{}).Name(), id.String(), uii.Metadata); err != nil { + return nil, err + } + + instrumentUpdated.Metadata = uii.Metadata + + return instrumentUpdated, nil + } + + if err := uc.MetadataRepo.Delete(ctx, reflect.TypeOf(i.Instrument{}).Name(), id.String()); err != nil { + return nil, err + } + + return instrumentUpdated, nil +} diff --git a/components/ledger/internal/app/command/update-instrument_test.go b/components/ledger/internal/app/command/update-instrument_test.go new file mode 100644 index 00000000..9988d277 --- /dev/null +++ b/components/ledger/internal/app/command/update-instrument_test.go @@ -0,0 +1,69 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/instrument" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestUpdateInstrumentByIDSuccess is responsible to test UpdateInstrumentByID with success +func TestUpdateInstrumentByIDSuccess(t *testing.T) { + id := uuid.New() + ledgerID := uuid.New() + organizationID := uuid.New() + instrument := &i.Instrument{ + ID: id.String(), + LedgerID: ledgerID.String(), + OrganizationID: organizationID.String(), + UpdatedAt: time.Now(), + } + + uc := UseCase{ + InstrumentRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.InstrumentRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), organizationID, ledgerID, id, instrument). + Return(instrument, nil). + Times(1) + res, err := uc.InstrumentRepo.Update(context.TODO(), organizationID, ledgerID, id, instrument) + + assert.Equal(t, instrument, res) + assert.Nil(t, err) +} + +// TestUpdateInstrumentByIDError is responsible to test UpdateInstrumentByID with error +func TestUpdateInstrumentByIDError(t *testing.T) { + errMSG := "errDatabaseItemNotFound" + id := uuid.New() + ledgerID := uuid.New() + organizationID := uuid.New() + instrument := &i.Instrument{ + ID: id.String(), + LedgerID: ledgerID.String(), + OrganizationID: organizationID.String(), + UpdatedAt: time.Now(), + } + + uc := UseCase{ + InstrumentRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.InstrumentRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), organizationID, ledgerID, id, instrument). + Return(nil, errors.New(errMSG)) + res, err := uc.InstrumentRepo.Update(context.TODO(), organizationID, ledgerID, id, instrument) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/command/update-ledger.go b/components/ledger/internal/app/command/update-ledger.go new file mode 100644 index 00000000..af6e8bb8 --- /dev/null +++ b/components/ledger/internal/app/command/update-ledger.go @@ -0,0 +1,70 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + "github.com/google/uuid" +) + +// UpdateLedgerByID update a ledger from the repository. +func (uc *UseCase) UpdateLedgerByID(ctx context.Context, organizationID, id string, uli *l.UpdateLedgerInput) (*l.Ledger, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to update ledger: %v", uli) + + if uli.Name == "" && uli.Status.IsEmpty() && uli.Metadata == nil { + return nil, common.UnprocessableOperationError{ + Message: "at least one of the allowed fields must be sent with a valid value [name, status.code, status.description, metadata]", + Code: "0006", + Err: nil, + } + } + + ledger := &l.Ledger{ + Name: uli.Name, + OrganizationID: organizationID, + Status: uli.Status, + } + + ledgerUpdated, err := uc.LedgerRepo.Update(ctx, uuid.MustParse(organizationID), uuid.MustParse(id), ledger) + if err != nil { + logger.Errorf("Error updating ledger on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(l.Ledger{}).Name(), + Message: fmt.Sprintf("Ledger with id %s was not found", id), + Code: "LEDGER_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if len(uli.Metadata) > 0 { + if err := common.CheckMetadataKeyAndValueLength(100, uli.Metadata); err != nil { + return nil, err + } + + if err := uc.MetadataRepo.Update(ctx, reflect.TypeOf(l.Ledger{}).Name(), id, uli.Metadata); err != nil { + return nil, err + } + + ledgerUpdated.Metadata = uli.Metadata + + return ledgerUpdated, nil + } + + if err := uc.MetadataRepo.Delete(ctx, reflect.TypeOf(l.Ledger{}).Name(), id); err != nil { + return nil, err + } + + return ledgerUpdated, nil +} diff --git a/components/ledger/internal/app/command/update-ledger_test.go b/components/ledger/internal/app/command/update-ledger_test.go new file mode 100644 index 00000000..0a16a39b --- /dev/null +++ b/components/ledger/internal/app/command/update-ledger_test.go @@ -0,0 +1,68 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/ledger" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestUpdateLedgerByIDSuccess is responsible to test UpdateLedgerByID with success +func TestUpdateLedgerByIDSuccess(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + + ledger := &l.Ledger{ + ID: id.String(), + OrganizationID: organizationID.String(), + UpdatedAt: time.Now(), + } + + uc := UseCase{ + LedgerRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.LedgerRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), organizationID, id, ledger). + Return(ledger, nil). + Times(1) + res, err := uc.LedgerRepo.Update(context.TODO(), organizationID, id, ledger) + + assert.Equal(t, ledger, res) + assert.Nil(t, err) +} + +// TestUpdateLedgerByIDError is responsible to test UpdateLedgerByID with error +func TestUpdateLedgerByIDError(t *testing.T) { + errMSG := "errDatabaseItemNotFound" + + id := uuid.New() + organizationID := uuid.New() + + ledger := &l.Ledger{ + ID: id.String(), + OrganizationID: organizationID.String(), + UpdatedAt: time.Now(), + } + + uc := UseCase{ + LedgerRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.LedgerRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), organizationID, id, ledger). + Return(nil, errors.New(errMSG)) + res, err := uc.LedgerRepo.Update(context.TODO(), organizationID, id, ledger) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/command/update-metadata_test.go b/components/ledger/internal/app/command/update-metadata_test.go new file mode 100644 index 00000000..7d65a552 --- /dev/null +++ b/components/ledger/internal/app/command/update-metadata_test.go @@ -0,0 +1,54 @@ +package command + +import ( + "context" + "errors" + "reflect" + "testing" + + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/metadata" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestMetadataUpdateSuccess is responsible to test MetadataUpdate with success +func TestMetadataUpdateSuccess(t *testing.T) { + id := uuid.New().String() + metadata := map[string]any{} + collection := reflect.TypeOf(o.Organization{}).Name() + uc := UseCase{ + MetadataRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.MetadataRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), collection, id, metadata). + Return(nil). + Times(1) + + err := uc.MetadataRepo.Update(context.TODO(), collection, id, metadata) + assert.Nil(t, err) +} + +// TestMetadataUpdateError is responsible to test MetadataUpdate with error +func TestMetadataUpdateError(t *testing.T) { + errMSG := "err to update metadata on mongodb" + id := uuid.New().String() + metadata := map[string]any{} + collection := reflect.TypeOf(o.Organization{}).Name() + uc := UseCase{ + MetadataRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.MetadataRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), collection, id, metadata). + Return(errors.New(errMSG)). + Times(1) + + err := uc.MetadataRepo.Update(context.TODO(), collection, id, metadata) + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) +} diff --git a/components/ledger/internal/app/command/update-organization.go b/components/ledger/internal/app/command/update-organization.go new file mode 100644 index 00000000..398cb3d2 --- /dev/null +++ b/components/ledger/internal/app/command/update-organization.go @@ -0,0 +1,71 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + "github.com/google/uuid" +) + +// UpdateOrganizationByID update an organization from the repository. +func (uc *UseCase) UpdateOrganizationByID(ctx context.Context, id string, uoi *o.UpdateOrganizationInput) (*o.Organization, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to update organization: %v", uoi) + + if uoi.LegalName == "" && uoi.DoingBusinessAs == nil && uoi.Address.IsEmpty() && uoi.Status.IsEmpty() && uoi.Metadata == nil { + return nil, common.UnprocessableOperationError{ + Message: "at least one of the allowed fields must be sent with a valid value [legalName, doingBusinessAs, address, status, metadata]", + Code: "0006", + Err: nil, + } + } + + organization := &o.Organization{ + LegalName: uoi.LegalName, + DoingBusinessAs: uoi.DoingBusinessAs, + Address: uoi.Address, + Status: uoi.Status, + } + + organizationUpdated, err := uc.OrganizationRepo.Update(ctx, uuid.MustParse(id), organization) + if err != nil { + logger.Errorf("Error updating organization on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(o.Organization{}).Name(), + Message: fmt.Sprintf("Organization with id %s was not found", id), + Code: "ORGANIZATION_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if len(uoi.Metadata) > 0 { + if err := common.CheckMetadataKeyAndValueLength(100, uoi.Metadata); err != nil { + return nil, err + } + + if err := uc.MetadataRepo.Update(ctx, reflect.TypeOf(o.Organization{}).Name(), id, uoi.Metadata); err != nil { + return nil, err + } + + organizationUpdated.Metadata = uoi.Metadata + + return organizationUpdated, nil + } + + if err := uc.MetadataRepo.Delete(ctx, reflect.TypeOf(o.Organization{}).Name(), id); err != nil { + return nil, err + } + + return organizationUpdated, nil +} diff --git a/components/ledger/internal/app/command/update-organization_test.go b/components/ledger/internal/app/command/update-organization_test.go new file mode 100644 index 00000000..ad1e2430 --- /dev/null +++ b/components/ledger/internal/app/command/update-organization_test.go @@ -0,0 +1,55 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/organization" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestUpdateOrganizationByIDSuccess is responsible to test UpdateOrganizationByID with success +func TestUpdateOrganizationByIDSuccess(t *testing.T) { + id := uuid.New() + organization := &o.Organization{ID: id.String(), UpdatedAt: time.Now()} + + uc := UseCase{ + OrganizationRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.OrganizationRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), id, organization). + Return(organization, nil). + Times(1) + res, err := uc.OrganizationRepo.Update(context.TODO(), id, organization) + + assert.Equal(t, organization, res) + assert.Nil(t, err) +} + +// TestUpdateOrganizationByIDError is responsible to test UpdateOrganizationByID with error +func TestUpdateOrganizationByIDError(t *testing.T) { + id := uuid.New() + errMSG := "errDatabaseItemNotFound" + organization := &o.Organization{ID: id.String(), UpdatedAt: time.Now()} + + uc := UseCase{ + OrganizationRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.OrganizationRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), id, organization). + Return(nil, errors.New(errMSG)) + res, err := uc.OrganizationRepo.Update(context.TODO(), id, organization) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/command/update-portfolio.go b/components/ledger/internal/app/command/update-portfolio.go new file mode 100644 index 00000000..effede74 --- /dev/null +++ b/components/ledger/internal/app/command/update-portfolio.go @@ -0,0 +1,69 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + "github.com/google/uuid" +) + +// UpdatePortfolioByID update a portfolio from the repository by given id. +func (uc *UseCase) UpdatePortfolioByID(ctx context.Context, organizationID, ledgerID, id string, upi *p.UpdatePortfolioInput) (*p.Portfolio, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to update portfolio: %v", upi) + + if upi.Name == "" && upi.Status.IsEmpty() && upi.Metadata == nil { + return nil, common.UnprocessableOperationError{ + Message: "at least one of the allowed fields must be sent with a valid value [name, status.code, status.description, metadata]", + Code: "0006", + Err: nil, + } + } + + portfolio := &p.Portfolio{ + Name: upi.Name, + Status: upi.Status, + } + + portfolioUpdated, err := uc.PortfolioRepo.Update(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(id), portfolio) + if err != nil { + logger.Errorf("Error updating portfolio on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Message: fmt.Sprintf("Portfolio with ledger id %s and portfolio id %s was not found", ledgerID, id), + Code: "PORTFOLIO_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if len(upi.Metadata) > 0 { + if err := common.CheckMetadataKeyAndValueLength(100, upi.Metadata); err != nil { + return nil, err + } + + if err := uc.MetadataRepo.Update(ctx, reflect.TypeOf(p.Portfolio{}).Name(), id, upi.Metadata); err != nil { + return nil, err + } + + portfolioUpdated.Metadata = upi.Metadata + + return portfolioUpdated, nil + } + + if err := uc.MetadataRepo.Delete(ctx, reflect.TypeOf(p.Portfolio{}).Name(), id); err != nil { + return nil, err + } + + return portfolioUpdated, nil +} diff --git a/components/ledger/internal/app/command/update-portfolio_test.go b/components/ledger/internal/app/command/update-portfolio_test.go new file mode 100644 index 00000000..78fef846 --- /dev/null +++ b/components/ledger/internal/app/command/update-portfolio_test.go @@ -0,0 +1,71 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/portfolio" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestUpdatePortfolioByIDSuccess is responsible to test UpdatePortfolioByID with success +func TestUpdatePortfolioByIDSuccess(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + portfolio := &p.Portfolio{ + ID: id.String(), + EntityID: uuid.New().String(), + OrganizationID: organizationID.String(), + LedgerID: ledgerID.String(), + UpdatedAt: time.Now(), + } + + uc := UseCase{ + PortfolioRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.PortfolioRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), organizationID, ledgerID, id, portfolio). + Return(portfolio, nil). + Times(1) + res, err := uc.PortfolioRepo.Update(context.TODO(), organizationID, ledgerID, id, portfolio) + + assert.Equal(t, portfolio, res) + assert.Nil(t, err) +} + +// TestUpdatePortfolioByIDError is responsible to test UpdatePortfolioByID with error +func TestUpdatePortfolioByIDError(t *testing.T) { + errMSG := "errDatabaseItemNotFound" + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + portfolio := &p.Portfolio{ + ID: id.String(), + OrganizationID: uuid.New().String(), + EntityID: uuid.New().String(), + LedgerID: ledgerID.String(), + UpdatedAt: time.Now(), + } + + uc := UseCase{ + PortfolioRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.PortfolioRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), organizationID, ledgerID, id, portfolio). + Return(nil, errors.New(errMSG)) + res, err := uc.PortfolioRepo.Update(context.TODO(), organizationID, ledgerID, id, portfolio) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/command/update-product.go b/components/ledger/internal/app/command/update-product.go new file mode 100644 index 00000000..80b4e57b --- /dev/null +++ b/components/ledger/internal/app/command/update-product.go @@ -0,0 +1,67 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + r "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + "github.com/google/uuid" +) + +// UpdateProductByID update a product from the repository by given id. +func (uc *UseCase) UpdateProductByID(ctx context.Context, organizationID, ledgerID, id string, upi *r.UpdateProductInput) (*r.Product, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Trying to update product: %v", upi) + + if upi.Name == "" && upi.Status.IsEmpty() && upi.Metadata == nil { + return nil, common.UnprocessableOperationError{ + Message: "at least one of the allowed fields must be sent with a valid value [name, status.code, status.description, metadata]", + Code: "0006", + Err: nil, + } + } + + product := &r.Product{ + Name: upi.Name, + Status: upi.Status, + } + + productUpdated, err := uc.ProductRepo.Update(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(id), product) + if err != nil { + logger.Errorf("Error updating product on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Message: fmt.Sprintf("Product with ledger id %s and product id %s was not found", ledgerID, id), + Code: "PRODUCT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if len(upi.Metadata) > 0 { + if err := common.CheckMetadataKeyAndValueLength(100, upi.Metadata); err != nil { + return nil, err + } + + if err := uc.MetadataRepo.Update(ctx, reflect.TypeOf(r.Product{}).Name(), id, upi.Metadata); err != nil { + return nil, err + } + + productUpdated.Metadata = upi.Metadata + } else { + if err := uc.MetadataRepo.Delete(ctx, reflect.TypeOf(r.Product{}).Name(), id); err != nil { + return nil, err + } + } + + return productUpdated, nil +} diff --git a/components/ledger/internal/app/command/update-product_test.go b/components/ledger/internal/app/command/update-product_test.go new file mode 100644 index 00000000..74aca49e --- /dev/null +++ b/components/ledger/internal/app/command/update-product_test.go @@ -0,0 +1,69 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + d "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/product" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestUpdateProductByIDSuccess is responsible to test UpdateProductByID with success +func TestUpdateProductByIDSuccess(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + product := &d.Product{ + ID: id.String(), + OrganizationID: organizationID.String(), + LedgerID: ledgerID.String(), + UpdatedAt: time.Now(), + } + + uc := UseCase{ + ProductRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.ProductRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), organizationID, ledgerID, id, product). + Return(product, nil). + Times(1) + res, err := uc.ProductRepo.Update(context.TODO(), organizationID, ledgerID, id, product) + + assert.Equal(t, product, res) + assert.Nil(t, err) +} + +// TestUpdateProductByIDError is responsible to test UpdateProductByID with error +func TestUpdateProductByIDError(t *testing.T) { + errMSG := "errDatabaseItemNotFound" + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + product := &d.Product{ + ID: id.String(), + OrganizationID: organizationID.String(), + LedgerID: ledgerID.String(), + UpdatedAt: time.Now(), + } + + uc := UseCase{ + ProductRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.ProductRepo.(*mock.MockRepository). + EXPECT(). + Update(gomock.Any(), organizationID, ledgerID, id, product). + Return(nil, errors.New(errMSG)) + res, err := uc.ProductRepo.Update(context.TODO(), organizationID, ledgerID, id, product) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/errors.go b/components/ledger/internal/app/errors.go new file mode 100644 index 00000000..de839564 --- /dev/null +++ b/components/ledger/internal/app/errors.go @@ -0,0 +1,6 @@ +package app + +import "errors" + +// ErrDatabaseItemNotFound is throws an item informed was not found +var ErrDatabaseItemNotFound = errors.New("errDatabaseItemNotFound") diff --git a/components/ledger/internal/app/query/get-all-accounts.go b/components/ledger/internal/app/query/get-all-accounts.go new file mode 100644 index 00000000..ba6993b1 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-accounts.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllAccount fetch all Account from the repository +func (uc *UseCase) GetAllAccount(ctx context.Context, organizationID, ledgerID, portfolioID string) ([]*a.Account, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving accounts") + + accounts, err := uc.AccountRepo.FindAll(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(portfolioID)) + if err != nil { + logger.Errorf("Error getting accounts on repo: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(a.Account{}).Name(), + Message: "Account was not found", + Code: "ACCOUNT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if accounts != nil { + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(a.Account{}).Name(), bson.M{}) + if err != nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(a.Account{}).Name(), + Message: "Metadata was not found", + Code: "ACCOUNT_NOT_FOUND", + Err: err, + } + } + + metadataMap := make(map[string]map[string]any, len(metadata)) + + for _, meta := range metadata { + metadataMap[meta.EntityID] = meta.Data + } + + for i := range accounts { + if data, ok := metadataMap[accounts[i].ID]; ok { + accounts[i].Metadata = data + } + } + } + + return accounts, nil +} diff --git a/components/ledger/internal/app/query/get-all-accounts_test.go b/components/ledger/internal/app/query/get-all-accounts_test.go new file mode 100644 index 00000000..3566a12a --- /dev/null +++ b/components/ledger/internal/app/query/get-all-accounts_test.go @@ -0,0 +1,55 @@ +package query + +import ( + "context" + "errors" + "testing" + + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/account" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetAllAccountsError is responsible to test GetAllAccounts with success and error +func TestGetAllAccounts(t *testing.T) { + organizationID := uuid.New() + ledgerID := uuid.New() + portfolioID := uuid.New() + + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockAccountRepo := mock.NewMockRepository(ctrl) + + uc := UseCase{ + AccountRepo: mockAccountRepo, + } + + t.Run("Success", func(t *testing.T) { + accounts := []*a.Account{{}} + mockAccountRepo. + EXPECT(). + FindAll(gomock.Any(), organizationID, ledgerID, portfolioID). + Return(accounts, nil). + Times(1) + res, err := uc.AccountRepo.FindAll(context.TODO(), organizationID, ledgerID, portfolioID) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMsg := "errDatabaseItemNotFound" + mockAccountRepo. + EXPECT(). + FindAll(gomock.Any(), organizationID, ledgerID, portfolioID). + Return(nil, errors.New(errMsg)). + Times(1) + res, err := uc.AccountRepo.FindAll(context.TODO(), organizationID, ledgerID, portfolioID) + + assert.EqualError(t, err, errMsg) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-all-instruments.go b/components/ledger/internal/app/query/get-all-instruments.go new file mode 100644 index 00000000..300b1d41 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-instruments.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllInstruments fetch all Instrument from the repository +func (uc *UseCase) GetAllInstruments(ctx context.Context, organizationID, ledgerID string) ([]*i.Instrument, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving instruments") + + instruments, err := uc.InstrumentRepo.FindAll(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID)) + if err != nil { + logger.Errorf("Error getting instruments on repo: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(i.Instrument{}).Name(), + Message: "Instrument was not found", + Code: "INSTRUMENT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if instruments != nil { + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(i.Instrument{}).Name(), bson.M{}) + if err != nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(i.Instrument{}).Name(), + Message: "Metadata was not found", + Code: "INSTRUMENT_NOT_FOUND", + Err: err, + } + } + + metadataMap := make(map[string]map[string]any, len(metadata)) + + for _, meta := range metadata { + metadataMap[meta.EntityID] = meta.Data + } + + for idx := range instruments { + if data, ok := metadataMap[instruments[idx].ID]; ok { + instruments[idx].Metadata = data + } + } + } + + return instruments, nil +} diff --git a/components/ledger/internal/app/query/get-all-instruments_test.go b/components/ledger/internal/app/query/get-all-instruments_test.go new file mode 100644 index 00000000..15c79478 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-instruments_test.go @@ -0,0 +1,54 @@ +package query + +import ( + "context" + "errors" + "testing" + + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/instrument" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetAllInstrumentsError is responsible to test GetAllInstruments with success and error +func TestGetAllInstruments(t *testing.T) { + ledgerID := uuid.New() + organizationID := uuid.New() + + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockInstrumentRepo := mock.NewMockRepository(ctrl) + + uc := UseCase{ + InstrumentRepo: mockInstrumentRepo, + } + + t.Run("Success", func(t *testing.T) { + instruments := []*i.Instrument{{}} + mockInstrumentRepo. + EXPECT(). + FindAll(gomock.Any(), organizationID, ledgerID). + Return(instruments, nil). + Times(1) + res, err := uc.InstrumentRepo.FindAll(context.TODO(), organizationID, ledgerID) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMsg := "errDatabaseItemNotFound" + mockInstrumentRepo. + EXPECT(). + FindAll(gomock.Any(), organizationID, ledgerID). + Return(nil, errors.New(errMsg)). + Times(1) + res, err := uc.InstrumentRepo.FindAll(context.TODO(), organizationID, ledgerID) + + assert.EqualError(t, err, errMsg) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-all-ledgers.go b/components/ledger/internal/app/query/get-all-ledgers.go new file mode 100644 index 00000000..70f59131 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-ledgers.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllLedgers fetch all Ledgers from the repository +func (uc *UseCase) GetAllLedgers(ctx context.Context, organizationID string) ([]*l.Ledger, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving ledgers") + + ledgers, err := uc.LedgerRepo.FindAll(ctx, uuid.MustParse(organizationID)) + if err != nil { + logger.Errorf("Error getting ledgers on repo: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(l.Ledger{}).Name(), + Message: "Ledgers was not found", + Code: "LEDGER_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if ledgers != nil { + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(l.Ledger{}).Name(), bson.M{}) + if err != nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(l.Ledger{}).Name(), + Message: "Metadata was not found", + Code: "LEDGER_NOT_FOUND", + Err: err, + } + } + + metadataMap := make(map[string]map[string]any, len(metadata)) + + for _, meta := range metadata { + metadataMap[meta.EntityID] = meta.Data + } + + for i := range ledgers { + if data, ok := metadataMap[ledgers[i].ID]; ok { + ledgers[i].Metadata = data + } + } + } + + return ledgers, nil +} diff --git a/components/ledger/internal/app/query/get-all-ledgers_test.go b/components/ledger/internal/app/query/get-all-ledgers_test.go new file mode 100644 index 00000000..78f1b9f0 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-ledgers_test.go @@ -0,0 +1,52 @@ +package query + +import ( + "context" + "errors" + "testing" + + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/ledger" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetAllLedgersError is responsible to test GetAllLedgers with success and error +func TestGetAllLedgers(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockLedgerRepo := mock.NewMockRepository(ctrl) + organizationID := uuid.New() + + uc := UseCase{ + LedgerRepo: mockLedgerRepo, + } + + t.Run("Success", func(t *testing.T) { + ledgers := []*l.Ledger{{}} + mockLedgerRepo. + EXPECT(). + FindAll(gomock.Any(), organizationID). + Return(ledgers, nil). + Times(1) + res, err := uc.LedgerRepo.FindAll(context.TODO(), organizationID) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMsg := "errDatabaseItemNotFound" + mockLedgerRepo. + EXPECT(). + FindAll(gomock.Any(), organizationID). + Return(nil, errors.New(errMsg)). + Times(1) + res, err := uc.LedgerRepo.FindAll(context.TODO(), organizationID) + + assert.EqualError(t, err, errMsg) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-all-metadata-accounts.go b/components/ledger/internal/app/query/get-all-metadata-accounts.go new file mode 100644 index 00000000..2b4a7104 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-accounts.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllMetadataAccounts fetch all Accounts from the repository +func (uc *UseCase) GetAllMetadataAccounts(ctx context.Context, key, value, organizationID, ledgerID, portfolioID string) ([]*a.Account, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving accounts") + + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(a.Account{}).Name(), bson.M{key: value}) + if err != nil || metadata == nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(a.Account{}).Name(), + Message: "Accounts by metadata was not found", + Code: "ACCOUNT_NOT_FOUND", + Err: err, + } + } + + uuids := make([]uuid.UUID, len(metadata)) + metadataMap := make(map[string]map[string]any, len(metadata)) + + for i, meta := range metadata { + uuids[i] = uuid.MustParse(meta.EntityID) + metadataMap[meta.EntityID] = meta.Data + } + + accounts, err := uc.AccountRepo.ListByIDs(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(portfolioID), uuids) + if err != nil { + logger.Errorf("Error getting accounts on repo by query params: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(a.Account{}).Name(), + Message: "Accounts by metadata was not found", + Code: "ACCOUNT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + for i := range accounts { + if data, ok := metadataMap[accounts[i].ID]; ok { + accounts[i].Metadata = data + } + } + + return accounts, nil +} diff --git a/components/ledger/internal/app/query/get-all-metadata-accounts_test.go b/components/ledger/internal/app/query/get-all-metadata-accounts_test.go new file mode 100644 index 00000000..9d740ea9 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-accounts_test.go @@ -0,0 +1,56 @@ +package query + +import ( + "context" + "errors" + "reflect" + "testing" + + meta "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/metadata" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +// TestGetAllMetadataAccounts is responsible to test TestGetAllMetadataAccounts with success and error +func TestGetAllMetadataAccounts(t *testing.T) { + collection := reflect.TypeOf(a.Account{}).Name() + filter := bson.M{"metadata": 1} + + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockMetadataRepo := mock.NewMockRepository(gomock.NewController(t)) + uc := UseCase{ + MetadataRepo: mockMetadataRepo, + } + + t.Run("Success", func(t *testing.T) { + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return([]*meta.Metadata{{ID: primitive.NewObjectID()}}, nil). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMSG := "errDatabaseItemNotFound" + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.EqualError(t, err, errMSG) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-all-metadata-instruments.go b/components/ledger/internal/app/query/get-all-metadata-instruments.go new file mode 100644 index 00000000..194c0b19 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-instruments.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllMetadataInstruments fetch all Instruments from the repository +func (uc *UseCase) GetAllMetadataInstruments(ctx context.Context, key, value, organizationID, ledgerID string) ([]*i.Instrument, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving instruments") + + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(i.Instrument{}).Name(), bson.M{key: value}) + if err != nil || metadata == nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(i.Instrument{}).Name(), + Message: "Instruments by metadata was not found", + Code: "INSTRUMENT_NOT_FOUND", + Err: err, + } + } + + uuids := make([]uuid.UUID, len(metadata)) + metadataMap := make(map[string]map[string]any, len(metadata)) + + for idx, meta := range metadata { + uuids[idx] = uuid.MustParse(meta.EntityID) + metadataMap[meta.EntityID] = meta.Data + } + + instruments, err := uc.InstrumentRepo.ListByIDs(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuids) + if err != nil { + logger.Errorf("Error getting instruments on repo by query params: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(i.Instrument{}).Name(), + Message: "Instruments by metadata was not found", + Code: "INSTRUMENT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + for idx := range instruments { + if data, ok := metadataMap[instruments[idx].ID]; ok { + instruments[idx].Metadata = data + } + } + + return instruments, nil +} diff --git a/components/ledger/internal/app/query/get-all-metadata-instruments_test.go b/components/ledger/internal/app/query/get-all-metadata-instruments_test.go new file mode 100644 index 00000000..9ecf4a09 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-instruments_test.go @@ -0,0 +1,56 @@ +package query + +import ( + "context" + "errors" + "reflect" + "testing" + + meta "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/metadata" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +// TestGetAllMetadataInstruments is responsible to test TestGetAllMetadataInstruments with success and error +func TestGetAllMetadataInstruments(t *testing.T) { + collection := reflect.TypeOf(i.Instrument{}).Name() + filter := bson.M{"metadata": 1} + + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockMetadataRepo := mock.NewMockRepository(gomock.NewController(t)) + uc := UseCase{ + MetadataRepo: mockMetadataRepo, + } + + t.Run("Success", func(t *testing.T) { + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return([]*meta.Metadata{{ID: primitive.NewObjectID()}}, nil). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMSG := "errDatabaseItemNotFound" + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.EqualError(t, err, errMSG) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-all-metadata-ledgers.go b/components/ledger/internal/app/query/get-all-metadata-ledgers.go new file mode 100644 index 00000000..5df2024d --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-ledgers.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllMetadataLedgers fetch all Ledgers from the repository +func (uc *UseCase) GetAllMetadataLedgers(ctx context.Context, key, value, organizationID string) ([]*l.Ledger, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving ledgers") + + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(l.Ledger{}).Name(), bson.M{key: value}) + if err != nil || metadata == nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(l.Ledger{}).Name(), + Message: "Ledgers by metadata was not found", + Code: "LEDGER_NOT_FOUND", + Err: err, + } + } + + uuids := make([]uuid.UUID, len(metadata)) + metadataMap := make(map[string]map[string]any, len(metadata)) + + for i, meta := range metadata { + uuids[i] = uuid.MustParse(meta.EntityID) + metadataMap[meta.EntityID] = meta.Data + } + + ledgers, err := uc.LedgerRepo.ListByIDs(ctx, uuid.MustParse(organizationID), uuids) + if err != nil { + logger.Errorf("Error getting ledgers on repo by query params: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(l.Ledger{}).Name(), + Message: "Ledgers by metadata was not found", + Code: "LEDGER_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + for i := range ledgers { + if data, ok := metadataMap[ledgers[i].ID]; ok { + ledgers[i].Metadata = data + } + } + + return ledgers, nil +} diff --git a/components/ledger/internal/app/query/get-all-metadata-ledgers_test.go b/components/ledger/internal/app/query/get-all-metadata-ledgers_test.go new file mode 100644 index 00000000..eadb7abe --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-ledgers_test.go @@ -0,0 +1,56 @@ +package query + +import ( + "context" + "errors" + "reflect" + "testing" + + meta "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/metadata" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +// TestGetAllMetadataLedgers is responsible to test TestGetAllMetadataLedgers with success and error +func TestGetAllMetadataLedgers(t *testing.T) { + collection := reflect.TypeOf(l.Ledger{}).Name() + filter := bson.M{"metadata": 1} + + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockMetadataRepo := mock.NewMockRepository(gomock.NewController(t)) + uc := UseCase{ + MetadataRepo: mockMetadataRepo, + } + + t.Run("Success", func(t *testing.T) { + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return([]*meta.Metadata{{ID: primitive.NewObjectID()}}, nil). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMSG := "errDatabaseItemNotFound" + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.EqualError(t, err, errMSG) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-all-metadata-organizations.go b/components/ledger/internal/app/query/get-all-metadata-organizations.go new file mode 100644 index 00000000..743c7c79 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-organizations.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllMetadataOrganizations fetch all Organizations from the repository +func (uc *UseCase) GetAllMetadataOrganizations(ctx context.Context, key string, value string) ([]*o.Organization, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving organizations") + + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(o.Organization{}).Name(), bson.M{key: value}) + if err != nil || metadata == nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(o.Organization{}).Name(), + Message: "Organizations by metadata was not found", + Code: "ORGANIZATION_NOT_FOUND", + Err: err, + } + } + + uuids := make([]uuid.UUID, len(metadata)) + metadataMap := make(map[string]map[string]any, len(metadata)) + + for i, meta := range metadata { + uuids[i] = uuid.MustParse(meta.EntityID) + metadataMap[meta.EntityID] = meta.Data + } + + organizations, err := uc.OrganizationRepo.ListByIDs(ctx, uuids) + if err != nil { + logger.Errorf("Error getting organizations on repo by query params: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(o.Organization{}).Name(), + Message: "Organizations by metadata was not found", + Code: "ORGANIZATION_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + for i := range organizations { + if data, ok := metadataMap[organizations[i].ID]; ok { + organizations[i].Metadata = data + } + } + + return organizations, nil +} diff --git a/components/ledger/internal/app/query/get-all-metadata-organizations_test.go b/components/ledger/internal/app/query/get-all-metadata-organizations_test.go new file mode 100644 index 00000000..4bc89977 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-organizations_test.go @@ -0,0 +1,56 @@ +package query + +import ( + "context" + "errors" + "reflect" + "testing" + + meta "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/metadata" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +// TestGetAllMetadataOrganizations is responsible to test TestGetAllMetadataOrganizations with success and error +func TestGetAllMetadataOrganizations(t *testing.T) { + collection := reflect.TypeOf(o.Organization{}).Name() + filter := bson.M{"metadata": 1} + + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockMetadataRepo := mock.NewMockRepository(gomock.NewController(t)) + uc := UseCase{ + MetadataRepo: mockMetadataRepo, + } + + t.Run("Success", func(t *testing.T) { + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return([]*meta.Metadata{{ID: primitive.NewObjectID()}}, nil). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMSG := "errDatabaseItemNotFound" + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.EqualError(t, err, errMSG) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-all-metadata-portfolios.go b/components/ledger/internal/app/query/get-all-metadata-portfolios.go new file mode 100644 index 00000000..61617abb --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-portfolios.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllMetadataPortfolios fetch all Portfolios from the repository +func (uc *UseCase) GetAllMetadataPortfolios(ctx context.Context, key, value, organizationID, ledgerID string) ([]*p.Portfolio, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving portfolios") + + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(p.Portfolio{}).Name(), bson.M{key: value}) + if err != nil || metadata == nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Message: "Portfolios by metadata was not found", + Code: "PORTFOLIO_NOT_FOUND", + Err: err, + } + } + + uuids := make([]uuid.UUID, len(metadata)) + metadataMap := make(map[string]map[string]any, len(metadata)) + + for i, meta := range metadata { + uuids[i] = uuid.MustParse(meta.EntityID) + metadataMap[meta.EntityID] = meta.Data + } + + portfolios, err := uc.PortfolioRepo.ListByIDs(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuids) + if err != nil { + logger.Errorf("Error getting portfolios on repo by query params: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Message: "Portfolios by metadata was not found", + Code: "PORTFOLIO_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + for i := range portfolios { + if data, ok := metadataMap[portfolios[i].ID]; ok { + portfolios[i].Metadata = data + } + } + + return portfolios, nil +} diff --git a/components/ledger/internal/app/query/get-all-metadata-portfolios_test.go b/components/ledger/internal/app/query/get-all-metadata-portfolios_test.go new file mode 100644 index 00000000..9d002769 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-portfolios_test.go @@ -0,0 +1,56 @@ +package query + +import ( + "context" + "errors" + "reflect" + "testing" + + meta "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/metadata" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +// TestGetAllMetadataPortfolios is responsible to test TestGetAllMetadataPortfolios with success and error +func TestGetAllMetadataPortfolios(t *testing.T) { + collection := reflect.TypeOf(p.Portfolio{}).Name() + filter := bson.M{"metadata": 1} + + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockMetadataRepo := mock.NewMockRepository(gomock.NewController(t)) + uc := UseCase{ + MetadataRepo: mockMetadataRepo, + } + + t.Run("Success", func(t *testing.T) { + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return([]*meta.Metadata{{ID: primitive.NewObjectID()}}, nil). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMSG := "errDatabaseItemNotFound" + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.EqualError(t, err, errMSG) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-all-metadata-products.go b/components/ledger/internal/app/query/get-all-metadata-products.go new file mode 100644 index 00000000..cef8bb2f --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-products.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + r "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllMetadataProducts fetch all Products from the repository +func (uc *UseCase) GetAllMetadataProducts(ctx context.Context, key, value, organizationID, ledgerID string) ([]*r.Product, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving products") + + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(r.Product{}).Name(), bson.M{key: value}) + if err != nil || metadata == nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Message: "Products by metadata was not found", + Code: "PRODUCT_NOT_FOUND", + Err: err, + } + } + + uuids := make([]uuid.UUID, len(metadata)) + metadataMap := make(map[string]map[string]any, len(metadata)) + + for i, meta := range metadata { + uuids[i] = uuid.MustParse(meta.EntityID) + metadataMap[meta.EntityID] = meta.Data + } + + products, err := uc.ProductRepo.FindByIDs(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuids) + if err != nil { + logger.Errorf("Error getting products on repo by query params: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Message: "Products by metadata was not found", + Code: "PRODUCT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + for i := range products { + if data, ok := metadataMap[products[i].ID]; ok { + products[i].Metadata = data + } + } + + return products, nil +} diff --git a/components/ledger/internal/app/query/get-all-metadata-products_test.go b/components/ledger/internal/app/query/get-all-metadata-products_test.go new file mode 100644 index 00000000..d4a51d66 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-metadata-products_test.go @@ -0,0 +1,56 @@ +package query + +import ( + "context" + "errors" + "reflect" + "testing" + + meta "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/metadata" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +// TestGetAllMetadataProducts is responsible to test TestGetAllMetadataProducts with success and error +func TestGetAllMetadataProducts(t *testing.T) { + collection := reflect.TypeOf(p.Product{}).Name() + filter := bson.M{"metadata": 1} + + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockMetadataRepo := mock.NewMockRepository(gomock.NewController(t)) + uc := UseCase{ + MetadataRepo: mockMetadataRepo, + } + + t.Run("Success", func(t *testing.T) { + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return([]*meta.Metadata{{ID: primitive.NewObjectID()}}, nil). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMSG := "errDatabaseItemNotFound" + mockMetadataRepo. + EXPECT(). + FindList(gomock.Any(), collection, filter). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.MetadataRepo.FindList(context.TODO(), collection, filter) + + assert.EqualError(t, err, errMSG) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-all-organizations.go b/components/ledger/internal/app/query/get-all-organizations.go new file mode 100644 index 00000000..76d474e2 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-organizations.go @@ -0,0 +1,61 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllOrganizations fetch all Organizations from the repository +func (uc *UseCase) GetAllOrganizations(ctx context.Context) ([]*o.Organization, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving organizations") + + organizations, err := uc.OrganizationRepo.FindAll(ctx) + if err != nil { + logger.Errorf("Error getting organizations on repo: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(o.Organization{}).Name(), + Message: "Organizations was not found", + Code: "ORGANIZATION_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if organizations != nil { + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(o.Organization{}).Name(), bson.M{}) + if err != nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(o.Organization{}).Name(), + Message: "Metadata was not found", + Code: "ORGANIZATION_NOT_FOUND", + Err: err, + } + } + + metadataMap := make(map[string]map[string]any, len(metadata)) + + for _, meta := range metadata { + metadataMap[meta.EntityID] = meta.Data + } + + for i := range organizations { + if data, ok := metadataMap[organizations[i].ID]; ok { + organizations[i].Metadata = data + } + } + } + + return organizations, nil +} diff --git a/components/ledger/internal/app/query/get-all-organizations_test.go b/components/ledger/internal/app/query/get-all-organizations_test.go new file mode 100644 index 00000000..2523fe98 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-organizations_test.go @@ -0,0 +1,50 @@ +package query + +import ( + "context" + "errors" + "testing" + + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/organization" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetAllOrganizationsError is responsible to test GetAllOrganizations with success and error +func TestGetAllOrganizations(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockOrganizationRepo := mock.NewMockRepository(ctrl) + + uc := UseCase{ + OrganizationRepo: mockOrganizationRepo, + } + + t.Run("Success", func(t *testing.T) { + organizations := []*o.Organization{{}} + mockOrganizationRepo. + EXPECT(). + FindAll(gomock.Any()). + Return(organizations, nil). + Times(1) + res, err := uc.OrganizationRepo.FindAll(context.TODO()) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMsg := "errDatabaseItemNotFound" + mockOrganizationRepo. + EXPECT(). + FindAll(gomock.Any()). + Return(nil, errors.New(errMsg)). + Times(1) + res, err := uc.OrganizationRepo.FindAll(context.TODO()) + + assert.EqualError(t, err, errMsg) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-all-portfolios.go b/components/ledger/internal/app/query/get-all-portfolios.go new file mode 100644 index 00000000..c8189eb4 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-portfolios.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllPortfolio fetch all Portfolio from the repository +func (uc *UseCase) GetAllPortfolio(ctx context.Context, organizationID, ledgerID string) ([]*p.Portfolio, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving portfolios") + + portfolios, err := uc.PortfolioRepo.FindAll(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID)) + if err != nil { + logger.Errorf("Error getting portfolios on repo: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Message: "Portfolio was not found", + Code: "PORTFOLIO_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if portfolios != nil { + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(p.Portfolio{}).Name(), bson.M{}) + if err != nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Message: "Metadata was not found", + Code: "PORTFOLIO_NOT_FOUND", + Err: err, + } + } + + metadataMap := make(map[string]map[string]any, len(metadata)) + + for _, meta := range metadata { + metadataMap[meta.EntityID] = meta.Data + } + + for i := range portfolios { + if data, ok := metadataMap[portfolios[i].ID]; ok { + portfolios[i].Metadata = data + } + } + } + + return portfolios, nil +} diff --git a/components/ledger/internal/app/query/get-all-portfolios_test.go b/components/ledger/internal/app/query/get-all-portfolios_test.go new file mode 100644 index 00000000..3b960aad --- /dev/null +++ b/components/ledger/internal/app/query/get-all-portfolios_test.go @@ -0,0 +1,54 @@ +package query + +import ( + "context" + "errors" + "testing" + + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/portfolio" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetAllPortfoliosError is responsible to test GetAllPortfolios with success and error +func TestGetAllPortfolios(t *testing.T) { + organizationID := uuid.New() + ledgerID := uuid.New() + + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockPortfolioRepo := mock.NewMockRepository(ctrl) + + uc := UseCase{ + PortfolioRepo: mockPortfolioRepo, + } + + t.Run("Success", func(t *testing.T) { + portfolios := []*p.Portfolio{{}} + mockPortfolioRepo. + EXPECT(). + FindAll(gomock.Any(), organizationID, ledgerID). + Return(portfolios, nil). + Times(1) + res, err := uc.PortfolioRepo.FindAll(context.TODO(), organizationID, ledgerID) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMsg := "errDatabaseItemNotFound" + mockPortfolioRepo. + EXPECT(). + FindAll(gomock.Any(), organizationID, ledgerID). + Return(nil, errors.New(errMsg)). + Times(1) + res, err := uc.PortfolioRepo.FindAll(context.TODO(), organizationID, ledgerID) + + assert.EqualError(t, err, errMsg) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-all-products.go b/components/ledger/internal/app/query/get-all-products.go new file mode 100644 index 00000000..f127ab12 --- /dev/null +++ b/components/ledger/internal/app/query/get-all-products.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + r "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// GetAllProducts fetch all Product from the repository +func (uc *UseCase) GetAllProducts(ctx context.Context, organizationID, ledgerID string) ([]*r.Product, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving products") + + products, err := uc.ProductRepo.FindAll(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID)) + if err != nil { + logger.Errorf("Error getting products on repo: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Message: "Product was not found", + Code: "PRODUCT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if products != nil { + metadata, err := uc.MetadataRepo.FindList(ctx, reflect.TypeOf(r.Product{}).Name(), bson.M{}) + if err != nil { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Message: "Metadata was not found", + Code: "PRODUCT_NOT_FOUND", + Err: err, + } + } + + metadataMap := make(map[string]map[string]any, len(metadata)) + + for _, meta := range metadata { + metadataMap[meta.EntityID] = meta.Data + } + + for i := range products { + if data, ok := metadataMap[products[i].ID]; ok { + products[i].Metadata = data + } + } + } + + return products, nil +} diff --git a/components/ledger/internal/app/query/get-all-products_test.go b/components/ledger/internal/app/query/get-all-products_test.go new file mode 100644 index 00000000..7c372d8d --- /dev/null +++ b/components/ledger/internal/app/query/get-all-products_test.go @@ -0,0 +1,54 @@ +package query + +import ( + "context" + "errors" + "testing" + + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/product" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetAllProductsError is responsible to test GetAllProducts with success and error +func TestGetAllProducts(t *testing.T) { + organizationID := uuid.New() + ledgerID := uuid.New() + + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockProductRepo := mock.NewMockRepository(ctrl) + + uc := UseCase{ + ProductRepo: mockProductRepo, + } + + t.Run("Success", func(t *testing.T) { + products := []*p.Product{{}} + mockProductRepo. + EXPECT(). + FindAll(gomock.Any(), organizationID, ledgerID). + Return(products, nil). + Times(1) + res, err := uc.ProductRepo.FindAll(context.TODO(), organizationID, ledgerID) + + assert.NoError(t, err) + assert.Len(t, res, 1) + }) + + t.Run("Error", func(t *testing.T) { + errMsg := "errDatabaseItemNotFound" + mockProductRepo. + EXPECT(). + FindAll(gomock.Any(), organizationID, ledgerID). + Return(nil, errors.New(errMsg)). + Times(1) + res, err := uc.ProductRepo.FindAll(context.TODO(), organizationID, ledgerID) + + assert.EqualError(t, err, errMsg) + assert.Nil(t, res) + }) +} diff --git a/components/ledger/internal/app/query/get-id-account.go b/components/ledger/internal/app/query/get-id-account.go new file mode 100644 index 00000000..80f7160b --- /dev/null +++ b/components/ledger/internal/app/query/get-id-account.go @@ -0,0 +1,50 @@ +package query + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + "github.com/google/uuid" +) + +// GetAccountByID get an Account from the repository by given id. +func (uc *UseCase) GetAccountByID(ctx context.Context, organizationID, ledgerID, portfolioID, id string) (*a.Account, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving account for id: %s", id) + + account, err := uc.AccountRepo.Find(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(portfolioID), uuid.MustParse(id)) + if err != nil { + logger.Errorf("Error getting account on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(a.Account{}).Name(), + Message: fmt.Sprintf("Account with id %s was not found", id), + Code: "ACCOUNT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if account != nil { + metadata, err := uc.MetadataRepo.FindByEntity(ctx, reflect.TypeOf(a.Account{}).Name(), id) + if err != nil { + 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/app/query/get-id-account_test.go b/components/ledger/internal/app/query/get-id-account_test.go new file mode 100644 index 00000000..179d51d7 --- /dev/null +++ b/components/ledger/internal/app/query/get-id-account_test.go @@ -0,0 +1,67 @@ +package query + +import ( + "context" + "errors" + "testing" + + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/account" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetAccountByIDSuccess is responsible to test GetAccountByID with success +func TestGetAccountByIDSuccess(t *testing.T) { + organizationID := uuid.New() + ledgerID := uuid.New() + portfolioID := uuid.New() + id := uuid.New() + + account := &a.Account{ + ID: id.String(), + OrganizationID: organizationID.String(), + LedgerID: ledgerID.String(), + PortfolioID: portfolioID.String(), + } + + uc := UseCase{ + AccountRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.AccountRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), organizationID, ledgerID, portfolioID, id). + Return(account, nil). + Times(1) + res, err := uc.AccountRepo.Find(context.TODO(), organizationID, ledgerID, portfolioID, id) + + assert.Equal(t, res, account) + assert.Nil(t, err) +} + +// TestGetAccountByIDError is responsible to test GetAccountByID with error +func TestGetAccountByIDError(t *testing.T) { + organizationID := uuid.New() + ledgerID := uuid.New() + portfolioID := uuid.New() + id := uuid.New() + + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + AccountRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.AccountRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), organizationID, ledgerID, portfolioID, id). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.AccountRepo.Find(context.TODO(), organizationID, ledgerID, portfolioID, id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/query/get-id-instrument.go b/components/ledger/internal/app/query/get-id-instrument.go new file mode 100644 index 00000000..9f620466 --- /dev/null +++ b/components/ledger/internal/app/query/get-id-instrument.go @@ -0,0 +1,50 @@ +package query + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + "github.com/google/uuid" +) + +// GetInstrumentByID get an Instrument from the repository by given id. +func (uc *UseCase) GetInstrumentByID(ctx context.Context, organizationID, ledgerID, id string) (*i.Instrument, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving instrument for id: %s", id) + + instrument, err := uc.InstrumentRepo.Find(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(id)) + if err != nil { + logger.Errorf("Error getting instrument on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(i.Instrument{}).Name(), + Message: fmt.Sprintf("Instrument with id %s was not found", id), + Code: "INSTRUMENT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if instrument != nil { + metadata, err := uc.MetadataRepo.FindByEntity(ctx, reflect.TypeOf(i.Instrument{}).Name(), id) + if err != nil { + logger.Errorf("Error get metadata on mongodb instrument: %v", err) + return nil, err + } + + if metadata != nil { + instrument.Metadata = metadata.Data + } + } + + return instrument, nil +} diff --git a/components/ledger/internal/app/query/get-id-instrument_test.go b/components/ledger/internal/app/query/get-id-instrument_test.go new file mode 100644 index 00000000..71e07012 --- /dev/null +++ b/components/ledger/internal/app/query/get-id-instrument_test.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "testing" + + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/instrument" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetInstrumentByIDSuccess is responsible to test GetInstrumentByID with success +func TestGetInstrumentByIDSuccess(t *testing.T) { + id := uuid.New() + ledgerID := uuid.New() + organizationID := uuid.New() + instrument := &i.Instrument{ + ID: id.String(), + LedgerID: ledgerID.String(), + OrganizationID: organizationID.String(), + } + + uc := UseCase{ + InstrumentRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.InstrumentRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), organizationID, ledgerID, id). + Return(instrument, nil). + Times(1) + res, err := uc.InstrumentRepo.Find(context.TODO(), organizationID, ledgerID, id) + + assert.Equal(t, res, instrument) + assert.Nil(t, err) +} + +// TestGetInstrumentByIDError is responsible to test GetInstrumentByID with error +func TestGetInstrumentByIDError(t *testing.T) { + id := uuid.New() + ledgerID := uuid.New() + organizationID := uuid.New() + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + InstrumentRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.InstrumentRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), organizationID, ledgerID, id). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.InstrumentRepo.Find(context.TODO(), organizationID, ledgerID, id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/query/get-id-ledger.go b/components/ledger/internal/app/query/get-id-ledger.go new file mode 100644 index 00000000..b7d5a615 --- /dev/null +++ b/components/ledger/internal/app/query/get-id-ledger.go @@ -0,0 +1,50 @@ +package query + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + "github.com/google/uuid" +) + +// GetLedgerByID Get a ledger from the repository by given id. +func (uc *UseCase) GetLedgerByID(ctx context.Context, organizationID, id string) (*l.Ledger, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving ledger for id: %s", id) + + ledger, err := uc.LedgerRepo.Find(ctx, uuid.MustParse(organizationID), uuid.MustParse(id)) + if err != nil { + logger.Errorf("Error getting ledger on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(l.Ledger{}).Name(), + Message: fmt.Sprintf("Ledger with id %s was not found", id), + Code: "LEDGER_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if ledger != nil { + metadata, err := uc.MetadataRepo.FindByEntity(ctx, reflect.TypeOf(l.Ledger{}).Name(), id) + if err != nil { + logger.Errorf("Error get metadata on mongodb ledger: %v", err) + return nil, err + } + + if metadata != nil { + ledger.Metadata = metadata.Data + } + } + + return ledger, nil +} diff --git a/components/ledger/internal/app/query/get-id-ledger_test.go b/components/ledger/internal/app/query/get-id-ledger_test.go new file mode 100644 index 00000000..4ddfa55b --- /dev/null +++ b/components/ledger/internal/app/query/get-id-ledger_test.go @@ -0,0 +1,56 @@ +package query + +import ( + "context" + "errors" + "testing" + + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/ledger" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetLedgerByIDSuccess is responsible to test GetLedgerByID with success +func TestGetLedgerByIDSuccess(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + ledger := &l.Ledger{ID: id.String()} + + uc := UseCase{ + LedgerRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.LedgerRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), organizationID, id). + Return(ledger, nil). + Times(1) + res, err := uc.LedgerRepo.Find(context.TODO(), organizationID, id) + + assert.Equal(t, res, ledger) + assert.Nil(t, err) +} + +// TestGetLedgerByIDError is responsible to test GetLedgerByID with error +func TestGetLedgerByIDError(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + LedgerRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.LedgerRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), organizationID, id). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.LedgerRepo.Find(context.TODO(), organizationID, id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/query/get-id-metadata_test.go b/components/ledger/internal/app/query/get-id-metadata_test.go new file mode 100644 index 00000000..6638ecbd --- /dev/null +++ b/components/ledger/internal/app/query/get-id-metadata_test.go @@ -0,0 +1,58 @@ +package query + +import ( + "context" + "errors" + "reflect" + "testing" + + meta "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/metadata" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +// TestMetadataFindByEntitySuccess is responsible to test MetadataFindByEntity with success +func TestMetadataFindByEntitySuccess(t *testing.T) { + id := uuid.New().String() + collection := reflect.TypeOf(o.Organization{}).Name() + metadata := &meta.Metadata{ID: primitive.NewObjectID()} + uc := UseCase{ + MetadataRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.MetadataRepo.(*mock.MockRepository). + EXPECT(). + FindByEntity(gomock.Any(), collection, id). + Return(metadata, nil). + Times(1) + + res, err := uc.MetadataRepo.FindByEntity(context.TODO(), collection, id) + + assert.Equal(t, res, metadata) + assert.Nil(t, err) +} + +// TestMetadataFindByEntityError is responsible to test MetadataFindByEntity with error +func TestMetadataFindByEntityError(t *testing.T) { + errMSG := "err to findByEntity metadata on mongodb" + id := uuid.New().String() + collection := reflect.TypeOf(o.Organization{}).Name() + uc := UseCase{ + MetadataRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.MetadataRepo.(*mock.MockRepository). + EXPECT(). + FindByEntity(gomock.Any(), collection, id). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.MetadataRepo.FindByEntity(context.TODO(), collection, id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/query/get-id-organization.go b/components/ledger/internal/app/query/get-id-organization.go new file mode 100644 index 00000000..ef318f95 --- /dev/null +++ b/components/ledger/internal/app/query/get-id-organization.go @@ -0,0 +1,50 @@ +package query + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + "github.com/google/uuid" +) + +// GetOrganizationByID fetch a new organization from the repository +func (uc *UseCase) GetOrganizationByID(ctx context.Context, id string) (*o.Organization, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving organization for id: %s", id) + + organization, err := uc.OrganizationRepo.Find(ctx, uuid.MustParse(id)) + if err != nil { + logger.Errorf("Error getting organization on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(o.Organization{}).Name(), + Message: fmt.Sprintf("Organization with id %s was not found", id), + Code: "ORGANIZATION_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if organization != nil { + metadata, err := uc.MetadataRepo.FindByEntity(ctx, reflect.TypeOf(o.Organization{}).Name(), id) + if err != nil { + logger.Errorf("Error get metadata on mongodb organization: %v", err) + return nil, err + } + + if metadata != nil { + organization.Metadata = metadata.Data + } + } + + return organization, nil +} diff --git a/components/ledger/internal/app/query/get-id-organization_test.go b/components/ledger/internal/app/query/get-id-organization_test.go new file mode 100644 index 00000000..d55a1ece --- /dev/null +++ b/components/ledger/internal/app/query/get-id-organization_test.go @@ -0,0 +1,54 @@ +package query + +import ( + "context" + "errors" + "testing" + + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/organization" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetOrganizationByIDSuccess is responsible to test GetOrganizationByID with success +func TestGetOrganizationByIDSuccess(t *testing.T) { + id := uuid.New() + organization := &o.Organization{ID: id.String()} + + uc := UseCase{ + OrganizationRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.OrganizationRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), id). + Return(organization, nil). + Times(1) + res, err := uc.OrganizationRepo.Find(context.TODO(), id) + + assert.Equal(t, res, organization) + assert.Nil(t, err) +} + +// TestGetOrganizationByIDError is responsible to test GetOrganizationByID with error +func TestGetOrganizationByIDError(t *testing.T) { + id := uuid.New() + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + OrganizationRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.OrganizationRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), id). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.OrganizationRepo.Find(context.TODO(), id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/query/get-id-portfolio.go b/components/ledger/internal/app/query/get-id-portfolio.go new file mode 100644 index 00000000..3ff9a06a --- /dev/null +++ b/components/ledger/internal/app/query/get-id-portfolio.go @@ -0,0 +1,50 @@ +package query + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + "github.com/google/uuid" +) + +// GetPortfolioByID get a Portfolio from the repository by given id. +func (uc *UseCase) GetPortfolioByID(ctx context.Context, organizationID, ledgerID, id string) (*p.Portfolio, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving portfolio for id: %s", id) + + portfolio, err := uc.PortfolioRepo.Find(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(id)) + if err != nil { + logger.Errorf("Error getting portfolio on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(p.Portfolio{}).Name(), + Message: fmt.Sprintf("Portfolio with id %s was not found", id), + Code: "PORTFOLIO_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if portfolio != nil { + metadata, err := uc.MetadataRepo.FindByEntity(ctx, reflect.TypeOf(p.Portfolio{}).Name(), id) + if err != nil { + logger.Errorf("Error get metadata on mongodb portfolio: %v", err) + return nil, err + } + + if metadata != nil { + portfolio.Metadata = metadata.Data + } + } + + return portfolio, nil +} diff --git a/components/ledger/internal/app/query/get-id-portfolio_test.go b/components/ledger/internal/app/query/get-id-portfolio_test.go new file mode 100644 index 00000000..7fd97a11 --- /dev/null +++ b/components/ledger/internal/app/query/get-id-portfolio_test.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "testing" + + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/portfolio" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetPortfolioByIDSuccess is responsible to test GetPortfolioByID with success +func TestGetPortfolioByIDSuccess(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + portfolio := &p.Portfolio{ + ID: id.String(), + LedgerID: ledgerID.String(), + OrganizationID: organizationID.String(), + } + + uc := UseCase{ + PortfolioRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.PortfolioRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), organizationID, ledgerID, id). + Return(portfolio, nil). + Times(1) + res, err := uc.PortfolioRepo.Find(context.TODO(), organizationID, ledgerID, id) + + assert.Equal(t, res, portfolio) + assert.Nil(t, err) +} + +// TestGetPortfolioByIDError is responsible to test GetPortfolioByID with error +func TestGetPortfolioByIDError(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + PortfolioRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.PortfolioRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), organizationID, ledgerID, id). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.PortfolioRepo.Find(context.TODO(), organizationID, ledgerID, id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/query/get-id-product.go b/components/ledger/internal/app/query/get-id-product.go new file mode 100644 index 00000000..6a2813bd --- /dev/null +++ b/components/ledger/internal/app/query/get-id-product.go @@ -0,0 +1,50 @@ +package query + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/LerianStudio/midaz/components/ledger/internal/app" + r "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + "github.com/google/uuid" +) + +// GetProductByID get a Product from the repository by given id. +func (uc *UseCase) GetProductByID(ctx context.Context, organizationID, ledgerID, id string) (*r.Product, error) { + logger := mlog.NewLoggerFromContext(ctx) + logger.Infof("Retrieving product for id: %s", id) + + product, err := uc.ProductRepo.Find(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(id)) + if err != nil { + logger.Errorf("Error getting product on repo by id: %v", err) + + if errors.Is(err, app.ErrDatabaseItemNotFound) { + return nil, common.EntityNotFoundError{ + EntityType: reflect.TypeOf(r.Product{}).Name(), + Message: fmt.Sprintf("Product with id %s was not found", id), + Code: "PRODUCT_NOT_FOUND", + Err: err, + } + } + + return nil, err + } + + if product != nil { + metadata, err := uc.MetadataRepo.FindByEntity(ctx, reflect.TypeOf(r.Product{}).Name(), id) + if err != nil { + logger.Errorf("Error get metadata on mongodb product: %v", err) + return nil, err + } + + if metadata != nil { + product.Metadata = metadata.Data + } + } + + return product, nil +} diff --git a/components/ledger/internal/app/query/get-id-product_test.go b/components/ledger/internal/app/query/get-id-product_test.go new file mode 100644 index 00000000..1fdc37a9 --- /dev/null +++ b/components/ledger/internal/app/query/get-id-product_test.go @@ -0,0 +1,62 @@ +package query + +import ( + "context" + "errors" + "testing" + + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + mock "github.com/LerianStudio/midaz/components/ledger/internal/gen/mock/product" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// TestGetProductByIDSuccess is responsible to test GetProductByID with success +func TestGetProductByIDSuccess(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + product := &p.Product{ + ID: id.String(), + LedgerID: ledgerID.String(), + OrganizationID: organizationID.String(), + } + + uc := UseCase{ + ProductRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.ProductRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), organizationID, ledgerID, id). + Return(product, nil). + Times(1) + res, err := uc.ProductRepo.Find(context.TODO(), organizationID, ledgerID, id) + + assert.Equal(t, res, product) + assert.Nil(t, err) +} + +// TestGetProductByIDError is responsible to test GetProductByID with error +func TestGetProductByIDError(t *testing.T) { + id := uuid.New() + organizationID := uuid.New() + ledgerID := uuid.New() + errMSG := "errDatabaseItemNotFound" + + uc := UseCase{ + ProductRepo: mock.NewMockRepository(gomock.NewController(t)), + } + + uc.ProductRepo.(*mock.MockRepository). + EXPECT(). + Find(gomock.Any(), organizationID, ledgerID, id). + Return(nil, errors.New(errMSG)). + Times(1) + res, err := uc.ProductRepo.Find(context.TODO(), organizationID, ledgerID, id) + + assert.NotEmpty(t, err) + assert.Equal(t, err.Error(), errMSG) + assert.Nil(t, res) +} diff --git a/components/ledger/internal/app/query/query.go b/components/ledger/internal/app/query/query.go new file mode 100644 index 00000000..61e29ca5 --- /dev/null +++ b/components/ledger/internal/app/query/query.go @@ -0,0 +1,35 @@ +package query + +import ( + m "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + r "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" +) + +// UseCase is a struct that aggregates various repositories for simplified access in use case implementations. +type UseCase struct { + // OrganizationRepo provides an abstraction on top of the organization data source. + OrganizationRepo o.Repository + + // LedgerRepo provides an abstraction on top of the ledger data source. + LedgerRepo l.Repository + + // ProductRepo provides an abstraction on top of the product data source. + ProductRepo r.Repository + + // PortfolioRepo provides an abstraction on top of the portfolio data source. + PortfolioRepo p.Repository + + // AccountRepo provides an abstraction on top of the account data source. + AccountRepo a.Repository + + // InstrumentRepo provides an abstraction on top of the instrument data source. + InstrumentRepo i.Repository + + // MetadataRepo provides an abstraction on top of the metadata data source. + MetadataRepo m.Repository +} diff --git a/components/ledger/internal/domain/metadata/metadata.go b/components/ledger/internal/domain/metadata/metadata.go new file mode 100644 index 00000000..b92faa34 --- /dev/null +++ b/components/ledger/internal/domain/metadata/metadata.go @@ -0,0 +1,72 @@ +package metadata + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// MetadataMongoDBModel represents the metadata into mongodb context +type MetadataMongoDBModel struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + EntityID string `bson:"entity_id"` + EntityName string `bson:"entity_name"` + Data JSON `bson:"metadata"` + CreatedAt time.Time `bson:"created_at"` + UpdatedAt time.Time `bson:"updated_at"` +} + +// Metadata is a struct designed to encapsulate payload data. +type Metadata struct { + ID primitive.ObjectID + EntityID string + EntityName string + Data JSON + CreatedAt time.Time + UpdatedAt time.Time +} + +// JSON document to save on mongodb +type JSON map[string]any + +// Value return marshall value data +func (mj JSON) Value() (driver.Value, error) { + return json.Marshal(mj) +} + +// Scan unmarshall value data +func (mj *JSON) Scan(value any) error { + b, ok := value.([]byte) + if !ok { + return errors.New("type assertion to []byte failed") + } + + return json.Unmarshal(b, &mj) +} + +// ToEntity converts an MetadataMongoDBModel to entity.Metadata +func (mmm *MetadataMongoDBModel) ToEntity() *Metadata { + return &Metadata{ + ID: mmm.ID, + EntityID: mmm.EntityID, + EntityName: mmm.EntityName, + Data: mmm.Data, + CreatedAt: mmm.CreatedAt, + UpdatedAt: mmm.UpdatedAt, + } +} + +// FromEntity converts an entity.Metadata to MetadataMongoDBModel +func (mmm *MetadataMongoDBModel) FromEntity(md *Metadata) error { + mmm.ID = md.ID + mmm.EntityID = md.EntityID + mmm.EntityName = md.EntityName + mmm.Data = md.Data + mmm.CreatedAt = md.CreatedAt + mmm.UpdatedAt = md.UpdatedAt + + return nil +} diff --git a/components/ledger/internal/domain/metadata/metadata_repository.go b/components/ledger/internal/domain/metadata/metadata_repository.go new file mode 100644 index 00000000..f4e5e476 --- /dev/null +++ b/components/ledger/internal/domain/metadata/metadata_repository.go @@ -0,0 +1,16 @@ +package metadata + +import ( + "context" +) + +// Repository provides an interface for operations related on mongodb a metadata entities. +// +//go:generate mockgen --destination=../../gen/mock/metadata/metadata_mock.go --package=mock . Repository +type Repository interface { + Create(ctx context.Context, collection string, metadata *Metadata) error + FindList(ctx context.Context, collection string, filter any) ([]*Metadata, error) + FindByEntity(ctx context.Context, collection, id string) (*Metadata, error) + Update(ctx context.Context, collection, id string, metadata map[string]any) error + Delete(ctx context.Context, collection, id string) error +} diff --git a/components/ledger/internal/domain/onboarding/ledger/ledger.go b/components/ledger/internal/domain/onboarding/ledger/ledger.go new file mode 100644 index 00000000..6260e184 --- /dev/null +++ b/components/ledger/internal/domain/onboarding/ledger/ledger.go @@ -0,0 +1,101 @@ +package ledger + +import ( + "database/sql" + "time" + + "github.com/google/uuid" +) + +// LedgerPostgreSQLModel represents the entity.Ledger into SQL context in Database +type LedgerPostgreSQLModel struct { + ID string + Name string + OrganizationID string + Status string + StatusDescription *string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime + Metadata map[string]any +} + +// CreateLedgerInput is a struct design to encapsulate request create payload data. +type CreateLedgerInput struct { + Name string `json:"name"` + Status Status `json:"status,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// UpdateLedgerInput is a struct design to encapsulate request update payload data. +type UpdateLedgerInput struct { + Name string `json:"name"` + Status Status `json:"status"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Ledger is a struct designed to encapsulate payload data. +type Ledger struct { + ID string `json:"id"` + Name string `json:"name"` + OrganizationID string `json:"organizationId"` + Status Status `json:"status"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt" sql:"index"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Status structure for marshaling/unmarshalling JSON. +type Status struct { + Code string `json:"code"` + Description *string `json:"description"` +} + +// IsEmpty method that set empty or nil in fields +func (s Status) IsEmpty() bool { + return s.Code == "" && s.Description == nil +} + +// ToEntity converts an LedgerPostgreSQLModel to entity.Ledger +func (t *LedgerPostgreSQLModel) ToEntity() *Ledger { + status := Status{ + Code: t.Status, + Description: t.StatusDescription, + } + + ledger := &Ledger{ + ID: t.ID, + Name: t.Name, + OrganizationID: t.OrganizationID, + Status: status, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + DeletedAt: nil, + } + + if !t.DeletedAt.Time.IsZero() { + deletedAtCopy := t.DeletedAt.Time + ledger.DeletedAt = &deletedAtCopy + } + + return ledger +} + +// FromEntity converts an entity.Ledger to LedgerPostgreSQLModel +func (t *LedgerPostgreSQLModel) FromEntity(ledger *Ledger) { + *t = LedgerPostgreSQLModel{ + ID: uuid.New().String(), + Name: ledger.Name, + OrganizationID: ledger.OrganizationID, + Status: ledger.Status.Code, + StatusDescription: ledger.Status.Description, + CreatedAt: ledger.CreatedAt, + UpdatedAt: ledger.UpdatedAt, + } + + if ledger.DeletedAt != nil { + deletedAtCopy := *ledger.DeletedAt + t.DeletedAt = sql.NullTime{Time: deletedAtCopy, Valid: true} + } +} diff --git a/components/ledger/internal/domain/onboarding/ledger/ledger_repository.go b/components/ledger/internal/domain/onboarding/ledger/ledger_repository.go new file mode 100644 index 00000000..bdb2576c --- /dev/null +++ b/components/ledger/internal/domain/onboarding/ledger/ledger_repository.go @@ -0,0 +1,19 @@ +package ledger + +import ( + "context" + + "github.com/google/uuid" +) + +// Repository provides an interface for operations related to ledger entities. +// +//go:generate mockgen --destination=../../gen/mock/ledger/ledger_mock.go --package=mock . Repository +type Repository interface { + Create(ctx context.Context, ledger *Ledger) (*Ledger, error) + Find(ctx context.Context, organizationID, id uuid.UUID) (*Ledger, error) + FindAll(ctx context.Context, organizationID uuid.UUID) ([]*Ledger, error) + ListByIDs(ctx context.Context, organizationID uuid.UUID, ids []uuid.UUID) ([]*Ledger, error) + Update(ctx context.Context, organizationID, id uuid.UUID, ledger *Ledger) (*Ledger, error) + Delete(ctx context.Context, organizationID, id uuid.UUID) error +} diff --git a/components/ledger/internal/domain/onboarding/organization/organization.go b/components/ledger/internal/domain/onboarding/organization/organization.go new file mode 100644 index 00000000..82c33faa --- /dev/null +++ b/components/ledger/internal/domain/onboarding/organization/organization.go @@ -0,0 +1,135 @@ +package organization + +import ( + "database/sql" + "time" + + "github.com/google/uuid" +) + +// OrganizationPostgreSQLModel represents the entity Organization into SQL context in Database +type OrganizationPostgreSQLModel struct { + ID string + ParentOrganizationID *string + LegalName string + DoingBusinessAs *string + LegalDocument string + Address Address + Status string + StatusDescription *string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime + Metadata map[string]any +} + +// CreateOrganizationInput is a struct design to encapsulate request create payload data. +type CreateOrganizationInput struct { + LegalName string `json:"legalName"` + ParentOrganizationID *string `json:"parentOrganizationId,omitempty"` + DoingBusinessAs *string `json:"doingBusinessAs,omitempty"` + LegalDocument string `json:"legalDocument"` + Address Address `json:"address"` + Status Status `json:"status,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// UpdateOrganizationInput is a struct design to encapsulate request update payload data. +type UpdateOrganizationInput struct { + LegalName string `json:"legalName"` + DoingBusinessAs *string `json:"doingBusinessAs,omitempty"` + Address Address `json:"address"` + Status Status `json:"status"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Organization is a struct designed to encapsulate response payload data. +type Organization struct { + ID string `json:"id,omitempty"` + ParentOrganizationID *string `json:"parentOrganizationId,omitempty"` + LegalName string `json:"legalName"` + DoingBusinessAs *string `json:"doingBusinessAs,omitempty"` + LegalDocument string `json:"legalDocument"` + Address Address `json:"address"` + Status Status `json:"status"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Status structure for marshaling/unmarshalling JSON. +type Status struct { + Code string `json:"code"` + Description *string `json:"description"` +} + +// IsEmpty method that set empty or nil in fields +func (s Status) IsEmpty() bool { + return s.Code == "" && s.Description == nil +} + +// Address structure for marshaling/unmarshalling JSON. +type Address struct { + Line1 string `json:"line1"` + Line2 *string `json:"line2,omitempty"` + Neighborhood string `json:"neighborhood"` + ZipCode string `json:"zipCode"` + City string `json:"city"` + State string `json:"state"` + Country string `json:"country"` // According to ISO 3166-1 alpha-2 +} + +// IsEmpty method that set empty or nil in fields +func (a Address) IsEmpty() bool { + return a.Line1 == "" && a.Line2 == nil && a.Neighborhood == "" && + a.ZipCode == "" && a.City == "" && a.State == "" && a.Country == "" +} + +// ToEntity converts an OrganizationPostgreSQLModel to entity.Organization +func (t *OrganizationPostgreSQLModel) ToEntity() *Organization { + status := Status{ + Code: t.Status, + Description: t.StatusDescription, + } + + organization := &Organization{ + ID: t.ID, + ParentOrganizationID: t.ParentOrganizationID, + LegalName: t.LegalName, + DoingBusinessAs: t.DoingBusinessAs, + LegalDocument: t.LegalDocument, + Address: t.Address, + Status: status, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + } + + if !t.DeletedAt.Time.IsZero() { + deletedAtCopy := t.DeletedAt.Time + organization.DeletedAt = &deletedAtCopy + } + + return organization +} + +// FromEntity converts an entity.Organization to OrganizationPostgresModel +func (t *OrganizationPostgreSQLModel) FromEntity(organization *Organization) { + *t = OrganizationPostgreSQLModel{ + ID: uuid.New().String(), + ParentOrganizationID: organization.ParentOrganizationID, + LegalName: organization.LegalName, + DoingBusinessAs: organization.DoingBusinessAs, + LegalDocument: organization.LegalDocument, + Address: organization.Address, + Status: organization.Status.Code, + StatusDescription: organization.Status.Description, + CreatedAt: organization.CreatedAt, + UpdatedAt: organization.UpdatedAt, + } + + if organization.DeletedAt != nil { + deletedAtCopy := *organization.DeletedAt + t.DeletedAt = sql.NullTime{Time: deletedAtCopy, Valid: true} + } +} diff --git a/components/ledger/internal/domain/onboarding/organization/organization_repository.go b/components/ledger/internal/domain/onboarding/organization/organization_repository.go new file mode 100644 index 00000000..1e05bc14 --- /dev/null +++ b/components/ledger/internal/domain/onboarding/organization/organization_repository.go @@ -0,0 +1,19 @@ +package organization + +import ( + "context" + + "github.com/google/uuid" +) + +// Repository provides an interface for operations related to organization entities. +// +//go:generate mockgen --destination=../../gen/mock/organization/organization_mock.go --package=mock . Repository +type Repository interface { + Create(ctx context.Context, organization *Organization) (*Organization, error) + Update(ctx context.Context, id uuid.UUID, organization *Organization) (*Organization, error) + Find(ctx context.Context, id uuid.UUID) (*Organization, error) + FindAll(ctx context.Context) ([]*Organization, error) + ListByIDs(ctx context.Context, ids []uuid.UUID) ([]*Organization, error) + Delete(ctx context.Context, id uuid.UUID) error +} diff --git a/components/ledger/internal/domain/portfolio/account/account.go b/components/ledger/internal/domain/portfolio/account/account.go new file mode 100644 index 00000000..c21ec8f0 --- /dev/null +++ b/components/ledger/internal/domain/portfolio/account/account.go @@ -0,0 +1,178 @@ +package account + +import ( + "database/sql" + "time" + + "github.com/google/uuid" +) + +// AccountPostgreSQLModel represents the entity Account into SQL context in Database +type AccountPostgreSQLModel struct { + ID string + Name string + ParentAccountID *string + EntityID string + InstrumentCode string + OrganizationID string + LedgerID string + PortfolioID string + ProductID string + AvailableBalance *float64 + OnHoldBalance *float64 + BalanceScale *float64 + Status string + StatusDescription *string + AllowSending bool + AllowReceiving bool + Alias string + Type string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime + Metadata map[string]any +} + +// CreateAccountInput is a struct design to encapsulate request create payload data. +type CreateAccountInput struct { + InstrumentCode string `json:"instrumentCode"` + Name string `json:"name"` + Alias string `json:"alias"` + Type string `json:"type"` + ParentAccountID *string `json:"parentAccountId"` + ProductID string `json:"productId"` + EntityID *string `json:"entityId"` + Balance Balance `json:"balance"` + Status Status `json:"status"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// UpdateAccountInput is a struct design to encapsulate request update payload data. +type UpdateAccountInput struct { + Name string `json:"name"` + Status Status `json:"status"` + Alias string `json:"alias"` + AllowSending bool `json:"allowSending"` + AllowReceiving bool `json:"allowReceiving"` + ProductID string `json:"productId"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Account is a struct designed to encapsulate response payload data. +type Account struct { + ID string `json:"id"` + Name string `json:"name"` + ParentAccountID *string `json:"parentAccountId,omitempty"` + EntityID string `json:"entityId"` + InstrumentCode string `json:"instrumentCode"` + OrganizationID string `json:"organizationId"` + LedgerID string `json:"ledgerId"` + PortfolioID string `json:"portfolioId"` + ProductID string `json:"productId"` + Balance Balance `json:"balance"` + Status Status `json:"status"` + AllowSending bool `json:"allowSending"` + AllowReceiving bool `json:"allowReceiving"` + Alias string `json:"alias"` + Type string `json:"type"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Status structure for marshaling/unmarshalling JSON. +type Status struct { + Code string `json:"code"` + Description *string `json:"description"` +} + +// IsEmpty method that set empty or nil in fields +func (s Status) IsEmpty() bool { + return s.Code == "" && s.Description == nil +} + +// Balance structure for marshaling/unmarshalling JSON. +type Balance struct { + Available *float64 `json:"available"` + OnHold *float64 `json:"onHold"` + Scale *float64 `json:"scale"` +} + +// IsEmpty method that set empty or nil in fields +func (b Balance) IsEmpty() bool { + return b.Available == nil && b.OnHold == nil && b.Scale == nil +} + +// ToEntity converts an AccountPostgreSQLModel to a response entity Account +func (t *AccountPostgreSQLModel) ToEntity() *Account { + status := Status{ + Code: t.Status, + Description: t.StatusDescription, + } + + balance := Balance{ + Available: t.AvailableBalance, + OnHold: t.OnHoldBalance, + Scale: t.BalanceScale, + } + + acc := &Account{ + ID: t.ID, + Name: t.Name, + ParentAccountID: t.ParentAccountID, + EntityID: t.EntityID, + InstrumentCode: t.InstrumentCode, + OrganizationID: t.OrganizationID, + LedgerID: t.LedgerID, + PortfolioID: t.PortfolioID, + ProductID: t.ProductID, + Balance: balance, + Status: status, + AllowSending: t.AllowSending, + AllowReceiving: t.AllowReceiving, + Alias: t.Alias, + Type: t.Type, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + DeletedAt: nil, + } + + if !t.DeletedAt.Time.IsZero() { + deletedAtCopy := t.DeletedAt.Time + acc.DeletedAt = &deletedAtCopy + } + + return acc +} + +// FromEntity converts a request entity Account to AccountPostgreSQLModel +func (t *AccountPostgreSQLModel) FromEntity(account *Account) { + *t = AccountPostgreSQLModel{ + ID: uuid.New().String(), + Name: account.Name, + ParentAccountID: account.ParentAccountID, + EntityID: account.EntityID, + InstrumentCode: account.InstrumentCode, + OrganizationID: account.OrganizationID, + LedgerID: account.LedgerID, + PortfolioID: account.PortfolioID, + ProductID: account.ProductID, + AvailableBalance: account.Balance.Available, + OnHoldBalance: account.Balance.OnHold, + BalanceScale: account.Balance.Scale, + Status: account.Status.Code, + StatusDescription: account.Status.Description, + AllowSending: account.AllowSending, + AllowReceiving: account.AllowReceiving, + Alias: account.Alias, + Type: account.Type, + CreatedAt: account.CreatedAt, + UpdatedAt: account.UpdatedAt, + } + + if account.DeletedAt != nil { + deletedAtCopy := *account.DeletedAt + t.DeletedAt = sql.NullTime{Time: deletedAtCopy, Valid: true} + } +} diff --git a/components/ledger/internal/domain/portfolio/account/account_repository.go b/components/ledger/internal/domain/portfolio/account/account_repository.go new file mode 100644 index 00000000..7037d173 --- /dev/null +++ b/components/ledger/internal/domain/portfolio/account/account_repository.go @@ -0,0 +1,19 @@ +package account + +import ( + "context" + + "github.com/google/uuid" +) + +// Repository provides an interface for operations related to account entities. +// +//go:generate mockgen --destination=../../gen/mock/account/account_mock.go --package=mock . Repository +type Repository interface { + Create(ctx context.Context, account *Account) (*Account, error) + FindAll(ctx context.Context, organizationID, ledgerID, portfolioID uuid.UUID) ([]*Account, error) + Find(ctx context.Context, organizationID, ledgerID, portfolioID, id uuid.UUID) (*Account, error) + ListByIDs(ctx context.Context, organizationID, ledgerID, portfolioID uuid.UUID, ids []uuid.UUID) ([]*Account, error) + Update(ctx context.Context, organizationID, ledgerID, portfolioID, id uuid.UUID, account *Account) (*Account, error) + Delete(ctx context.Context, organizationID, ledgerID, portfolioID, id uuid.UUID) error +} diff --git a/components/ledger/internal/domain/portfolio/instrument/instrument.go b/components/ledger/internal/domain/portfolio/instrument/instrument.go new file mode 100644 index 00000000..c90abf7e --- /dev/null +++ b/components/ledger/internal/domain/portfolio/instrument/instrument.go @@ -0,0 +1,114 @@ +package instrument + +import ( + "database/sql" + "time" + + "github.com/google/uuid" +) + +// InstrumentPostgreSQLModel represents the entity Instrument into SQL context in Database +type InstrumentPostgreSQLModel struct { + ID string + Name string + Type string + Code string + Status string + StatusDescription *string + LedgerID string + OrganizationID string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime + Metadata map[string]any +} + +// CreateInstrumentInput is a struct design to encapsulate request create payload data. +type CreateInstrumentInput struct { + Name string `json:"name"` + Type string `json:"type"` + Code string `json:"code"` + Status Status `json:"status"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// UpdateInstrumentInput is a struct design to encapsulate request update payload data. +type UpdateInstrumentInput struct { + Name string `json:"name"` + Status Status `json:"status"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Instrument is a struct designed to encapsulate payload data. +type Instrument struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Code string `json:"code"` + Status Status `json:"status"` + LedgerID string `json:"ledgerId"` + OrganizationID string `json:"organizationId"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Status structure for marshaling/unmarshalling JSON. +type Status struct { + Code string `json:"code"` + Description *string `json:"description"` +} + +// IsEmpty method that set empty or nil in fields +func (s Status) IsEmpty() bool { + return s.Code == "" && s.Description == nil +} + +// ToEntity converts an InstrumentPostgreSQLModel to entity response Instrument +func (t *InstrumentPostgreSQLModel) ToEntity() *Instrument { + status := Status{ + Code: t.Status, + Description: t.StatusDescription, + } + + instrument := &Instrument{ + ID: t.ID, + Name: t.Name, + Type: t.Type, + Code: t.Code, + Status: status, + LedgerID: t.LedgerID, + OrganizationID: t.OrganizationID, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + } + + if !t.DeletedAt.Time.IsZero() { + deletedAtCopy := t.DeletedAt.Time + instrument.DeletedAt = &deletedAtCopy + } + + return instrument +} + +// FromEntity converts a request entity Instrument to InstrumentPostgreSQLModel +func (t *InstrumentPostgreSQLModel) FromEntity(instrument *Instrument) { + *t = InstrumentPostgreSQLModel{ + ID: uuid.New().String(), + Name: instrument.Name, + Type: instrument.Type, + Code: instrument.Code, + Status: instrument.Status.Code, + StatusDescription: instrument.Status.Description, + LedgerID: instrument.LedgerID, + OrganizationID: instrument.OrganizationID, + CreatedAt: instrument.CreatedAt, + UpdatedAt: instrument.UpdatedAt, + } + + if instrument.DeletedAt != nil { + deletedAtCopy := *instrument.DeletedAt + t.DeletedAt = sql.NullTime{Time: deletedAtCopy, Valid: true} + } +} diff --git a/components/ledger/internal/domain/portfolio/instrument/instrument_repository.go b/components/ledger/internal/domain/portfolio/instrument/instrument_repository.go new file mode 100644 index 00000000..8a47a637 --- /dev/null +++ b/components/ledger/internal/domain/portfolio/instrument/instrument_repository.go @@ -0,0 +1,19 @@ +package instrument + +import ( + "context" + + "github.com/google/uuid" +) + +// Repository provides an interface for operations related to instrument entities. +// +//go:generate mockgen --destination=../../gen/mock/instrument/instrument_mock.go --package=mock . Repository +type Repository interface { + Create(ctx context.Context, instrument *Instrument) (*Instrument, error) + FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID) ([]*Instrument, error) + ListByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*Instrument, error) + Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*Instrument, error) + Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, instrument *Instrument) (*Instrument, error) + Delete(ctx context.Context, organizationID, ledgerID, id uuid.UUID) error +} diff --git a/components/ledger/internal/domain/portfolio/portfolio/portfolio.go b/components/ledger/internal/domain/portfolio/portfolio/portfolio.go new file mode 100644 index 00000000..f911b96d --- /dev/null +++ b/components/ledger/internal/domain/portfolio/portfolio/portfolio.go @@ -0,0 +1,110 @@ +package portfolio + +import ( + "database/sql" + "time" + + "github.com/google/uuid" +) + +// PortfolioPostgreSQLModel represents the entity Portfolio into SQL context in Database +type PortfolioPostgreSQLModel struct { + ID string + Name string + EntityID string + LedgerID string + OrganizationID string + Status string + StatusDescription *string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime + Metadata map[string]any +} + +// CreatePortfolioInput is a struct design to encapsulate request create payload data. +type CreatePortfolioInput struct { + EntityID string `json:"entityId"` + Name string `json:"name"` + Status Status `json:"status"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// UpdatePortfolioInput is a struct design to encapsulate payload data. +type UpdatePortfolioInput struct { + Name string `json:"name"` + Status Status `json:"status"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Portfolio is a struct designed to encapsulate request update payload data. +type Portfolio struct { + ID string `json:"id"` + Name string `json:"name"` + EntityID string `json:"entityId"` + LedgerID string `json:"ledgerId"` + OrganizationID string `json:"organizationId"` + Status Status `json:"status"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Status structure for marshaling/unmarshalling JSON. +type Status struct { + Code string `json:"code"` + Description *string `json:"description"` +} + +// IsEmpty method that set empty or nil in fields +func (s Status) IsEmpty() bool { + return s.Code == "" && s.Description == nil +} + +// ToEntity converts an PortfolioPostgreSQLModel to entity.Portfolio +func (t *PortfolioPostgreSQLModel) ToEntity() *Portfolio { + status := Status{ + Code: t.Status, + Description: t.StatusDescription, + } + + portfolio := &Portfolio{ + ID: t.ID, + Name: t.Name, + EntityID: t.EntityID, + LedgerID: t.LedgerID, + OrganizationID: t.OrganizationID, + Status: status, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + DeletedAt: nil, + } + + if !t.DeletedAt.Time.IsZero() { + deletedAtCopy := t.DeletedAt.Time + portfolio.DeletedAt = &deletedAtCopy + } + + return portfolio +} + +// FromEntity converts an entity.Portfolio to PortfolioPostgreSQLModel +func (t *PortfolioPostgreSQLModel) FromEntity(portfolio *Portfolio) { + *t = PortfolioPostgreSQLModel{ + ID: uuid.New().String(), + Name: portfolio.Name, + EntityID: portfolio.EntityID, + LedgerID: portfolio.LedgerID, + OrganizationID: portfolio.OrganizationID, + Status: portfolio.Status.Code, + StatusDescription: portfolio.Status.Description, + CreatedAt: portfolio.CreatedAt, + UpdatedAt: portfolio.UpdatedAt, + } + + if portfolio.DeletedAt != nil { + deletedAtCopy := *portfolio.DeletedAt + t.DeletedAt = sql.NullTime{Time: deletedAtCopy, Valid: true} + } +} diff --git a/components/ledger/internal/domain/portfolio/portfolio/portfolio_repository.go b/components/ledger/internal/domain/portfolio/portfolio/portfolio_repository.go new file mode 100644 index 00000000..413dde60 --- /dev/null +++ b/components/ledger/internal/domain/portfolio/portfolio/portfolio_repository.go @@ -0,0 +1,20 @@ +package portfolio + +import ( + "context" + + "github.com/google/uuid" +) + +// Repository provides an interface for operations related to portfolio entities. +// +//go:generate mockgen --destination=../../gen/mock/portfolio/portfolio_mock.go --package=mock . Repository +type Repository interface { + Create(ctx context.Context, portfolio *Portfolio) (*Portfolio, error) + FindByIDEntity(ctx context.Context, organizationID, ledgerID, entityID uuid.UUID) (*Portfolio, error) + FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID) ([]*Portfolio, error) + Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*Portfolio, error) + ListByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*Portfolio, error) + Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, portfolio *Portfolio) (*Portfolio, error) + Delete(ctx context.Context, organizationID, ledgerID, id uuid.UUID) error +} diff --git a/components/ledger/internal/domain/portfolio/product/product.go b/components/ledger/internal/domain/portfolio/product/product.go new file mode 100644 index 00000000..6f86a7bc --- /dev/null +++ b/components/ledger/internal/domain/portfolio/product/product.go @@ -0,0 +1,105 @@ +package product + +import ( + "database/sql" + "time" + + "github.com/google/uuid" +) + +// ProductPostgreSQLModel represents the entity Product into SQL context in Database +type ProductPostgreSQLModel struct { + ID string + Name string + LedgerID string + OrganizationID string + Status string + StatusDescription *string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime + Metadata map[string]any +} + +// CreateProductInput is a struct design to encapsulate request create payload data. +type CreateProductInput struct { + Name string `json:"name"` + Status Status `json:"status"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// UpdateProductInput is a struct design to encapsulate request update payload data. +type UpdateProductInput struct { + Name string `json:"name"` + Status Status `json:"status"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Product is a struct designed to encapsulate payload data. +type Product struct { + ID string `json:"id"` + Name string `json:"name"` + LedgerID string `json:"ledgerId"` + OrganizationID string `json:"organizationId"` + Status Status `json:"status"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Status structure for marshaling/unmarshalling JSON. +type Status struct { + Code string `json:"code"` + Description *string `json:"description"` +} + +// IsEmpty method that set empty or nil in fields +func (s Status) IsEmpty() bool { + return s.Code == "" && s.Description == nil +} + +// ToEntity converts an ProductPostgreSQLModel to entity.Product +func (t *ProductPostgreSQLModel) ToEntity() *Product { + status := Status{ + Code: t.Status, + Description: t.StatusDescription, + } + + product := &Product{ + ID: t.ID, + Name: t.Name, + LedgerID: t.LedgerID, + OrganizationID: t.OrganizationID, + Status: status, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + DeletedAt: nil, + } + + if !t.DeletedAt.Time.IsZero() { + deletedAtCopy := t.DeletedAt.Time + product.DeletedAt = &deletedAtCopy + } + + return product +} + +// FromEntity converts an entity.Product to ProductPostgreSQLModel +func (t *ProductPostgreSQLModel) FromEntity(product *Product) { + *t = ProductPostgreSQLModel{ + ID: uuid.New().String(), + Name: product.Name, + LedgerID: product.LedgerID, + OrganizationID: product.OrganizationID, + Status: product.Status.Code, + StatusDescription: product.Status.Description, + CreatedAt: product.CreatedAt, + UpdatedAt: product.UpdatedAt, + } + + if product.DeletedAt != nil { + deletedAtCopy := *product.DeletedAt + t.DeletedAt = sql.NullTime{Time: deletedAtCopy, Valid: true} + } +} diff --git a/components/ledger/internal/domain/portfolio/product/product_repository.go b/components/ledger/internal/domain/portfolio/product/product_repository.go new file mode 100644 index 00000000..70477742 --- /dev/null +++ b/components/ledger/internal/domain/portfolio/product/product_repository.go @@ -0,0 +1,20 @@ +package product + +import ( + "context" + + "github.com/google/uuid" +) + +// Repository provides an interface for operations related to product entities. +// +//go:generate mockgen --destination=../../gen/mock/product/product_mock.go --package=mock . Repository +type Repository interface { + Create(ctx context.Context, product *Product) (*Product, error) + FindByName(ctx context.Context, organizationID, ledgerID uuid.UUID, name string) (bool, error) + FindAll(ctx context.Context, organizationID, ledgerID uuid.UUID) ([]*Product, error) + FindByIDs(ctx context.Context, organizationID, ledgerID uuid.UUID, ids []uuid.UUID) ([]*Product, error) + Find(ctx context.Context, organizationID, ledgerID, id uuid.UUID) (*Product, error) + Update(ctx context.Context, organizationID, ledgerID, id uuid.UUID, product *Product) (*Product, error) + Delete(ctx context.Context, organizationID, ledgerID, id uuid.UUID) error +} diff --git a/components/ledger/internal/gen/inject.go b/components/ledger/internal/gen/inject.go new file mode 100644 index 00000000..146b7bbf --- /dev/null +++ b/components/ledger/internal/gen/inject.go @@ -0,0 +1,104 @@ +//go:build wireinject +// +build wireinject + +package gen + +import ( + "fmt" + "github.com/LerianStudio/midaz/components/ledger/internal/adapters/database/mongodb" + "github.com/LerianStudio/midaz/components/ledger/internal/adapters/database/postgres" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + "github.com/LerianStudio/midaz/components/ledger/internal/ports" + "sync" + + "github.com/LerianStudio/midaz/common/mmongo" + + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mpostgres" + "github.com/LerianStudio/midaz/common/mzap" + "github.com/LerianStudio/midaz/components/ledger/internal/app/command" + "github.com/LerianStudio/midaz/components/ledger/internal/app/query" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + httpHandler "github.com/LerianStudio/midaz/components/ledger/internal/ports/http" + "github.com/LerianStudio/midaz/components/ledger/internal/service" + "github.com/google/wire" +) + +var onceConfig sync.Once + +const prdEnvName = "production" + +func setupPostgreSQLConnection(cfg *service.Config) *mpostgres.PostgresConnection { + connStrPrimary := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", + cfg.PrimaryDBHost, cfg.PrimaryDBUser, cfg.PrimaryDBPassword, cfg.PrimaryDBName, cfg.PrimaryDBPort) + + connStrReplica := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", + cfg.ReplicaDBHost, cfg.ReplicaDBUser, cfg.ReplicaDBPassword, cfg.ReplicaDBName, cfg.ReplicaDBPort) + + return &mpostgres.PostgresConnection{ + ConnectionStringPrimary: connStrPrimary, + ConnectionStringReplica: connStrReplica, + PrimaryDBName: cfg.PrimaryDBName, + ReplicaDBName: cfg.ReplicaDBName, + } +} + +func setupMongoDBConnection(cfg *service.Config) *mmongo.MongoConnection { + connStrSource := fmt.Sprintf("mongodb://%s:%s@%s:%s", + cfg.MongoDBUser, cfg.MongoDBPassword, cfg.MongoDBHost, cfg.MongoDBPort) + + return &mmongo.MongoConnection{ + ConnectionStringSource: connStrSource, + Database: cfg.MongoDBName, + } +} + +var ( + serviceSet = wire.NewSet( + common.InitLocalEnvConfig, + mzap.InitializeLogger, + setupPostgreSQLConnection, + setupMongoDBConnection, + service.NewConfig, + httpHandler.NewRouter, + service.NewServer, + postgres.NewOrganizationPostgreSQLRepository, + postgres.NewLedgerPostgreSQLRepository, + postgres.NewInstrumentPostgreSQLRepository, + postgres.NewPortfolioPostgreSQLRepository, + postgres.NewProductPostgreSQLRepository, + postgres.NewAccountPostgreSQLRepository, + mongodb.NewMetadataMongoDBRepository, + wire.Struct(new(ports.OrganizationHandler), "*"), + wire.Struct(new(ports.LedgerHandler), "*"), + wire.Struct(new(ports.InstrumentHandler), "*"), + wire.Struct(new(ports.PortfolioHandler), "*"), + wire.Struct(new(ports.ProductHandler), "*"), + wire.Struct(new(ports.AccountHandler), "*"), + wire.Struct(new(command.UseCase), "*"), + wire.Struct(new(query.UseCase), "*"), + wire.Bind(new(organization.Repository), new(*postgres.OrganizationPostgreSQLRepository)), + wire.Bind(new(ledger.Repository), new(*postgres.LedgerPostgreSQLRepository)), + wire.Bind(new(instrument.Repository), new(*postgres.InstrumentPostgreSQLRepository)), + wire.Bind(new(portfolio.Repository), new(*postgres.PortfolioPostgreSQLRepository)), + wire.Bind(new(product.Repository), new(*postgres.ProductPostgreSQLRepository)), + wire.Bind(new(account.Repository), new(*postgres.AccountPostgreSQLRepository)), + wire.Bind(new(metadata.Repository), new(*mongodb.MetadataMongoDBRepository)), + ) + + svcSet = wire.NewSet( + wire.Struct(new(service.Service), "Server", "Logger"), + ) +) + +// InitializeService the setup the dependencies and returns a new *service.Service instance +func InitializeService() *service.Service { + wire.Build(serviceSet, svcSet) + + return nil +} diff --git a/components/ledger/internal/gen/mock/account/account_mock.go b/components/ledger/internal/gen/mock/account/account_mock.go new file mode 100644 index 00000000..2977f39d --- /dev/null +++ b/components/ledger/internal/gen/mock/account/account_mock.go @@ -0,0 +1,131 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/LerianStudio/midaz/components/ledger/internal/domain/account (interfaces: Repository) +// +// Generated by this command: +// +// mockgen --destination=../../gen/mock/account/account_mock.go --package=mock . Repository +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + reflect "reflect" + + uuid "github.com/google/uuid" + gomock "go.uber.org/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRepository) Create(arg0 context.Context, arg1 *account.Account) (*account.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(*account.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +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), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockRepository) Delete(arg0 context.Context, arg1, arg2, arg3, arg4 uuid.UUID) error { + m.ctrl.T.Helper() + 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(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), arg0, arg1, arg2, arg3, arg4) +} + +// Find mocks base method. +func (m *MockRepository) Find(arg0 context.Context, arg1, arg2, arg3, arg4 uuid.UUID) (*account.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Find", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(*account.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Find indicates an expected call of Find. +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), arg0, arg1, arg2, arg3, arg4) +} + +// FindAll mocks base method. +func (m *MockRepository) FindAll(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) ([]*account.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAll", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]*account.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAll indicates an expected call of FindAll. +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), arg0, arg1, arg2, arg3) +} + +// ListByIDs mocks base method. +func (m *MockRepository) ListByIDs(arg0 context.Context, arg1, arg2, arg3 uuid.UUID, arg4 []uuid.UUID) ([]*account.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByIDs", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].([]*account.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListByIDs indicates an expected call of ListByIDs. +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), arg0, arg1, arg2, arg3, arg4) +} + +// Update mocks base method. +func (m *MockRepository) Update(arg0 context.Context, arg1, arg2, arg3, arg4 uuid.UUID, arg5 *account.Account) (*account.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3, arg4, arg5) + ret0, _ := ret[0].(*account.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +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), arg0, arg1, arg2, arg3, arg4, arg5) +} diff --git a/components/ledger/internal/gen/mock/instrument/instrument_mock.go b/components/ledger/internal/gen/mock/instrument/instrument_mock.go new file mode 100644 index 00000000..d3284059 --- /dev/null +++ b/components/ledger/internal/gen/mock/instrument/instrument_mock.go @@ -0,0 +1,131 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/LerianStudio/midaz/components/ledger/internal/domain/instrument (interfaces: Repository) +// +// Generated by this command: +// +// mockgen --destination=../../gen/mock/instrument/instrument_mock.go --package=mock . Repository +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + reflect "reflect" + + uuid "github.com/google/uuid" + gomock "go.uber.org/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRepository) Create(arg0 context.Context, arg1 *instrument.Instrument) (*instrument.Instrument, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(*instrument.Instrument) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +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), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockRepository) Delete(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) error { + m.ctrl.T.Helper() + 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(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), arg0, arg1, arg2, arg3) +} + +// Find mocks base method. +func (m *MockRepository) Find(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) (*instrument.Instrument, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Find", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*instrument.Instrument) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Find indicates an expected call of Find. +func (mr *MockRepositoryMockRecorder) Find(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + 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(arg0 context.Context, arg1, arg2 uuid.UUID) ([]*instrument.Instrument, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAll", arg0, arg1, arg2) + ret0, _ := ret[0].([]*instrument.Instrument) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAll indicates an expected call of FindAll. +func (mr *MockRepositoryMockRecorder) FindAll(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAll", reflect.TypeOf((*MockRepository)(nil).FindAll), arg0, arg1, arg2) +} + +// ListByIDs mocks base method. +func (m *MockRepository) ListByIDs(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 []uuid.UUID) ([]*instrument.Instrument, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByIDs", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]*instrument.Instrument) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListByIDs indicates an expected call of ListByIDs. +func (mr *MockRepositoryMockRecorder) ListByIDs(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + 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(arg0 context.Context, arg1, arg2, arg3 uuid.UUID, arg4 *instrument.Instrument) (*instrument.Instrument, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(*instrument.Instrument) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockRepositoryMockRecorder) Update(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), arg0, arg1, arg2, arg3, arg4) +} diff --git a/components/ledger/internal/gen/mock/ledger/ledger_mock.go b/components/ledger/internal/gen/mock/ledger/ledger_mock.go new file mode 100644 index 00000000..ef86bbb1 --- /dev/null +++ b/components/ledger/internal/gen/mock/ledger/ledger_mock.go @@ -0,0 +1,131 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/LerianStudio/midaz/components/ledger/internal/domain/ledger (interfaces: Repository) +// +// Generated by this command: +// +// mockgen --destination=../../gen/mock/ledger/ledger_mock.go --package=mock . Repository +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + reflect "reflect" + + uuid "github.com/google/uuid" + gomock "go.uber.org/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRepository) Create(arg0 context.Context, arg1 *ledger.Ledger) (*ledger.Ledger, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(*ledger.Ledger) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +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), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockRepository) Delete(arg0 context.Context, arg1, arg2 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRepositoryMockRecorder) Delete(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), arg0, arg1, arg2) +} + +// Find mocks base method. +func (m *MockRepository) Find(arg0 context.Context, arg1, arg2 uuid.UUID) (*ledger.Ledger, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Find", arg0, arg1, arg2) + ret0, _ := ret[0].(*ledger.Ledger) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Find indicates an expected call of Find. +func (mr *MockRepositoryMockRecorder) Find(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockRepository)(nil).Find), arg0, arg1, arg2) +} + +// FindAll mocks base method. +func (m *MockRepository) FindAll(arg0 context.Context, arg1 uuid.UUID) ([]*ledger.Ledger, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAll", arg0, arg1) + ret0, _ := ret[0].([]*ledger.Ledger) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAll indicates an expected call of FindAll. +func (mr *MockRepositoryMockRecorder) FindAll(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAll", reflect.TypeOf((*MockRepository)(nil).FindAll), arg0, arg1) +} + +// ListByIDs mocks base method. +func (m *MockRepository) ListByIDs(arg0 context.Context, arg1 uuid.UUID, arg2 []uuid.UUID) ([]*ledger.Ledger, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByIDs", arg0, arg1, arg2) + ret0, _ := ret[0].([]*ledger.Ledger) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListByIDs indicates an expected call of ListByIDs. +func (mr *MockRepositoryMockRecorder) ListByIDs(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByIDs", reflect.TypeOf((*MockRepository)(nil).ListByIDs), arg0, arg1, arg2) +} + +// Update mocks base method. +func (m *MockRepository) Update(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 *ledger.Ledger) (*ledger.Ledger, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*ledger.Ledger) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockRepositoryMockRecorder) Update(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), arg0, arg1, arg2, arg3) +} diff --git a/components/ledger/internal/gen/mock/metadata/metadata_mock.go b/components/ledger/internal/gen/mock/metadata/metadata_mock.go new file mode 100644 index 00000000..a46cdfa6 --- /dev/null +++ b/components/ledger/internal/gen/mock/metadata/metadata_mock.go @@ -0,0 +1,113 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata (interfaces: Repository) +// +// Generated by this command: +// +// mockgen --destination=../../gen/mock/metadata/metadata_mock.go --package=mock . Repository +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + metadata "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + gomock "go.uber.org/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRepository) Create(arg0 context.Context, arg1 string, arg2 *metadata.Metadata) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockRepositoryMockRecorder) Create(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), arg0, arg1, arg2) +} + +// Delete mocks base method. +func (m *MockRepository) Delete(arg0 context.Context, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRepositoryMockRecorder) Delete(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), arg0, arg1, arg2) +} + +// FindByEntity mocks base method. +func (m *MockRepository) FindByEntity(arg0 context.Context, arg1, arg2 string) (*metadata.Metadata, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByEntity", arg0, arg1, arg2) + ret0, _ := ret[0].(*metadata.Metadata) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByEntity indicates an expected call of FindByEntity. +func (mr *MockRepositoryMockRecorder) FindByEntity(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByEntity", reflect.TypeOf((*MockRepository)(nil).FindByEntity), arg0, arg1, arg2) +} + +// FindList mocks base method. +func (m *MockRepository) FindList(arg0 context.Context, arg1 string, arg2 any) ([]*metadata.Metadata, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindList", arg0, arg1, arg2) + ret0, _ := ret[0].([]*metadata.Metadata) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindList indicates an expected call of FindList. +func (mr *MockRepositoryMockRecorder) FindList(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindList", reflect.TypeOf((*MockRepository)(nil).FindList), arg0, arg1, arg2) +} + +// Update mocks base method. +func (m *MockRepository) Update(arg0 context.Context, arg1, arg2 string, arg3 map[string]any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockRepositoryMockRecorder) Update(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), arg0, arg1, arg2, arg3) +} diff --git a/components/ledger/internal/gen/mock/organization/organization_mock.go b/components/ledger/internal/gen/mock/organization/organization_mock.go new file mode 100644 index 00000000..100f878a --- /dev/null +++ b/components/ledger/internal/gen/mock/organization/organization_mock.go @@ -0,0 +1,131 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/LerianStudio/midaz/components/ledger/internal/domain/organization (interfaces: Repository) +// +// Generated by this command: +// +// mockgen --destination=../../gen/mock/organization/organization_mock.go --package=mock . Repository +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + reflect "reflect" + + uuid "github.com/google/uuid" + gomock "go.uber.org/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRepository) Create(arg0 context.Context, arg1 *organization.Organization) (*organization.Organization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(*organization.Organization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +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), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockRepository) Delete(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRepositoryMockRecorder) Delete(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), arg0, arg1) +} + +// Find mocks base method. +func (m *MockRepository) Find(arg0 context.Context, arg1 uuid.UUID) (*organization.Organization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Find", arg0, arg1) + ret0, _ := ret[0].(*organization.Organization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Find indicates an expected call of Find. +func (mr *MockRepositoryMockRecorder) Find(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockRepository)(nil).Find), arg0, arg1) +} + +// FindAll mocks base method. +func (m *MockRepository) FindAll(arg0 context.Context) ([]*organization.Organization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAll", arg0) + ret0, _ := ret[0].([]*organization.Organization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAll indicates an expected call of FindAll. +func (mr *MockRepositoryMockRecorder) FindAll(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAll", reflect.TypeOf((*MockRepository)(nil).FindAll), arg0) +} + +// ListByIDs mocks base method. +func (m *MockRepository) ListByIDs(arg0 context.Context, arg1 []uuid.UUID) ([]*organization.Organization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByIDs", arg0, arg1) + ret0, _ := ret[0].([]*organization.Organization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListByIDs indicates an expected call of ListByIDs. +func (mr *MockRepositoryMockRecorder) ListByIDs(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByIDs", reflect.TypeOf((*MockRepository)(nil).ListByIDs), arg0, arg1) +} + +// Update mocks base method. +func (m *MockRepository) Update(arg0 context.Context, arg1 uuid.UUID, arg2 *organization.Organization) (*organization.Organization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2) + ret0, _ := ret[0].(*organization.Organization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockRepositoryMockRecorder) Update(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), arg0, arg1, arg2) +} diff --git a/components/ledger/internal/gen/mock/portfolio/portfolio_mock.go b/components/ledger/internal/gen/mock/portfolio/portfolio_mock.go new file mode 100644 index 00000000..66835233 --- /dev/null +++ b/components/ledger/internal/gen/mock/portfolio/portfolio_mock.go @@ -0,0 +1,146 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio (interfaces: Repository) +// +// Generated by this command: +// +// mockgen --destination=../../gen/mock/portfolio/portfolio_mock.go --package=mock . Repository +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + reflect "reflect" + + uuid "github.com/google/uuid" + gomock "go.uber.org/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRepository) Create(arg0 context.Context, arg1 *portfolio.Portfolio) (*portfolio.Portfolio, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(*portfolio.Portfolio) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +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), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockRepository) Delete(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) error { + m.ctrl.T.Helper() + 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(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), arg0, arg1, arg2, arg3) +} + +// Find mocks base method. +func (m *MockRepository) Find(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) (*portfolio.Portfolio, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Find", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*portfolio.Portfolio) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Find indicates an expected call of Find. +func (mr *MockRepositoryMockRecorder) Find(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + 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(arg0 context.Context, arg1, arg2 uuid.UUID) ([]*portfolio.Portfolio, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAll", arg0, arg1, arg2) + ret0, _ := ret[0].([]*portfolio.Portfolio) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAll indicates an expected call of FindAll. +func (mr *MockRepositoryMockRecorder) FindAll(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAll", reflect.TypeOf((*MockRepository)(nil).FindAll), arg0, arg1, arg2) +} + +// FindByIDEntity mocks base method. +func (m *MockRepository) FindByIDEntity(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) (*portfolio.Portfolio, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByIDEntity", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*portfolio.Portfolio) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByIDEntity indicates an expected call of FindByIDEntity. +func (mr *MockRepositoryMockRecorder) FindByIDEntity(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByIDEntity", reflect.TypeOf((*MockRepository)(nil).FindByIDEntity), arg0, arg1, arg2, arg3) +} + +// ListByIDs mocks base method. +func (m *MockRepository) ListByIDs(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 []uuid.UUID) ([]*portfolio.Portfolio, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByIDs", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]*portfolio.Portfolio) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListByIDs indicates an expected call of ListByIDs. +func (mr *MockRepositoryMockRecorder) ListByIDs(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + 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(arg0 context.Context, arg1, arg2, arg3 uuid.UUID, arg4 *portfolio.Portfolio) (*portfolio.Portfolio, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(*portfolio.Portfolio) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockRepositoryMockRecorder) Update(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), arg0, arg1, arg2, arg3, arg4) +} diff --git a/components/ledger/internal/gen/mock/product/product_mock.go b/components/ledger/internal/gen/mock/product/product_mock.go new file mode 100644 index 00000000..7aecaa28 --- /dev/null +++ b/components/ledger/internal/gen/mock/product/product_mock.go @@ -0,0 +1,146 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/LerianStudio/midaz/components/ledger/internal/domain/product (interfaces: Repository) +// +// Generated by this command: +// +// mockgen --destination=../../gen/mock/product/product_mock.go --package=mock . Repository +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + reflect "reflect" + + uuid "github.com/google/uuid" + gomock "go.uber.org/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRepository) Create(arg0 context.Context, arg1 *product.Product) (*product.Product, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(*product.Product) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +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), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockRepository) Delete(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) error { + m.ctrl.T.Helper() + 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(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), arg0, arg1, arg2, arg3) +} + +// Find mocks base method. +func (m *MockRepository) Find(arg0 context.Context, arg1, arg2, arg3 uuid.UUID) (*product.Product, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Find", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*product.Product) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Find indicates an expected call of Find. +func (mr *MockRepositoryMockRecorder) Find(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + 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(arg0 context.Context, arg1, arg2 uuid.UUID) ([]*product.Product, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAll", arg0, arg1, arg2) + ret0, _ := ret[0].([]*product.Product) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAll indicates an expected call of FindAll. +func (mr *MockRepositoryMockRecorder) FindAll(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAll", reflect.TypeOf((*MockRepository)(nil).FindAll), arg0, arg1, arg2) +} + +// FindByIDs mocks base method. +func (m *MockRepository) FindByIDs(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 []uuid.UUID) ([]*product.Product, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByIDs", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]*product.Product) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByIDs indicates an expected call of FindByIDs. +func (mr *MockRepositoryMockRecorder) FindByIDs(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByIDs", reflect.TypeOf((*MockRepository)(nil).FindByIDs), arg0, arg1, arg2, arg3) +} + +// FindByName mocks base method. +func (m *MockRepository) FindByName(arg0 context.Context, arg1, arg2 uuid.UUID, arg3 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByName", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByName indicates an expected call of FindByName. +func (mr *MockRepositoryMockRecorder) FindByName(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByName", reflect.TypeOf((*MockRepository)(nil).FindByName), arg0, arg1, arg2, arg3) +} + +// Update mocks base method. +func (m *MockRepository) Update(arg0 context.Context, arg1, arg2, arg3 uuid.UUID, arg4 *product.Product) (*product.Product, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(*product.Product) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockRepositoryMockRecorder) Update(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), arg0, arg1, arg2, arg3, arg4) +} diff --git a/components/ledger/internal/gen/wire_gen.go b/components/ledger/internal/gen/wire_gen.go new file mode 100644 index 00000000..458e2de3 --- /dev/null +++ b/components/ledger/internal/gen/wire_gen.go @@ -0,0 +1,136 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run -mod=mod github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject + +package gen + +import ( + "fmt" + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mmongo" + "github.com/LerianStudio/midaz/common/mpostgres" + "github.com/LerianStudio/midaz/common/mzap" + "github.com/LerianStudio/midaz/components/ledger/internal/adapters/database/mongodb" + "github.com/LerianStudio/midaz/components/ledger/internal/adapters/database/postgres" + "github.com/LerianStudio/midaz/components/ledger/internal/app/command" + "github.com/LerianStudio/midaz/components/ledger/internal/app/query" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/metadata" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + "github.com/LerianStudio/midaz/components/ledger/internal/ports" + "github.com/LerianStudio/midaz/components/ledger/internal/ports/http" + "github.com/LerianStudio/midaz/components/ledger/internal/service" + "github.com/google/wire" + "sync" +) + +// Injectors from inject.go: + +// InitializeService the setup the dependencies and returns a new *service.Service instance +func InitializeService() *service.Service { + config := service.NewConfig() + postgresConnection := setupPostgreSQLConnection(config) + organizationPostgreSQLRepository := postgres.NewOrganizationPostgreSQLRepository(postgresConnection) + ledgerPostgreSQLRepository := postgres.NewLedgerPostgreSQLRepository(postgresConnection) + productPostgreSQLRepository := postgres.NewProductPostgreSQLRepository(postgresConnection) + portfolioPostgreSQLRepository := postgres.NewPortfolioPostgreSQLRepository(postgresConnection) + accountPostgreSQLRepository := postgres.NewAccountPostgreSQLRepository(postgresConnection) + instrumentPostgreSQLRepository := postgres.NewInstrumentPostgreSQLRepository(postgresConnection) + mongoConnection := setupMongoDBConnection(config) + metadataMongoDBRepository := mongodb.NewMetadataMongoDBRepository(mongoConnection) + useCase := &command.UseCase{ + OrganizationRepo: organizationPostgreSQLRepository, + LedgerRepo: ledgerPostgreSQLRepository, + ProductRepo: productPostgreSQLRepository, + PortfolioRepo: portfolioPostgreSQLRepository, + AccountRepo: accountPostgreSQLRepository, + InstrumentRepo: instrumentPostgreSQLRepository, + MetadataRepo: metadataMongoDBRepository, + } + queryUseCase := &query.UseCase{ + OrganizationRepo: organizationPostgreSQLRepository, + LedgerRepo: ledgerPostgreSQLRepository, + ProductRepo: productPostgreSQLRepository, + PortfolioRepo: portfolioPostgreSQLRepository, + AccountRepo: accountPostgreSQLRepository, + InstrumentRepo: instrumentPostgreSQLRepository, + MetadataRepo: metadataMongoDBRepository, + } + accountHandler := &ports.AccountHandler{ + Command: useCase, + Query: queryUseCase, + } + portfolioHandler := &ports.PortfolioHandler{ + Command: useCase, + Query: queryUseCase, + } + ledgerHandler := &ports.LedgerHandler{ + Command: useCase, + Query: queryUseCase, + } + instrumentHandler := &ports.InstrumentHandler{ + Command: useCase, + Query: queryUseCase, + } + organizationHandler := &ports.OrganizationHandler{ + Command: useCase, + Query: queryUseCase, + } + productHandler := &ports.ProductHandler{ + Command: useCase, + Query: queryUseCase, + } + app := http.NewRouter(accountHandler, portfolioHandler, ledgerHandler, instrumentHandler, organizationHandler, productHandler) + logger := mzap.InitializeLogger() + server := service.NewServer(config, app, logger) + serviceService := &service.Service{ + Server: server, + Logger: logger, + } + return serviceService +} + +// inject.go: + +var onceConfig sync.Once + +const prdEnvName = "production" + +func setupPostgreSQLConnection(cfg *service.Config) *mpostgres.PostgresConnection { + connStrPrimary := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", + cfg.PrimaryDBHost, cfg.PrimaryDBUser, cfg.PrimaryDBPassword, cfg.PrimaryDBName, cfg.PrimaryDBPort) + + connStrReplica := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", + cfg.ReplicaDBHost, cfg.ReplicaDBUser, cfg.ReplicaDBPassword, cfg.ReplicaDBName, cfg.ReplicaDBPort) + + return &mpostgres.PostgresConnection{ + ConnectionStringPrimary: connStrPrimary, + ConnectionStringReplica: connStrReplica, + PrimaryDBName: cfg.PrimaryDBName, + ReplicaDBName: cfg.ReplicaDBName, + } +} + +func setupMongoDBConnection(cfg *service.Config) *mmongo.MongoConnection { + connStrSource := fmt.Sprintf("mongodb://%s:%s@%s:%s", + cfg.MongoDBUser, cfg.MongoDBPassword, cfg.MongoDBHost, cfg.MongoDBPort) + + return &mmongo.MongoConnection{ + ConnectionStringSource: connStrSource, + Database: cfg.MongoDBName, + } +} + +var ( + serviceSet = wire.NewSet(common.InitLocalEnvConfig, mzap.InitializeLogger, setupPostgreSQLConnection, + setupMongoDBConnection, service.NewConfig, http.NewRouter, service.NewServer, postgres.NewOrganizationPostgreSQLRepository, postgres.NewLedgerPostgreSQLRepository, postgres.NewInstrumentPostgreSQLRepository, postgres.NewPortfolioPostgreSQLRepository, postgres.NewProductPostgreSQLRepository, postgres.NewAccountPostgreSQLRepository, mongodb.NewMetadataMongoDBRepository, wire.Struct(new(ports.OrganizationHandler), "*"), wire.Struct(new(ports.LedgerHandler), "*"), wire.Struct(new(ports.InstrumentHandler), "*"), wire.Struct(new(ports.PortfolioHandler), "*"), wire.Struct(new(ports.ProductHandler), "*"), wire.Struct(new(ports.AccountHandler), "*"), wire.Struct(new(command.UseCase), "*"), wire.Struct(new(query.UseCase), "*"), wire.Bind(new(organization.Repository), new(*postgres.OrganizationPostgreSQLRepository)), wire.Bind(new(ledger.Repository), new(*postgres.LedgerPostgreSQLRepository)), wire.Bind(new(instrument.Repository), new(*postgres.InstrumentPostgreSQLRepository)), wire.Bind(new(portfolio.Repository), new(*postgres.PortfolioPostgreSQLRepository)), wire.Bind(new(product.Repository), new(*postgres.ProductPostgreSQLRepository)), wire.Bind(new(account.Repository), new(*postgres.AccountPostgreSQLRepository)), wire.Bind(new(metadata.Repository), new(*mongodb.MetadataMongoDBRepository)), + ) + + svcSet = wire.NewSet(wire.Struct(new(service.Service), "Server", "Logger")) +) diff --git a/components/ledger/internal/main.go b/components/ledger/internal/main.go new file mode 100644 index 00000000..73837c8c --- /dev/null +++ b/components/ledger/internal/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/components/ledger/internal/gen" +) + +func main() { + common.InitLocalEnvConfig() + gen.InitializeService().Run() +} diff --git a/components/ledger/internal/ports/account.go b/components/ledger/internal/ports/account.go new file mode 100644 index 00000000..15d0e6d9 --- /dev/null +++ b/components/ledger/internal/ports/account.go @@ -0,0 +1,150 @@ +package ports + +import ( + "github.com/LerianStudio/midaz/common/mlog" + commonHTTP "github.com/LerianStudio/midaz/common/net/http" + "github.com/LerianStudio/midaz/components/ledger/internal/app/command" + "github.com/LerianStudio/midaz/components/ledger/internal/app/query" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + "github.com/gofiber/fiber/v2" +) + +// AccountHandler struct contains an account use case for managing account related operations. +type AccountHandler struct { + Command *command.UseCase + Query *query.UseCase +} + +// CreateAccount is a method that creates account information. +func (handler *AccountHandler) CreateAccount(i any, c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + portfolioID := c.Params("portfolio_id") + + logger.Infof("Initiating create of Account with Portfolio ID: %s", portfolioID) + + payload := i.(*a.CreateAccountInput) + logger.Infof("Request to create a Account with details: %#v", payload) + + account, err := handler.Command.CreateAccount(ctx, organizationID, ledgerID, portfolioID, payload) + if err != nil { + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully created Account") + + return commonHTTP.Created(c, account) +} + +// GetAllAccounts is a method that retrieves all Accounts. +func (handler *AccountHandler) GetAllAccounts(c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + portfolioID := c.Params("portfolio_id") + + logger.Infof("Get Accounts with Portfolio ID: %s", portfolioID) + + for key, value := range c.Queries() { + logger.Infof("Initiating retrieval of all Accounts by metadata") + + organizations, err := handler.Query.GetAllMetadataAccounts(ctx, key, value, organizationID, ledgerID, portfolioID) + if err != nil { + logger.Errorf("Failed to retrieve all Accounts, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Accounts by metadata") + + return commonHTTP.OK(c, organizations) + } + + logger.Infof("Initiating retrieval of all Accounts ") + + accounts, err := handler.Query.GetAllAccount(ctx, organizationID, ledgerID, portfolioID) + if err != nil { + logger.Errorf("Failed to retrieve all Accounts, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Accounts") + + return commonHTTP.OK(c, accounts) +} + +// GetAccountByID is a method that retrieves Account information by a given id. +func (handler *AccountHandler) GetAccountByID(c *fiber.Ctx) error { + ctx := c.UserContext() + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + portfolioID := c.Params("portfolio_id") + id := c.Params("id") + + logger := mlog.NewLoggerFromContext(ctx) + + logger.Infof("Initiating retrieval of Account with Portfolio ID: %s and Account ID: %s", portfolioID, id) + + account, err := handler.Query.GetAccountByID(ctx, organizationID, ledgerID, portfolioID, id) + if err != nil { + logger.Errorf("Failed to retrieve Account with Portfolio ID: %s and Account ID: %s, Error: %s", portfolioID, id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved Account with Portfolio ID: %s and Account ID: %s", portfolioID, id) + + return commonHTTP.OK(c, account) +} + +// UpdateAccount is a method that updates Account information. +func (handler *AccountHandler) UpdateAccount(i any, c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + portfolioID := c.Params("portfolio_id") + id := c.Params("id") + + logger.Infof("Initiating update of Account with Portfolio ID: %s and Account ID: %s", portfolioID, id) + + payload := i.(*a.UpdateAccountInput) + logger.Infof("Request to update an Account with details: %#v", payload) + + account, err := handler.Command.UpdateAccountByID(ctx, organizationID, ledgerID, portfolioID, id, payload) + if err != nil { + logger.Errorf("Failed to update Account with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully updated Account with Portfolio ID: %s and Account ID: %s", portfolioID, id) + + return commonHTTP.OK(c, account) +} + +// DeleteAccountByID is a method that removes Account information by a given ids. +func (handler *AccountHandler) DeleteAccountByID(c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + portfolioID := c.Params("portfolio_id") + id := c.Params("id") + + logger.Infof("Initiating removal of Account with Portfolio ID: %s and Account ID: %s", portfolioID, id) + + if err := handler.Command.DeleteAccountByID(ctx, organizationID, ledgerID, portfolioID, id); err != nil { + logger.Errorf("Failed to remove Account with Portfolio ID: %s and Account ID: %s, Error: %s", portfolioID, id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully removed Account with Portfolio ID: %s and Account ID: %s", portfolioID, id) + + return commonHTTP.NoContent(c) +} diff --git a/components/ledger/internal/ports/http/routes.go b/components/ledger/internal/ports/http/routes.go new file mode 100644 index 00000000..074adbc1 --- /dev/null +++ b/components/ledger/internal/ports/http/routes.go @@ -0,0 +1,79 @@ +package http + +import ( + lib "github.com/LerianStudio/midaz/common/net/http" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + a "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/account" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + r "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + "github.com/LerianStudio/midaz/components/ledger/internal/ports" + "github.com/LerianStudio/midaz/components/ledger/internal/service" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +// NewRouter registers routes to the Server. +func NewRouter(ah *ports.AccountHandler, ph *ports.PortfolioHandler, lh *ports.LedgerHandler, ih *ports.InstrumentHandler, oh *ports.OrganizationHandler, rh *ports.ProductHandler) *fiber.App { + f := fiber.New() + + _ = service.NewConfig() + + f.Use(cors.New()) + f.Use(lib.WithCorrelationID()) + + // jwt := lib.NewJWTMiddleware(config.JWKAddress) + + // -- Routes -- + + // Organizations + f.Post("/v1/organizations", lib.WithBody(new(o.CreateOrganizationInput), oh.CreateOrganization)) + f.Patch("/v1/organizations/:id", lib.WithBody(new(o.UpdateOrganizationInput), oh.UpdateOrganization)) + f.Get("/v1/organizations", oh.GetAllOrganizations) + f.Get("/v1/organizations/:id", oh.GetOrganizationByID) + f.Delete("/v1/organizations/:id", oh.DeleteOrganizationByID) + + // Ledgers + f.Post("/v1/organizations/:organization_id/ledgers", lib.WithBody(new(l.CreateLedgerInput), lh.CreateLedger)) + f.Patch("/v1/organizations/:organization_id/ledgers/:id", lib.WithBody(new(l.UpdateLedgerInput), lh.UpdateLedger)) + f.Get("/v1/organizations/:organization_id/ledgers", lh.GetAllLedgers) + f.Get("/v1/organizations/:organization_id/ledgers/:id", lh.GetLedgerByID) + f.Delete("/v1/organizations/:organization_id/ledgers/:id", lh.DeleteLedgerByID) + + // Instruments + f.Post("/v1/organizations/:organization_id/ledgers/:ledger_id/instruments", lib.WithBody(new(i.CreateInstrumentInput), ih.CreateInstrument)) + f.Patch("/v1/organizations/:organization_id/ledgers/:ledger_id/instruments/:id", lib.WithBody(new(i.UpdateInstrumentInput), ih.UpdateInstrument)) + f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/instruments", ih.GetAllInstruments) + f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/instruments/:id", ih.GetInstrumentByID) + f.Delete("/v1/organizations/:organization_id/ledgers/:ledger_id/instruments/:id", ih.DeleteInstrumentByID) + + // Portfolios + f.Post("/v1/organizations/:organization_id/ledgers/:ledger_id/portfolios", lib.WithBody(new(p.CreatePortfolioInput), ph.CreatePortfolio)) + f.Patch("/v1/organizations/:organization_id/ledgers/:ledger_id/portfolios/:id", lib.WithBody(new(p.UpdatePortfolioInput), ph.UpdatePortfolio)) + f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/portfolios", ph.GetAllPortfolios) + f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/portfolios/:id", ph.GetPortfolioByID) + f.Delete("/v1/organizations/:organization_id/ledgers/:ledger_id/portfolios/:id", ph.DeletePortfolioByID) + + // Product + f.Post("/v1/organizations/:organization_id/ledgers/:ledger_id/products", lib.WithBody(new(r.CreateProductInput), rh.CreateProduct)) + f.Patch("/v1/organizations/:organization_id/ledgers/:ledger_id/products/:id", lib.WithBody(new(r.UpdateProductInput), rh.UpdateProduct)) + f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/products", rh.GetAllProducts) + f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/products/:id", rh.GetProductByID) + f.Delete("/v1/organizations/:organization_id/ledgers/:ledger_id/products/:id", rh.DeleteProductByID) + + // Accounts + f.Post("/v1/organizations/:organization_id/ledgers/:ledger_id/portfolios/:portfolio_id/accounts", lib.WithBody(new(a.CreateAccountInput), ah.CreateAccount)) + f.Patch("/v1/organizations/:organization_id/ledgers/:ledger_id/portfolios/:portfolio_id/accounts/:id", lib.WithBody(new(a.UpdateAccountInput), ah.UpdateAccount)) + f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/portfolios/:portfolio_id/accounts", ah.GetAllAccounts) + f.Get("/v1/organizations/:organization_id/ledgers/:ledger_id/portfolios/:portfolio_id/accounts/:id", ah.GetAccountByID) + f.Delete("/v1/organizations/:organization_id/ledgers/:ledger_id/portfolios/:portfolio_id/accounts/:id", ah.DeleteAccountByID) + + // Health + f.Get("/health", lib.Ping) + + // Doc + lib.DocAPI("ledger", "Ledger API", f) + + return f +} diff --git a/components/ledger/internal/ports/instrument.go b/components/ledger/internal/ports/instrument.go new file mode 100644 index 00000000..879b69ee --- /dev/null +++ b/components/ledger/internal/ports/instrument.go @@ -0,0 +1,156 @@ +package ports + +import ( + "github.com/LerianStudio/midaz/common/mlog" + commonHTTP "github.com/LerianStudio/midaz/common/net/http" + "github.com/LerianStudio/midaz/components/ledger/internal/app/command" + "github.com/LerianStudio/midaz/components/ledger/internal/app/query" + i "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/instrument" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" +) + +// InstrumentHandler struct contains an instrument use case for managing instrument related operations. +type InstrumentHandler struct { + Command *command.UseCase + Query *query.UseCase +} + +// CreateInstrument is a method that creates instrument information. +func (handler *InstrumentHandler) CreateInstrument(a any, c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + logger.Infof("Initiating create of Instrument with organization ID: %s", organizationID) + + ledgerID := c.Params("ledger_id") + logger.Infof("Initiating create of Instrument with ledger ID: %s", ledgerID) + + payload := a.(*i.CreateInstrumentInput) + logger.Infof("Request to create a Instrument with details: %#v", payload) + + instrument, err := handler.Command.CreateInstrument(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), payload) + if err != nil { + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully created Instrument") + + return commonHTTP.Created(c, instrument) +} + +// GetAllInstruments is a method that retrieves all Instruments. +func (handler *InstrumentHandler) GetAllInstruments(c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + logger.Infof("Initiating create of Instrument with organization ID: %s", organizationID) + + ledgerID := c.Params("ledger_id") + logger.Infof("Initiating create of Instrument with ledger ID: %s", ledgerID) + + for key, value := range c.Queries() { + logger.Infof("Initiating retrieval of all Instruments by metadata") + + organizations, err := handler.Query.GetAllMetadataInstruments(ctx, key, value, organizationID, ledgerID) + if err != nil { + logger.Errorf("Failed to retrieve all Instruments, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Instruments by metadata") + + return commonHTTP.OK(c, organizations) + } + + logger.Infof("Initiating retrieval of all Instruments ") + + instruments, err := handler.Query.GetAllInstruments(ctx, organizationID, ledgerID) + if err != nil { + logger.Errorf("Failed to retrieve all Instruments, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Instruments") + + return commonHTTP.OK(c, instruments) +} + +// GetInstrumentByID is a method that retrieves Instrument information by a given id. +func (handler *InstrumentHandler) GetInstrumentByID(c *fiber.Ctx) error { + ctx := c.UserContext() + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + id := c.Params("id") + + logger := mlog.NewLoggerFromContext(ctx) + + logger.Infof("Initiating retrieval of Instrument with Ledger ID: %s and Instrument ID: %s", ledgerID, id) + + instrument, err := handler.Query.GetInstrumentByID(ctx, organizationID, ledgerID, id) + if err != nil { + logger.Errorf("Failed to retrieve Instrument with Ledger ID: %s and Instrument ID: %s, Error: %s", ledgerID, id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved Instrument with Ledger ID: %s and Instrument ID: %s", ledgerID, id) + + return commonHTTP.OK(c, instrument) +} + +// UpdateInstrument is a method that updates Instrument information. +func (handler *InstrumentHandler) UpdateInstrument(a any, c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + id := c.Params("id") + + logger.Infof("Initiating update of Instrument with Ledger ID: %s and Instrument ID: %s", ledgerID, id) + + payload := a.(*i.UpdateInstrumentInput) + logger.Infof("Request to update an Instrument with details: %#v", payload) + + _, err := handler.Command.UpdateInstrumentByID(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(id), payload) + if err != nil { + logger.Errorf("Failed to update Instrument with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + instrument, err := handler.Query.GetInstrumentByID(ctx, organizationID, ledgerID, id) + if err != nil { + logger.Errorf("Failed to get update Instrument with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully updated Instrument with Ledger ID: %s and Instrument ID: %s", ledgerID, id) + + return commonHTTP.OK(c, instrument) +} + +// DeleteInstrumentByID is a method that removes Instrument information by a given ids. +func (handler *InstrumentHandler) DeleteInstrumentByID(c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + id := c.Params("id") + + logger.Infof("Initiating removal of Instrument with Ledger ID: %s and Instrument ID: %s", ledgerID, id) + + if err := handler.Command.DeleteInstrumentByID(ctx, uuid.MustParse(organizationID), uuid.MustParse(ledgerID), uuid.MustParse(id)); err != nil { + logger.Errorf("Failed to remove Instrument with Ledger ID: %s and Instrument ID: %s, Error: %s", ledgerID, id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully removed Instrument with Ledger ID: %s and Instrument ID: %s", ledgerID, id) + + return commonHTTP.NoContent(c) +} diff --git a/components/ledger/internal/ports/ledger.go b/components/ledger/internal/ports/ledger.go new file mode 100644 index 00000000..aed5d842 --- /dev/null +++ b/components/ledger/internal/ports/ledger.go @@ -0,0 +1,155 @@ +package ports + +import ( + "os" + + "github.com/LerianStudio/midaz/common/mlog" + commonHTTP "github.com/LerianStudio/midaz/common/net/http" + "github.com/LerianStudio/midaz/components/ledger/internal/app/command" + "github.com/LerianStudio/midaz/components/ledger/internal/app/query" + l "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/ledger" + "github.com/gofiber/fiber/v2" +) + +// LedgerHandler struct contains a ledger use case for managing ledger related operations. +type LedgerHandler struct { + Command *command.UseCase + Query *query.UseCase +} + +// CreateLedger is a method that creates Ledger information. +func (handler *LedgerHandler) CreateLedger(i any, c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + + payload := i.(*l.CreateLedgerInput) + logger.Infof("Request to create an ledger with details: %#v", payload) + + ledger, err := handler.Command.CreateLedger(ctx, organizationID, payload) + if err != nil { + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully created ledger") + + return commonHTTP.Created(c, ledger) +} + +// GetLedgerByID is a method that retrieves Ledger information by a given id. +func (handler *LedgerHandler) GetLedgerByID(c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := mlog.NewLoggerFromContext(ctx) + + id := c.Params("id") + logger.Infof("Initiating retrieval of Ledger with ID: %s", id) + + organizationID := c.Params("organization_id") + + ledger, err := handler.Query.GetLedgerByID(ctx, organizationID, id) + if err != nil { + logger.Errorf("Failed to retrieve Ledger with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved Ledger with ID: %s", id) + + return commonHTTP.OK(c, ledger) +} + +// GetAllLedgers is a method that retrieves all ledgers. +func (handler *LedgerHandler) GetAllLedgers(c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + + for key, value := range c.Queries() { + logger.Infof("Initiating retrieval of all Ledgers by metadata") + + ledgers, err := handler.Query.GetAllMetadataLedgers(ctx, key, value, organizationID) + if err != nil { + logger.Errorf("Failed to retrieve all Ledgers, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Ledgers by metadata") + + return commonHTTP.OK(c, ledgers) + } + + logger.Infof("Initiating retrieval of all Ledgers ") + + ledgers, err := handler.Query.GetAllLedgers(ctx, organizationID) + if err != nil { + logger.Errorf("Failed to retrieve all Ledgers, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Ledgers") + + return commonHTTP.OK(c, ledgers) +} + +// UpdateLedger is a method that updates Ledger information. +func (handler *LedgerHandler) UpdateLedger(p any, c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + id := c.Params("id") + logger.Infof("Initiating update of Ledger with ID: %s", id) + + organizationID := c.Params("organization_id") + + payload := p.(*l.UpdateLedgerInput) + logger.Infof("Request to update an Ledger with details: %#v", payload) + + _, err := handler.Command.UpdateLedgerByID(ctx, organizationID, id, payload) + if err != nil { + logger.Errorf("Failed to update Ledger with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + ledger, err := handler.Query.GetLedgerByID(ctx, organizationID, id) + if err != nil { + logger.Errorf("Failed to retrieve Ledger with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully updated Ledger with ID: %s", id) + + return commonHTTP.OK(c, ledger) +} + +// DeleteLedgerByID is a method that removes Ledger information by a given id. +func (handler *LedgerHandler) DeleteLedgerByID(c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := mlog.NewLoggerFromContext(ctx) + + id := c.Params("id") + logger.Infof("Initiating removal of Ledeger with ID: %s", id) + + organizationID := c.Params("organization_id") + + if os.Getenv("ENV_NAME") == "production" { + logger.Errorf("Failed to remove Ledger with ID: %s in ", id) + + return commonHTTP.BadRequest(c, &fiber.Map{ + "code": "0008", + "message": "Action not allowed.", + }) + } + + if err := handler.Command.DeleteLedgerByID(ctx, organizationID, id); err != nil { + logger.Errorf("Failed to remove Ledeger with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully removed Ledeger with ID: %s", id) + + return commonHTTP.NoContent(c) +} diff --git a/components/ledger/internal/ports/organization.go b/components/ledger/internal/ports/organization.go new file mode 100644 index 00000000..76733d43 --- /dev/null +++ b/components/ledger/internal/ports/organization.go @@ -0,0 +1,146 @@ +package ports + +import ( + "os" + + "github.com/LerianStudio/midaz/common/mlog" + commonHTTP "github.com/LerianStudio/midaz/common/net/http" + "github.com/LerianStudio/midaz/components/ledger/internal/app/command" + "github.com/LerianStudio/midaz/components/ledger/internal/app/query" + o "github.com/LerianStudio/midaz/components/ledger/internal/domain/onboarding/organization" + "github.com/gofiber/fiber/v2" +) + +// OrganizationHandler struct contains an organization use case for managing organization related operations. +type OrganizationHandler struct { + Command *command.UseCase + Query *query.UseCase +} + +// CreateOrganization is a method that creates Organization information. +func (handler *OrganizationHandler) CreateOrganization(p any, c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := mlog.NewLoggerFromContext(ctx) + + payload := p.(*o.CreateOrganizationInput) + logger.Infof("Request to create an organization with details: %#v", payload) + + organization, err := handler.Command.CreateOrganization(ctx, payload) + if err != nil { + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully created organization: %s", organization) + + return commonHTTP.Created(c, organization) +} + +// UpdateOrganization is a method that updates Organization information. +func (handler *OrganizationHandler) UpdateOrganization(p any, c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + id := c.Params("id") + logger.Infof("Initiating update of Organization with ID: %s", id) + + payload := p.(*o.UpdateOrganizationInput) + logger.Infof("Request to update an organization with details: %#v", payload) + + _, err := handler.Command.UpdateOrganizationByID(ctx, id, payload) + if err != nil { + logger.Errorf("Failed to update Organization with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + organizations, err := handler.Query.GetOrganizationByID(ctx, id) + if err != nil { + logger.Errorf("Failed to retrieve Organization with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully updated Organization with ID: %s", id) + + return commonHTTP.OK(c, organizations) +} + +// GetOrganizationByID is a method that retrieves Organization information by a given id. +func (handler *OrganizationHandler) GetOrganizationByID(c *fiber.Ctx) error { + ctx := c.UserContext() + + id := c.Params("id") + + logger := mlog.NewLoggerFromContext(ctx) + + logger.Infof("Initiating retrieval of Organization with ID: %s", id) + + organizations, err := handler.Query.GetOrganizationByID(ctx, id) + if err != nil { + logger.Errorf("Failed to retrieve Organization with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved Organization with ID: %s", id) + + return commonHTTP.OK(c, organizations) +} + +// GetAllOrganizations is a method that retrieves all Organizations. +func (handler *OrganizationHandler) GetAllOrganizations(c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + for key, value := range c.Queries() { + logger.Infof("Initiating retrieval of all Organizations by metadata") + + organizations, err := handler.Query.GetAllMetadataOrganizations(ctx, key, value) + if err != nil { + logger.Errorf("Failed to retrieve all Organizations, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Organizations by metadata") + + return commonHTTP.OK(c, organizations) + } + + logger.Infof("Initiating retrieval of all Organizations ") + + organizations, err := handler.Query.GetAllOrganizations(ctx) + if err != nil { + logger.Errorf("Failed to retrieve all Organizations, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Organizations") + + return commonHTTP.OK(c, organizations) +} + +// DeleteOrganizationByID is a method that removes Organization information by a given id. +func (handler *OrganizationHandler) DeleteOrganizationByID(c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := mlog.NewLoggerFromContext(ctx) + + id := c.Params("id") + logger.Infof("Initiating removal of Organization with ID: %s", id) + + if os.Getenv("ENV_NAME") == "production" { + logger.Errorf("Failed to remove Organization with ID: %s in ", id) + + return commonHTTP.BadRequest(c, &fiber.Map{ + "code": "0008", + "message": "Action not allowed.", + }) + } + + if err := handler.Command.DeleteOrganizationByID(ctx, id); err != nil { + logger.Errorf("Failed to remove Organization with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully removed Organization with ID: %s", id) + + return commonHTTP.NoContent(c) +} diff --git a/components/ledger/internal/ports/portfolio.go b/components/ledger/internal/ports/portfolio.go new file mode 100644 index 00000000..4a4efb42 --- /dev/null +++ b/components/ledger/internal/ports/portfolio.go @@ -0,0 +1,153 @@ +package ports + +import ( + "github.com/LerianStudio/midaz/common/mlog" + commonHTTP "github.com/LerianStudio/midaz/common/net/http" + "github.com/LerianStudio/midaz/components/ledger/internal/app/command" + "github.com/LerianStudio/midaz/components/ledger/internal/app/query" + p "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/portfolio" + "github.com/gofiber/fiber/v2" +) + +// PortfolioHandler struct contains a portfolio use case for managing portfolio related operations. +type PortfolioHandler struct { + Command *command.UseCase + Query *query.UseCase +} + +// CreatePortfolio is a method that creates portfolio information. +func (handler *PortfolioHandler) CreatePortfolio(i any, c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + + logger.Infof("Initiating create of Portfolio with ledger ID: %s", ledgerID) + + payload := i.(*p.CreatePortfolioInput) + + logger.Infof("Request to create a Portfolio with details: %#v", payload) + + portfolio, err := handler.Command.CreatePortfolio(ctx, organizationID, ledgerID, payload) + if err != nil { + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully created Portfolio") + + return commonHTTP.Created(c, portfolio) +} + +// GetAllPortfolios is a method that retrieves all Portfolios. +func (handler *PortfolioHandler) GetAllPortfolios(c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + logger.Infof("Get Portfolios with Organization: %s and Ledger ID: %s", organizationID, ledgerID) + + for key, value := range c.Queries() { + logger.Infof("Initiating retrieval of all Portfolios by metadata") + + portfolios, err := handler.Query.GetAllMetadataPortfolios(ctx, key, value, organizationID, ledgerID) + if err != nil { + logger.Errorf("Failed to retrieve all Portfolios, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Portfolios by metadata") + + return commonHTTP.OK(c, portfolios) + } + + logger.Infof("Initiating retrieval of all Portfolios") + + portfolios, err := handler.Query.GetAllPortfolio(ctx, organizationID, ledgerID) + if err != nil { + logger.Errorf("Failed to retrieve all Portfolios, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Portfolios") + + return commonHTTP.OK(c, portfolios) +} + +// GetPortfolioByID is a method that retrieves Portfolio information by a given id. +func (handler *PortfolioHandler) GetPortfolioByID(c *fiber.Ctx) error { + ctx := c.UserContext() + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + id := c.Params("id") + + logger := mlog.NewLoggerFromContext(ctx) + + logger.Infof("Initiating retrieval of Portfolio with Organization: %s Ledger ID: %s and Portfolio ID: %s", organizationID, ledgerID, id) + + portfolio, err := handler.Query.GetPortfolioByID(ctx, organizationID, ledgerID, id) + if err != nil { + logger.Errorf("Failed to retrieve Portfolio with Ledger ID: %s and Portfolio ID: %s, Error: %s", ledgerID, id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved Portfolio with Ledger ID: %s and Portfolio ID: %s", ledgerID, id) + + return commonHTTP.OK(c, portfolio) +} + +// UpdatePortfolio is a method that updates Portfolio information. +func (handler *PortfolioHandler) UpdatePortfolio(i any, c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + id := c.Params("id") + + logger.Infof("Initiating update of Portfolio with Organization: %s Ledger ID: %s and Portfolio ID: %s", organizationID, ledgerID, id) + + payload := i.(*p.UpdatePortfolioInput) + logger.Infof("Request to update an Portfolio with details: %#v", payload) + + _, err := handler.Command.UpdatePortfolioByID(ctx, organizationID, ledgerID, id, payload) + if err != nil { + logger.Errorf("Failed to update Portfolio with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + portfolio, err := handler.Query.GetPortfolioByID(ctx, organizationID, ledgerID, id) + if err != nil { + logger.Errorf("Failed to retrieve Portfolio with Ledger ID: %s and Portfolio ID: %s, Error: %s", ledgerID, id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully updated Portfolio with Ledger ID: %s and Portfolio ID: %s", ledgerID, id) + + return commonHTTP.OK(c, portfolio) +} + +// DeletePortfolioByID is a method that removes Portfolio information by a given ids. +func (handler *PortfolioHandler) DeletePortfolioByID(c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + id := c.Params("id") + + logger.Infof("Initiating removal of Portfolio with Organization: %s Ledger ID: %s and Portfolio ID: %s", organizationID, ledgerID, id) + + if err := handler.Command.DeletePortfolioByID(ctx, organizationID, ledgerID, id); err != nil { + logger.Errorf("Failed to remove Portfolio with Ledger ID: %s and Portfolio ID: %s, Error: %s", ledgerID, id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully removed Portfolio with Ledger ID: %s and Portfolio ID: %s", ledgerID, id) + + return commonHTTP.NoContent(c) +} diff --git a/components/ledger/internal/ports/product.go b/components/ledger/internal/ports/product.go new file mode 100644 index 00000000..7bad2e63 --- /dev/null +++ b/components/ledger/internal/ports/product.go @@ -0,0 +1,148 @@ +package ports + +import ( + "github.com/LerianStudio/midaz/common/mlog" + commonHTTP "github.com/LerianStudio/midaz/common/net/http" + "github.com/LerianStudio/midaz/components/ledger/internal/app/command" + "github.com/LerianStudio/midaz/components/ledger/internal/app/query" + r "github.com/LerianStudio/midaz/components/ledger/internal/domain/portfolio/product" + "github.com/gofiber/fiber/v2" +) + +// ProductHandler struct contains a product use case for managing product related operations. +type ProductHandler struct { + Command *command.UseCase + Query *query.UseCase +} + +// CreateProduct is a method that creates product information. +func (handler *ProductHandler) CreateProduct(i any, c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + logger.Infof("Initiating create of Product with organization ID: %s and ledger ID: %s", organizationID, ledgerID) + + payload := i.(*r.CreateProductInput) + logger.Infof("Request to create a Product with details: %#v", payload) + + product, err := handler.Command.CreateProduct(ctx, organizationID, ledgerID, payload) + if err != nil { + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully created Product") + + return commonHTTP.Created(c, product) +} + +// GetAllProducts is a method that retrieves all Products. +func (handler *ProductHandler) GetAllProducts(c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + logger.Infof("Get Products with organization ID: %s and ledger ID: %s", organizationID, ledgerID) + + for key, value := range c.Queries() { + logger.Infof("Initiating retrieval of all Products by metadata") + + organizations, err := handler.Query.GetAllMetadataProducts(ctx, key, value, organizationID, ledgerID) + if err != nil { + logger.Errorf("Failed to retrieve all Products, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Products by metadata") + + return commonHTTP.OK(c, organizations) + } + + logger.Infof("Initiating retrieval of all Products ") + + products, err := handler.Query.GetAllProducts(ctx, organizationID, ledgerID) + if err != nil { + logger.Errorf("Failed to retrieve all Products, Error: %s", err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved all Products") + + return commonHTTP.OK(c, products) +} + +// GetProductByID is a method that retrieves Product information by a given id. +func (handler *ProductHandler) GetProductByID(c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + id := c.Params("id") + logger.Infof("Initiating retrieval of Product with Organization ID: %s and Ledger ID: %s and Product ID: %s", organizationID, ledgerID, id) + + product, err := handler.Query.GetProductByID(ctx, organizationID, ledgerID, id) + if err != nil { + logger.Errorf("Failed to retrieve Product with Ledger ID: %s and Product ID: %s, Error: %s", ledgerID, id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully retrieved Product with Organization ID: %s and Ledger ID: %s and Product ID: %s", organizationID, ledgerID, id) + + return commonHTTP.OK(c, product) +} + +// UpdateProduct is a method that updates Product information. +func (handler *ProductHandler) UpdateProduct(i any, c *fiber.Ctx) error { + ctx := c.UserContext() + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + id := c.Params("id") + logger.Infof("Initiating update of Product with Organization ID: %s and Ledger ID: %s and Product ID: %s", organizationID, ledgerID, id) + + payload := i.(*r.UpdateProductInput) + logger.Infof("Request to update an Product with details: %#v", payload) + + _, err := handler.Command.UpdateProductByID(ctx, organizationID, ledgerID, id, payload) + if err != nil { + logger.Errorf("Failed to update Product with ID: %s, Error: %s", id, err.Error()) + return commonHTTP.WithError(c, err) + } + + product, err := handler.Query.GetProductByID(ctx, organizationID, ledgerID, id) + if err != nil { + logger.Errorf("Failed to retrieve Product with Ledger ID: %s and Product ID: %s, Error: %s", ledgerID, id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully updated Product with Organization ID: %s and Ledger ID: %s and Product ID: %s", organizationID, ledgerID, id) + + return commonHTTP.OK(c, product) +} + +// DeleteProductByID is a method that removes Product information by a given ids. +func (handler *ProductHandler) DeleteProductByID(c *fiber.Ctx) error { + ctx := c.UserContext() + + logger := mlog.NewLoggerFromContext(ctx) + + organizationID := c.Params("organization_id") + ledgerID := c.Params("ledger_id") + id := c.Params("id") + + logger.Infof("Initiating removal of Product with Organization ID: %s and Ledger ID: %s and Product ID: %s", organizationID, ledgerID, id) + + if err := handler.Command.DeleteProductByID(ctx, organizationID, ledgerID, id); err != nil { + logger.Errorf("Failed to remove Product with Ledger ID: %s and Product ID: %s, Error: %s", ledgerID, id, err.Error()) + return commonHTTP.WithError(c, err) + } + + logger.Infof("Successfully removed Product with Organization ID: %s and Ledger ID: %s and Product ID: %s", organizationID, ledgerID, id) + + return commonHTTP.NoContent(c) +} diff --git a/components/ledger/internal/service/config.go b/components/ledger/internal/service/config.go new file mode 100644 index 00000000..6b054895 --- /dev/null +++ b/components/ledger/internal/service/config.go @@ -0,0 +1,37 @@ +package service + +import ( + common "github.com/LerianStudio/midaz/common" +) + +// Config is the top level configuration struct for the entire application. +type Config struct { + EnvName string `env:"ENV_NAME"` + ServerAddress string `env:"SERVER_ADDRESS"` + PrimaryDBHost string `env:"DB_HOST"` + PrimaryDBUser string `env:"DB_USER"` + PrimaryDBPassword string `env:"DB_PASSWORD"` + PrimaryDBName string `env:"DB_NAME"` + PrimaryDBPort string `env:"DB_PORT"` + ReplicaDBHost string `env:"DB_REPLICA_HOST"` + ReplicaDBUser string `env:"DB_REPLICA_USER"` + ReplicaDBPassword string `env:"DB_REPLICA_PASSWORD"` + ReplicaDBName string `env:"DB_REPLICA_NAME"` + ReplicaDBPort string `env:"DB_REPLICA_PORT"` + MongoDBHost string `env:"MONGO_HOST"` + MongoDBName string `env:"MONGO_NAME"` + MongoDBUser string `env:"MONGO_USER"` + MongoDBPassword string `env:"MONGO_PASSWORD"` + MongoDBPort string `env:"MONGO_PORT"` +} + +// NewConfig creates a instance of Config. +func NewConfig() *Config { + cfg := &Config{} + + if err := common.SetConfigFromEnvVars(cfg); err != nil { + panic(err) + } + + return cfg +} diff --git a/components/ledger/internal/service/server.go b/components/ledger/internal/service/server.go new file mode 100644 index 00000000..f4570159 --- /dev/null +++ b/components/ledger/internal/service/server.go @@ -0,0 +1,39 @@ +package service + +import ( + common "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" + "github.com/gofiber/fiber/v2" + "github.com/pkg/errors" +) + +// Server represents the http server for Ledger service. +type Server struct { + app *fiber.App + serverAddress string + mlog.Logger +} + +// ServerAddress returns is a convenience method to return the server address. +func (s *Server) ServerAddress() string { + return s.serverAddress +} + +// NewServer creates an instance of Server. +func NewServer(cfg *Config, app *fiber.App, logger mlog.Logger) *Server { + return &Server{ + app: app, + serverAddress: cfg.ServerAddress, + Logger: logger, + } +} + +// Run runs the server. +func (s *Server) Run(l *common.Launcher) error { + err := s.app.Listen(s.ServerAddress()) + if err != nil { + return errors.Wrap(err, "failed to run the server") + } + + return nil +} diff --git a/components/ledger/internal/service/service.go b/components/ledger/internal/service/service.go new file mode 100644 index 00000000..fc34c286 --- /dev/null +++ b/components/ledger/internal/service/service.go @@ -0,0 +1,21 @@ +package service + +import ( + common "github.com/LerianStudio/midaz/common" + "github.com/LerianStudio/midaz/common/mlog" +) + +// Service is the application glue where we put all top level components to be used. +type Service struct { + *Server + mlog.Logger +} + +// Run starts the application. +// This is the only necessary code to run an app in main.go +func (app *Service) Run() { + common.NewLauncher( + common.WithLogger(app.Logger), + common.RunApp("service", app.Server), + ).Run() +} diff --git a/components/ledger/migrations/000001_create_organization_table.down.sql b/components/ledger/migrations/000001_create_organization_table.down.sql new file mode 100644 index 00000000..8f51f99f --- /dev/null +++ b/components/ledger/migrations/000001_create_organization_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS organization; diff --git a/components/ledger/migrations/000001_create_organization_table.up.sql b/components/ledger/migrations/000001_create_organization_table.up.sql new file mode 100644 index 00000000..a498f910 --- /dev/null +++ b/components/ledger/migrations/000001_create_organization_table.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS organization +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + parent_organization_id UUID, + legal_name TEXT NOT NULL, + doing_business_as TEXT, + legal_document TEXT NOT NULL, + address JSONB NOT NULL, + status TEXT NOT NULL, + status_description TEXT, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + FOREIGN KEY (parent_organization_id) REFERENCES organization (id) +); \ No newline at end of file diff --git a/components/ledger/migrations/000002_create_ledger_table.down.sql b/components/ledger/migrations/000002_create_ledger_table.down.sql new file mode 100644 index 00000000..4b8c3e92 --- /dev/null +++ b/components/ledger/migrations/000002_create_ledger_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS ledger; diff --git a/components/ledger/migrations/000002_create_ledger_table.up.sql b/components/ledger/migrations/000002_create_ledger_table.up.sql new file mode 100644 index 00000000..7cf90a02 --- /dev/null +++ b/components/ledger/migrations/000002_create_ledger_table.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS ledger +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + organization_id UUID NOT NULL, + status TEXT NOT NULL, + status_description TEXT, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + FOREIGN KEY (organization_id) REFERENCES organization (id) +); \ No newline at end of file diff --git a/components/ledger/migrations/000003_create_instrument_table.down.sql b/components/ledger/migrations/000003_create_instrument_table.down.sql new file mode 100644 index 00000000..e48f9343 --- /dev/null +++ b/components/ledger/migrations/000003_create_instrument_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS instrument; diff --git a/components/ledger/migrations/000003_create_instrument_table.up.sql b/components/ledger/migrations/000003_create_instrument_table.up.sql new file mode 100644 index 00000000..2ec2b960 --- /dev/null +++ b/components/ledger/migrations/000003_create_instrument_table.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS instrument +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT, + type TEXT NOT NULL, + code TEXT NOT NULL, + status TEXT NOT NULL, + status_description TEXT, + ledger_id UUID NOT NULL, + organization_id UUID NOT NULL, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + FOREIGN KEY (ledger_id) REFERENCES ledger (id), + FOREIGN KEY (organization_id) REFERENCES organization (id), + UNIQUE (code) +); \ No newline at end of file diff --git a/components/ledger/migrations/000004_create_product_table.down.sql b/components/ledger/migrations/000004_create_product_table.down.sql new file mode 100644 index 00000000..6c3c4f5e --- /dev/null +++ b/components/ledger/migrations/000004_create_product_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS product; diff --git a/components/ledger/migrations/000004_create_product_table.up.sql b/components/ledger/migrations/000004_create_product_table.up.sql new file mode 100644 index 00000000..e3f47f58 --- /dev/null +++ b/components/ledger/migrations/000004_create_product_table.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS product +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT, + ledger_id UUID NOT NULL, + organization_id UUID NOT NULL, + status TEXT NOT NULL, + status_description TEXT, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + FOREIGN KEY (ledger_id) REFERENCES ledger (id), + FOREIGN KEY (organization_id) REFERENCES organization (id) +); \ No newline at end of file diff --git a/components/ledger/migrations/000005_create_portfolio_table.down.sql b/components/ledger/migrations/000005_create_portfolio_table.down.sql new file mode 100644 index 00000000..e0c4d4f4 --- /dev/null +++ b/components/ledger/migrations/000005_create_portfolio_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS portfolio; diff --git a/components/ledger/migrations/000005_create_portfolio_table.up.sql b/components/ledger/migrations/000005_create_portfolio_table.up.sql new file mode 100644 index 00000000..8ad4fd5b --- /dev/null +++ b/components/ledger/migrations/000005_create_portfolio_table.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS portfolio +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT, + entity_id UUID NOT NULL, + ledger_id UUID NOT NULL, + organization_id UUID NOT NULL, + status TEXT NOT NULL, + status_description TEXT, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + FOREIGN KEY (ledger_id) REFERENCES ledger (id), + FOREIGN KEY (organization_id) REFERENCES organization (id) +); \ No newline at end of file diff --git a/components/ledger/migrations/000006_create_account_table.down.sql b/components/ledger/migrations/000006_create_account_table.down.sql new file mode 100644 index 00000000..8bb10aa3 --- /dev/null +++ b/components/ledger/migrations/000006_create_account_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS account; diff --git a/components/ledger/migrations/000006_create_account_table.up.sql b/components/ledger/migrations/000006_create_account_table.up.sql new file mode 100644 index 00000000..f0c2f07f --- /dev/null +++ b/components/ledger/migrations/000006_create_account_table.up.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS account +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT, + parent_account_id UUID, + entity_id UUID NOT NULL, + instrument_code TEXT NOT NULL, + organization_id UUID NOT NULL, + ledger_id UUID NOT NULL, + portfolio_id UUID NOT NULL, + product_id UUID NOT NULL, + available_balance NUMERIC NOT NULL, + on_hold_balance NUMERIC NOT NULL, + balance_scale NUMERIC NOT NULL, + status TEXT NOT NULL, + status_description TEXT, + allow_sending BOOLEAN NOT NULL, + allow_receiving BOOLEAN NOT NULL, + alias TEXT NOT NULL, + type TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + FOREIGN KEY (parent_account_id) REFERENCES account (id), + FOREIGN KEY (instrument_code) REFERENCES instrument (code), + FOREIGN KEY (organization_id) REFERENCES organization (id), + FOREIGN KEY (ledger_id) REFERENCES ledger (id), + FOREIGN KEY (portfolio_id) REFERENCES portfolio (id), + FOREIGN KEY (product_id) REFERENCES product (id) + ); \ No newline at end of file diff --git a/components/ledger/setup/00_init.sql b/components/ledger/setup/00_init.sql new file mode 100644 index 00000000..6422abf1 --- /dev/null +++ b/components/ledger/setup/00_init.sql @@ -0,0 +1,2 @@ +create user replicator with replication encrypted password 'replicator_password'; +select pg_create_physical_replication_slot('replication_slot'); \ No newline at end of file diff --git a/components/mdz/cmd/login/login.go b/components/mdz/cmd/login/login.go new file mode 100644 index 00000000..32909f1e --- /dev/null +++ b/components/mdz/cmd/login/login.go @@ -0,0 +1,162 @@ +package login + +import ( + "context" + "errors" + "fmt" + "net/url" + "time" + + "github.com/LerianStudio/midaz/components/mdz/pkg" + "github.com/pkg/browser" + "github.com/pterm/pterm" + + "github.com/spf13/cobra" + "github.com/zitadel/oidc/v2/pkg/client/rp" + "github.com/zitadel/oidc/v2/pkg/oidc" +) + +// LogIn func that can log in on midaz +func LogIn(ctx context.Context, dialog Dialog, relyingParty rp.RelyingParty) (*oidc.AccessTokenResponse, error) { + deviceCode, err := rp.DeviceAuthorization(relyingParty.OAuthConfig().Scopes, relyingParty) + if err != nil { + return nil, err + } + + uri, err := url.Parse(deviceCode.VerificationURI) + if err != nil { + panic(err) + } + + query := uri.Query() + query.Set("user_code", deviceCode.UserCode) + uri.RawQuery = query.Encode() + + if err := browser.OpenURL(uri.String()); err != nil { + if !errors.Is(err, pkg.ErrOpenningBrowser) { + return nil, err + } + + fmt.Println("No browser detected") + } + + dialog.DisplayURIAndCode(deviceCode.VerificationURI, deviceCode.UserCode) + + return rp.DeviceAccessToken(ctx, deviceCode.DeviceCode, time.Duration(deviceCode.Interval)*time.Second, relyingParty) +} + +// Dialog provides an interface for DisplayURIAndCode +type Dialog interface { + DisplayURIAndCode(uri, code string) +} + +// DialogFn is a func that implements uri and code +type DialogFn func(uri, code string) + +// DisplayURIAndCode is a func that return a type DialogFn +func (fn DialogFn) DisplayURIAndCode(uri, code string) { + fn(uri, code) +} + +// Store is a struct designed to encapsulate payload data. +type Store struct { + Profile *pkg.Profile `json:"-"` + DeviceCode string `json:"deviceCode"` + LoginURI string `json:"loginUri"` + BrowserURL string `json:"browserUrl"` + Success bool `json:"success"` +} + +// Controller is a struct to encapsulate a *Store struct. +type Controller struct { + store *Store +} + +// NewDefaultLoginStore is a func that return a struct *Store +func NewDefaultLoginStore() *Store { + return &Store{ + Profile: nil, + DeviceCode: "", + LoginURI: "", + BrowserURL: "", + Success: false, + } +} + +// GetStore is a func that return a struct *Store +func (c *Controller) GetStore() *Store { + return c.store +} + +// NewLoginController is a func that return a struct *Controller +func NewLoginController() *Controller { + return &Controller{ + store: NewDefaultLoginStore(), + } +} + +// Run is a fun that executes Open func and return a Renderable interface +func (c *Controller) Run(cmd *cobra.Command, args []string) (pkg.Renderable, error) { + cfg, err := pkg.GetConfig(cmd) + if err != nil { + return nil, err + } + + profile := pkg.GetCurrentProfile(cmd, cfg) + + membershipURI, err := cmd.Flags().GetString(pkg.MembershipURIFlag) + if err != nil { + return nil, err + } + + if membershipURI == "" { + membershipURI = profile.GetMembershipURI() + } + + relyingParty, err := pkg.GetAuthRelyingParty(pkg.GetHTTPClient(cmd, map[string][]string{}), membershipURI) + if err != nil { + return nil, err + } + + c.store.Profile = profile + + ret, err := LogIn(cmd.Context(), DialogFn(func(uri, code string) { + c.store.DeviceCode = code + c.store.LoginURI = uri + fmt.Println("Link :", fmt.Sprintf("%s?user_code=%s", c.store.LoginURI, c.store.DeviceCode)) + }), relyingParty) + if err != nil { + return nil, err + } + + if ret != nil { + c.store.Success = true + + profile.UpdateToken(ret) + } + + profile.SetMembershipURI(membershipURI) + + currentProfileName := pkg.GetCurrentProfileName(cmd, cfg) + + cfg.SetCurrentProfile(currentProfileName, profile) + + return c, cfg.Persist() +} + +// Render is a func that show if you are logged +func (c *Controller) Render(cmd *cobra.Command, args []string) error { + pterm.Success.WithWriter(cmd.OutOrStdout()).Printfln("Logged!") + return nil +} + +// NewCommand is a func that execute some commands +func NewCommand() *cobra.Command { + return pkg.NewCommand("login", + pkg.WithStringFlag(pkg.MembershipURIFlag, "", "service url"), + pkg.WithHiddenFlag(pkg.MembershipURIFlag), + pkg.WithShortDescription("Login"), + pkg.WithArgs(cobra.ExactArgs(0)), + pkg.WithController(NewLoginController()), + ) +} diff --git a/components/mdz/cmd/root.go b/components/mdz/cmd/root.go new file mode 100644 index 00000000..54949d85 --- /dev/null +++ b/components/mdz/cmd/root.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + + "github.com/LerianStudio/midaz/components/mdz/cmd/login" + "github.com/LerianStudio/midaz/components/mdz/cmd/ui" + "github.com/LerianStudio/midaz/components/mdz/cmd/version" + + "github.com/spf13/cobra" +) + +// NewRootCommand is a func that use cmd commands +func NewRootCommand() *cobra.Command { + var cfgFile string + + var debug bool + + cmd := &cobra.Command{ + Use: "mdz", + Short: "mdz is the CLI interface to use Midaz services", + } + + // Global flags + cmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.mdz.yaml)") + cmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "enable debug mode") + + // Subcommands + + // cmd.AddCommand(auth.NewCommand()) // TODO + // cmd.AddCommand(ledger.NewCommand()) // TODO + cmd.AddCommand(login.NewCommand()) + cmd.AddCommand(ui.NewCommand()) + cmd.AddCommand(version.NewCommand()) + + return cmd +} + +// Execute is a func that open nem root command +func Execute() { + cobra.EnableCommandSorting = false + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + + if err := NewRootCommand().ExecuteContext(ctx); err != nil { + fmt.Println(err) + + os.Exit(1) + } + + defer cancel() +} diff --git a/components/mdz/cmd/ui/ui.go b/components/mdz/cmd/ui/ui.go new file mode 100644 index 00000000..c109114e --- /dev/null +++ b/components/mdz/cmd/ui/ui.go @@ -0,0 +1,95 @@ +package ui + +import ( + "fmt" + "os/exec" + "runtime" + + "github.com/LerianStudio/midaz/components/mdz/pkg" + "github.com/spf13/cobra" +) + +// Payload is a struct designed to encapsulate payload data. +type Payload struct { + StackURL string `json:"stackUrl"` + Found bool `json:"browserFound"` +} + +// Controller is a struct designed to encapsulate payload data. +type Controller struct { + store *Payload +} + +var _ pkg.Controller[*Payload] = (*Controller)(nil) + +// NewDefaultUIStore is a func that returns a *UiStruct struct +func NewDefaultUIStore() *Payload { + return &Payload{ + StackURL: "https://console.midaz.cloud", + Found: false, + } +} + +// NewUIController is a func that returns a *Controller struct +func NewUIController() *Controller { + return &Controller{ + store: NewDefaultUIStore(), + } +} + +func openURL(url string) error { + var ( + cmd string + args []string + ) + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + + args = append(args, url) + + return startCommand(cmd, args...) +} + +func startCommand(cmd string, args ...string) error { + c := exec.Command(cmd, args...) + + return c.Start() +} + +// GetStore is a func that returns a *Payload struct +func (c *Controller) GetStore() *Payload { + return c.store +} + +// Run is a fun that executes Open func and return an interface +func (c *Controller) Run(cmd *cobra.Command, args []string) (pkg.Renderable, error) { + if err := openURL(c.store.StackURL); err != nil { + c.store.Found = true + } + + return c, nil +} + +// Render is a func that open Url +func (c *Controller) Render(cmd *cobra.Command, args []string) error { + fmt.Println("Opening url: ", c.store.StackURL) + + return nil +} + +// NewCommand is a func that execute some commands +func NewCommand() *cobra.Command { + return pkg.NewStackCommand("ui", + pkg.WithShortDescription("Open UI"), + pkg.WithArgs(cobra.ExactArgs(0)), + pkg.WithController(NewUIController()), + ) +} diff --git a/components/mdz/cmd/version/version.go b/components/mdz/cmd/version/version.go new file mode 100644 index 00000000..bdd113dc --- /dev/null +++ b/components/mdz/cmd/version/version.go @@ -0,0 +1,68 @@ +package version + +import ( + "github.com/LerianStudio/midaz/components/mdz/pkg" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +// Version variable to set mdz version +var Version = "mdz version mdz1.0.0" + +// Store is a struct designed to encapsulate payload data. +type Store struct { + Version string `json:"version"` + BuildDate string `json:"buildDate"` + Commit string `json:"commit"` +} + +// Controller is a struct that return *VersionStore struct +type Controller struct { + store *Store +} + +var _ pkg.Controller[*Store] = (*Controller)(nil) + +// NewDefaultVersionStore return a *VersionStore struct with the version +func NewDefaultVersionStore() *Store { + return &Store{ + Version: Version, + } +} + +// NewVersionController return a *Controller struct with a new version store +func NewVersionController() *Controller { + return &Controller{ + store: NewDefaultVersionStore(), + } +} + +// NewCommand is a func that execute some commands and return a *cobra.Command struct +func NewCommand() *cobra.Command { + return pkg.NewCommand("version", + pkg.WithShortDescription("Get version"), + pkg.WithArgs(cobra.ExactArgs(0)), + pkg.WithController(NewVersionController()), + ) +} + +// GetStore is a func that return a *VersionStore struct +func (c *Controller) GetStore() *Store { + return c.store +} + +// Run is a func that return a Renderable interface +func (c *Controller) Run(cmd *cobra.Command, args []string) (pkg.Renderable, error) { + return c, nil +} + +// Render is a func that receive a struct *cobra.Command +func (c *Controller) Render(cmd *cobra.Command, args []string) error { + tableData := pterm.TableData{} + tableData = append(tableData, []string{pterm.LightCyan("Version"), c.store.Version}) + + return pterm.DefaultTable. + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render() +} diff --git a/components/mdz/main.go b/components/mdz/main.go new file mode 100644 index 00000000..77fb2453 --- /dev/null +++ b/components/mdz/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/LerianStudio/midaz/components/mdz/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/components/mdz/pkg/claims.go b/components/mdz/pkg/claims.go new file mode 100644 index 00000000..7af311ba --- /dev/null +++ b/components/mdz/pkg/claims.go @@ -0,0 +1,19 @@ +package pkg + +type stackClaim struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` +} +type organizationClaim struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Stacks []stackClaim `json:"stacks"` +} +type ( + organizationsClaim []organizationClaim + userClaims struct { + Email string `json:"email"` + Subject string `json:"sub"` + Org organizationsClaim `json:"org"` + } +) diff --git a/components/mdz/pkg/command.go b/components/mdz/pkg/command.go new file mode 100644 index 00000000..f0feed0a --- /dev/null +++ b/components/mdz/pkg/command.go @@ -0,0 +1,392 @@ +package pkg + +import ( + "encoding/json" + "fmt" + + "github.com/pkg/errors" + "github.com/segmentio/ksuid" + "github.com/spf13/cobra" +) + +const ( + stackFlag = "stack" + organizationFlag = "organization" + outputFlag = "output" +) + +var ( + + // ErrOrganizationNotSpecified indicates that no organization was specified when one was required. + ErrOrganizationNotSpecified = errors.New("organization not specified") + + // ErrMultipleOrganizationsFound indicates that more than one organization was found when only one was expected, and no specific organization was specified. + ErrMultipleOrganizationsFound = errors.New("found more than one organization and no organization specified") +) + +// GetSelectedOrganization retrieves the selected organization from the command. +func GetSelectedOrganization(cmd *cobra.Command) string { + return GetString(cmd, organizationFlag) +} + +// RetrieveOrganizationIDFromFlagOrProfile retrieves the organization ID from the command flag or profile. +func RetrieveOrganizationIDFromFlagOrProfile(cmd *cobra.Command, cfg *Config) (string, error) { + if id := GetSelectedOrganization(cmd); id != "" { + return id, nil + } + + if defaultOrganization := GetCurrentProfile(cmd, cfg).GetDefaultOrganization(); defaultOrganization != "" { + return defaultOrganization, nil + } + + return "", ErrOrganizationNotSpecified +} + +// GetSelectedStackID retrieves the selected stack ID from the command. +func GetSelectedStackID(cmd *cobra.Command) string { + return GetString(cmd, stackFlag) +} + +// CommandOption is an interface for options that can be applied to a cobra.Command. +type CommandOption interface { + apply(cmd *cobra.Command) +} + +// CommandOptionFn is a function that applies a CommandOption to a cobra.Command. +type CommandOptionFn func(cmd *cobra.Command) + +func (fn CommandOptionFn) apply(cmd *cobra.Command) { + fn(cmd) +} + +// WithPersistentStringFlag is a helper function that returns a CommandOptionFn that applies a persistent string flag to a cobra.Command. +func WithPersistentStringFlag(name, defaultValue, help string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.PersistentFlags().String(name, defaultValue, help) + } +} + +// WithStringFlag is a helper function that returns a CommandOptionFn that applies a string flag to a cobra.Command. +func WithStringFlag(name, defaultValue, help string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.Flags().String(name, defaultValue, help) + } +} + +// WithPersistentStringPFlag is a helper function that returns a CommandOptionFn that applies a persistent string flag to a cobra.Command. +func WithPersistentStringPFlag(name, short, defaultValue, help string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.PersistentFlags().StringP(name, short, defaultValue, help) + } +} + +// WithBoolFlag is a helper function that returns a CommandOptionFn that applies a boolean flag to a cobra.Command. +func WithBoolFlag(name string, defaultValue bool, help string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.Flags().Bool(name, defaultValue, help) + } +} + +// WithAliases is a helper function that returns a CommandOptionFn that applies aliases to a cobra.Command. +func WithAliases(aliases ...string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.Aliases = aliases + } +} + +// WithPersistentBoolPFlag is a helper function that returns a CommandOptionFn that applies a persistent boolean flag to a cobra.Command. +func WithPersistentBoolPFlag(name, short string, defaultValue bool, help string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.PersistentFlags().BoolP(name, short, defaultValue, help) + } +} + +// WithPersistentBoolFlag is a helper function that returns a CommandOptionFn that applies a persistent boolean flag to a cobra.Command. +func WithPersistentBoolFlag(name string, defaultValue bool, help string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.PersistentFlags().Bool(name, defaultValue, help) + } +} + +// WithIntFlag is a helper function that returns a CommandOptionFn that applies an integer flag to a cobra.Command. +func WithIntFlag(name string, defaultValue int, help string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.Flags().Int(name, defaultValue, help) + } +} + +// WithStringSliceFlag is a helper function that returns a CommandOptionFn that applies a string slice flag to a cobra.Command. +func WithStringSliceFlag(name string, defaultValue []string, help string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.Flags().StringSlice(name, defaultValue, help) + } +} + +// WithStringArrayFlag is a helper function that returns a CommandOptionFn that applies a string array flag to a cobra.Command. +func WithStringArrayFlag(name string, defaultValue []string, help string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.Flags().StringArray(name, defaultValue, help) + } +} + +// WithHiddenFlag is a helper function that returns a CommandOptionFn that applies a hidden flag to a cobra.Command. +func WithHiddenFlag(name string) CommandOptionFn { + return func(cmd *cobra.Command) { + _ = cmd.Flags().MarkHidden(name) + } +} + +// WithHidden is a helper function that returns a CommandOptionFn that applies a hidden flag to a cobra.Command. +func WithHidden() CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.Hidden = true + } +} + +// WithRunE is a helper function that returns a CommandOptionFn that applies a run function to a cobra.Command. +func WithRunE(fn func(cmd *cobra.Command, args []string) error) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.RunE = fn + } +} + +// WithPersistentPreRunE is a helper function that returns a CommandOptionFn that applies a persistent pre-run function to a cobra.Command. +func WithPersistentPreRunE(fn func(cmd *cobra.Command, args []string) error) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.PersistentPreRunE = fn + } +} + +// WithPreRunE is a helper function that returns a CommandOptionFn that applies a pre-run function to a cobra.Command. +func WithPreRunE(fn func(cmd *cobra.Command, args []string) error) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.PreRunE = fn + } +} + +// WithDeprecatedFlag is a helper function that returns a CommandOptionFn that applies a deprecated flag to a cobra.Command. +func WithDeprecatedFlag(name, message string) CommandOptionFn { + return func(cmd *cobra.Command) { + _ = cmd.Flags().MarkDeprecated(name, message) + } +} + +// WithDeprecated is a helper function that returns a CommandOptionFn that applies a deprecated flag to a cobra.Command. +func WithDeprecated(message string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.Deprecated = message + } +} + +// WithChildCommands is a helper function that returns a CommandOptionFn that applies child commands to a cobra.Command. +func WithChildCommands(cmds ...*cobra.Command) CommandOptionFn { + return func(cmd *cobra.Command) { + for _, child := range cmds { + cmd.AddCommand(child) + } + } +} + +// WithShortDescription is a helper function that returns a CommandOptionFn that applies a short description to a cobra.Command. +func WithShortDescription(v string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.Short = v + } +} + +// WithArgs is a helper function that returns a CommandOptionFn that applies positional arguments to a cobra.Command. +func WithArgs(p cobra.PositionalArgs) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.Args = p + } +} + +// WithValidArgs is a helper function that returns a CommandOptionFn that applies valid arguments to a cobra.Command. +func WithValidArgs(validArgs ...string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.ValidArgs = validArgs + } +} + +// WithValidArgsFunction returns a CommandOptionFn that sets a custom validation function for command arguments. +func WithValidArgsFunction(fn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.ValidArgsFunction = fn + } +} + +// WithDescription is a helper function that returns a CommandOptionFn that applies a description to a cobra.Command. +func WithDescription(v string) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.Long = v + } +} + +// WithSilenceUsage is a helper function that returns a CommandOptionFn that applies silence usage to a cobra.Command. +func WithSilenceUsage() CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.SilenceUsage = true + } +} + +// WithSilenceError is a helper function that returns a CommandOptionFn that applies silence error to a cobra.Command. +func WithSilenceError() CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.SilenceErrors = true + } +} + +// NewStackCommand is a helper function that returns a new stack command with the given options. +func NewStackCommand(use string, opts ...CommandOption) *cobra.Command { + cmd := NewMembershipCommand(use, + append(opts, + WithPersistentStringFlag(stackFlag, "", "Specific stack (not required if only one stack is present)"), + )..., + ) + + _ = cmd.RegisterFlagCompletionFunc("stack", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cfg, err := GetConfig(cmd) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + profile := GetCurrentProfile(cmd, cfg) + + claims, err := profile.getUserInfo() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + selectedOrganization := GetSelectedOrganization(cmd) + if selectedOrganization == "" { + selectedOrganization = profile.defaultOrganization + } + + ret := make([]string, 0) + + for _, org := range claims.Org { + if selectedOrganization != "" && selectedOrganization != org.ID { + continue + } + + for _, stack := range org.Stacks { + ret = append(ret, fmt.Sprintf("%s\t%s", stack.ID, stack.DisplayName)) + } + } + + return ret, cobra.ShellCompDirectiveDefault + }) + + return cmd +} + +// WithController wraps a controller's Run method as a cobra command's RunE function. +func WithController[T any](c Controller[T]) CommandOptionFn { + return func(cmd *cobra.Command) { + cmd.RunE = func(cmd *cobra.Command, args []string) error { + renderable, err := c.Run(cmd, args) + if err != nil { + return err + } + + err = WithRender(cmd, args, c, renderable) + if err != nil { + return err + } + + return nil + } + } +} + +// WithRender handles the rendering of the output based on the output flag. +func WithRender[T any](cmd *cobra.Command, args []string, controller Controller[T], renderable Renderable) error { + output := GetString(cmd, OutputFlag) + + switch output { + case "json": + export := ExportedData{ + Data: controller.GetStore(), + } + + out, err := json.Marshal(export) + if err != nil { + return err + } + + if err := json.NewEncoder(cmd.OutOrStdout()).Encode(out); err != nil { + return err + } + default: + return renderable.Render(cmd, args) + } + + return nil +} + +// NewMembershipCommand is a helper function that returns a new membership command with the given options. +func NewMembershipCommand(use string, opts ...CommandOption) *cobra.Command { + cmd := NewCommand(use, + append(opts, + WithPersistentStringFlag(organizationFlag, "", "Selected organization (not required if only one organization is present)"), + )..., + ) + + _ = cmd.RegisterFlagCompletionFunc("organization", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cfg, err := GetConfig(cmd) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + profile := GetCurrentProfile(cmd, cfg) + + claims, err := profile.getUserInfo() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + ret := make([]string, 0) + for _, org := range claims.Org { + ret = append(ret, fmt.Sprintf("%s\t%s", org.ID, org.DisplayName)) + } + + return ret, cobra.ShellCompDirectiveDefault + }) + + return cmd +} + +// NewCommand creates a new cobra command with the specified use string and options. +// +// Parameters: +// - use: a string representing the use of the command. +// - opts: variadic CommandOption options. +// Return type: +// - *cobra.Command: a pointer to the created cobra command. +func NewCommand(use string, opts ...CommandOption) *cobra.Command { + cmd := &cobra.Command{ + Use: use, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if GetBool(cmd, TelemetryFlag) { + cfg, err := GetConfig(cmd) + if err != nil { + return + } + + if cfg.GetUniqueID() == "" { + uniqueID := ksuid.New().String() + cfg.SetUniqueID(uniqueID) + err = cfg.Persist() + if err != nil { + return + } + } + } + }, + } + for _, opt := range opts { + opt.apply(cmd) + } + + return cmd +} diff --git a/components/mdz/pkg/config.go b/components/mdz/pkg/config.go new file mode 100644 index 00000000..ca9261a6 --- /dev/null +++ b/components/mdz/pkg/config.go @@ -0,0 +1,176 @@ +package pkg + +import ( + "encoding/json" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +type persistedConfig struct { + CurrentProfile string `json:"currentProfile"` + Profiles map[string]*Profile `json:"profiles"` + UniqueID string `json:"uniqueID"` +} + +// Config represents the configuration for the application. +// It encapsulates details such as the current profile in use, a unique identifier for the configuration, +// a map of profile names to Profile instances, and a reference to the ConfigManager responsible for managing this configuration. +type Config struct { + currentProfile string + uniqueID string + profiles map[string]*Profile + manager *ConfigManager +} + +// MarshalJSON implements the json.Marshaler interface for Config. +// It converts the Config instance into a JSON-encoded byte slice. +func (c *Config) MarshalJSON() ([]byte, error) { + return json.Marshal(persistedConfig{ + CurrentProfile: c.currentProfile, + Profiles: c.profiles, + UniqueID: c.uniqueID, + }) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Config. +// It populates the Config instance with data from a JSON-encoded byte slice. +func (c *Config) UnmarshalJSON(data []byte) error { + cfg := &persistedConfig{} + if err := json.Unmarshal(data, cfg); err != nil { + return err + } + + *c = Config{ + currentProfile: cfg.CurrentProfile, + profiles: cfg.Profiles, + uniqueID: cfg.UniqueID, + } + + return nil +} + +// GetProfile retrieves a profile by name from the configuration. +// If the profile does not exist, it returns nil. +func (c *Config) GetProfile(name string) *Profile { + p := c.profiles[name] + if p != nil { + p.config = c + } + + return p +} + +// GetProfileOrDefault retrieves a profile by name from the configuration. +// If the profile does not exist, it creates a new profile with the given membership URI and returns it. +func (c *Config) GetProfileOrDefault(name, membershipURI string) *Profile { + p := c.GetProfile(name) + if p == nil { + if c.profiles == nil { + c.profiles = map[string]*Profile{} + } + + f := &Profile{ + membershipURI: membershipURI, + config: c, + } + + c.profiles[name] = f + + return f + } + + return p +} + +// DeleteProfile removes a profile from the configuration by name. +// If the profile does not exist, it returns an error. +func (c *Config) DeleteProfile(s string) error { + _, ok := c.profiles[s] + if !ok { + return errors.New("not found") + } + + delete(c.profiles, s) + + return nil +} + +// Persist saves the configuration to the file system. +// It uses the ConfigManager to update the configuration file with the current configuration. +func (c *Config) Persist() error { + return c.manager.UpdateConfig(c) +} + +// SetCurrentProfile sets the current profile to the given name and profile. +// It also updates the current profile name to the given name. +func (c *Config) SetCurrentProfile(name string, profile *Profile) { + c.profiles[name] = profile + c.currentProfile = name +} + +// SetUniqueID sets the unique identifier for the configuration. +func (c *Config) SetUniqueID(id string) { + c.uniqueID = id +} + +// SetProfile sets the profile with the given name to the given profile. +func (c *Config) SetProfile(name string, profile *Profile) { + c.profiles[name] = profile +} + +// GetUniqueID retrieves the unique identifier for the configuration. +func (c *Config) GetUniqueID() string { + return c.uniqueID +} + +// GetProfiles retrieves the map of profile names to Profile instances from the configuration. +func (c *Config) GetProfiles() map[string]*Profile { + return c.profiles +} + +// GetCurrentProfileName retrieves the name of the current profile from the configuration. +func (c *Config) GetCurrentProfileName() string { + return c.currentProfile +} + +// SetCurrentProfileName sets the name of the current profile to the given string. +func (c *Config) SetCurrentProfileName(s string) { + c.currentProfile = s +} + +// GetConfig retrieves the configuration from the file system. +// It uses the ConfigManager to load the configuration from the file system. +func GetConfig(cmd *cobra.Command) (*Config, error) { + return GetConfigManager(cmd).Load() +} + +// GetConfigManager retrieves the ConfigManager instance associated with the given cobra.Command. +// It uses the FileFlag to determine the configuration file path and returns a new ConfigManager instance. +func GetConfigManager(cmd *cobra.Command) *ConfigManager { + return NewConfigManager(GetString(cmd, FileFlag)) +} + +// GetCurrentProfileName retrieves the name of the current profile from the given cobra.Command and configuration. +// If the ProfileFlag is set, it returns the value of the ProfileFlag. +// Otherwise, it returns the current profile name from the configuration. +func GetCurrentProfileName(cmd *cobra.Command, config *Config) string { + if profile := GetString(cmd, ProfileFlag); profile != "" { + return profile + } + + currentProfileName := config.GetCurrentProfileName() + + if currentProfileName == "" { + currentProfileName = "default" + } + + return currentProfileName +} + +// GetCurrentProfile retrieves the current profile from the given cobra.Command and configuration. +// It returns the current profile from the configuration. +// If the current profile does not exist, it creates a new profile with the default membership URI and returns it. +func GetCurrentProfile(cmd *cobra.Command, cfg *Config) *Profile { + return cfg.GetProfileOrDefault(GetCurrentProfileName(cmd, cfg), defaultMembershipURI) +} diff --git a/components/mdz/pkg/controller.go b/components/mdz/pkg/controller.go new file mode 100644 index 00000000..ba342598 --- /dev/null +++ b/components/mdz/pkg/controller.go @@ -0,0 +1,30 @@ +package pkg + +import ( + "github.com/spf13/cobra" +) + +// Renderable defines an interface for objects that can render themselves. +// It requires implementing the Render method, which takes a cobra.Command and +// a slice of strings (arguments) and returns an error. +type Renderable interface { + Render(cmd *cobra.Command, args []string) error +} + +// Controller is a generic interface that defines the structure of a controller +// capable of handling commands. It requires two methods: +// - GetStore, which returns a store of type T. +// - Run, which takes a cobra.Command and a slice of strings (arguments), +// and returns a Renderable and an error. This method is responsible for +// executing the command's logic. +type Controller[T any] interface { + GetStore() T + Run(cmd *cobra.Command, args []string) (Renderable, error) +} + +// ExportedData represents a generic structure for data that can be exported. +// It contains a single field, Data, which can hold any type of value. The field +// is tagged with `json:"data"` to specify its JSON key when serialized. +type ExportedData struct { + Data any `json:"data"` +} diff --git a/components/mdz/pkg/flags.go b/components/mdz/pkg/flags.go new file mode 100644 index 00000000..77308d9c --- /dev/null +++ b/components/mdz/pkg/flags.go @@ -0,0 +1,106 @@ +package pkg + +import ( + "os" + "strconv" + "strings" + "time" + + "github.com/iancoleman/strcase" + "github.com/spf13/cobra" +) + +const ( + // MembershipURIFlag specifies the URI for membership + MembershipURIFlag = "membership-uri" + // FileFlag specifies the configuration file + FileFlag = "config" + // ProfileFlag specifies the profile to use + ProfileFlag = "profile" + // OutputFlag specifies the output format + OutputFlag = "output" + // DebugFlag specifies whether to run the command in debug mode + DebugFlag = "debug" + // TelemetryFlag specifies whether to enable telemetry + TelemetryFlag = "telemetry" +) + +// GetBool retrieves the boolean value of the specified flag from the command. +func GetBool(cmd *cobra.Command, flagName string) bool { + v, err := cmd.Flags().GetBool(flagName) + if err != nil { + fromEnv := strings.ToLower(os.Getenv(strcase.ToScreamingSnake(flagName))) + return fromEnv == "true" || fromEnv == "1" + } + + return v +} + +// GetString retrieves the string value of the specified flag from the command. +func GetString(cmd *cobra.Command, flagName string) string { + v, err := cmd.Flags().GetString(flagName) + if err != nil || v == "" { + return os.Getenv(strcase.ToScreamingSnake(flagName)) + } + + return v +} + +// GetStringSlice retrieves the string slice value of the specified flag from the command. +func GetStringSlice(cmd *cobra.Command, flagName string) []string { + v, err := cmd.Flags().GetStringSlice(flagName) + if err != nil || len(v) == 0 { + envVar := os.Getenv(strcase.ToScreamingSnake(flagName)) + if envVar == "" { + return []string{} + } + + return strings.Split(envVar, " ") + } + + return v +} + +// GetInt retrieves the integer value of the specified flag from the command. +func GetInt(cmd *cobra.Command, flagName string) int { + v, err := cmd.Flags().GetInt(flagName) + if err != nil { + v := os.Getenv(strcase.ToScreamingSnake(flagName)) + if v != "" { + v, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return 0 + } + + return int(v) + } + + return 0 + } + + return v +} + +// GetDateTime retrieves the time value of the specified flag from the command. +func GetDateTime(cmd *cobra.Command, flagName string) (*time.Time, error) { + v, err := cmd.Flags().GetString(flagName) + if err != nil || v == "" { + v = os.Getenv(strcase.ToScreamingSnake(flagName)) + } + + if v == "" { + return nil, nil + } + + t, err := time.Parse(time.RFC3339, v) + if err != nil { + return nil, err + } + + return &t, nil +} + +// Ptr returns a pointer to the given value. +func Ptr[T any](t T) *T { + return &t +} diff --git a/components/mdz/pkg/http.go b/components/mdz/pkg/http.go new file mode 100644 index 00000000..a5fd52a6 --- /dev/null +++ b/components/mdz/pkg/http.go @@ -0,0 +1,163 @@ +package pkg + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httputil" + + "github.com/TylerBrock/colorjson" + "github.com/spf13/cobra" +) + +// GetHTTPClient creates and returns a new HTTP client configured based on the provided +// cobra.Command flags and default headers. It utilizes the NewHTTPClient function, +// passing in the values of the InsecureTlsFlag and DebugFlag, along with any +// defaultHeaders specified. This allows for the creation of a customized http.Client +// instance tailored to the needs of the application, including support for insecure +// TLS connections and debugging capabilities. +func GetHTTPClient(cmd *cobra.Command, defaultHeaders map[string][]string) *http.Client { + return NewHTTPClient( + GetBool(cmd, DebugFlag), // Enables or disables debugging output. + defaultHeaders, // Sets default headers for all requests made by the client. + ) +} + +// RoundTripperFn is a function type that implements the http.RoundTripper interface. +// It allows any function with the appropriate signature to be used as an http.RoundTripper. +// This is useful for creating custom transport behaviors in an http.Client. +type RoundTripperFn func(req *http.Request) (*http.Response, error) + +// RoundTrip executes the RoundTripperFn function, effectively making RoundTripperFn +// an http.RoundTripper. This method allows RoundTripperFn to satisfy the http.RoundTripper +// interface, enabling its use as a custom transport mechanism within an http.Client. +func (fn RoundTripperFn) RoundTrip(req *http.Request) (*http.Response, error) { + return fn(req) +} + +func printBody(data []byte) { + if len(data) == 0 { + return + } + + raw := make(map[string]any) + + if err := json.Unmarshal(data, &raw); err == nil { + f := colorjson.NewFormatter() + f.Indent = 2 + + colorized, err := f.Marshal(raw) + if err != nil { + panic(err) + } + + fmt.Println(string(colorized)) + } else { + fmt.Println(string(data)) + } +} + +func debugRoundTripper(rt http.RoundTripper) RoundTripperFn { + return func(req *http.Request) (*http.Response, error) { + data, err := httputil.DumpRequest(req, false) + if err != nil { + panic(err) + } + + fmt.Println(string(data)) + + if req.Body != nil { + data, err = io.ReadAll(req.Body) + if err != nil { + panic(err) + } + + if err := req.Body.Close(); err != nil { + panic(err) + } + + req.Body = io.NopCloser(bytes.NewBuffer(data)) + + printBody(data) + } + + rsp, err := rt.RoundTrip(req) + if err != nil { + return nil, err + } + + data, err = httputil.DumpResponse(rsp, false) + if err != nil { + panic(err) + } + + fmt.Println(string(data)) + + if rsp.Body != nil { + data, err = io.ReadAll(rsp.Body) + if err != nil { + panic(err) + } + + if err := rsp.Body.Close(); err != nil { + panic(err) + } + + rsp.Body = io.NopCloser(bytes.NewBuffer(data)) + printBody(data) + } + + return rsp, nil + } +} + +func defaultHeadersRoundTripper(rt http.RoundTripper, headers map[string][]string) RoundTripperFn { + return func(req *http.Request) (*http.Response, error) { + for k, v := range headers { + for _, vv := range v { + req.Header.Add(k, vv) + } + } + + return rt.RoundTrip(req) + } +} + +// NewHTTPClient initializes and returns a new http.Client with customizable behavior. +// It allows for the configuration of TLS insecurity (skipping TLS verification), +// enabling a debug mode for additional logging, and setting default headers for all requests. +// +// Parameters: +// - insecureTLS: If true, the client will accept any TLS certificate presented by the server +// and any host name in that certificate. This is useful for testing with self-signed certificates. +// - debug: If true, wraps the transport in a debugging layer that logs all requests and responses. +// This is helpful for development and troubleshooting. +// - defaultHeaders: A map of header names to their values which will be added to every request +// made by this client. Useful for setting headers like `Authorization` or `User-Agent` that +// should be included in all requests. +// +// Returns: +// - A pointer to an initialized http.Client configured as specified. +func NewHTTPClient(debug bool, defaultHeaders map[string][]string) *http.Client { + var transport http.RoundTripper = &http.Transport{ + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: false, + }, + } + + if debug { + transport = debugRoundTripper(transport) + } + + if len(defaultHeaders) > 0 { + transport = defaultHeadersRoundTripper(transport, defaultHeaders) + } + + return &http.Client{ + Transport: transport, + } +} diff --git a/components/mdz/pkg/manager.go b/components/mdz/pkg/manager.go new file mode 100644 index 00000000..9f6b2f6a --- /dev/null +++ b/components/mdz/pkg/manager.go @@ -0,0 +1,73 @@ +package pkg + +import ( + "encoding/json" + "os" + "path" +) + +const ( + defaultMembershipURI = "https://app.midaz.cloud/api" +) + +// ConfigManager is a struct that use a string configFilePath +type ConfigManager struct { + configFilePath string +} + +// Load is a func that load a filepath and return a *Config of application +func (m *ConfigManager) Load() (*Config, error) { + f, err := os.Open(m.configFilePath) + if err != nil { + if os.IsNotExist(err) { + return &Config{ + profiles: map[string]*Profile{}, + manager: m, + }, nil + } + + return nil, err + } + defer f.Close() + + cfg := &Config{} + if err := json.NewDecoder(f).Decode(cfg); err != nil { + return nil, err + } + + cfg.manager = m + if cfg.profiles == nil { + cfg.profiles = map[string]*Profile{} + } + + return cfg, nil +} + +// UpdateConfig is a func that update a *Config of application +func (m *ConfigManager) UpdateConfig(config *Config) error { + if err := os.MkdirAll(path.Dir(m.configFilePath), 0o700); err != nil { + return err + } + + f, err := os.OpenFile(m.configFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + return err + } + defer f.Close() + + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + + if err := enc.Encode(config); err != nil { + return err + } + + return nil +} + +// NewConfigManager is a func that return a struc of *ConfigManager +func NewConfigManager(configFilePath string) *ConfigManager { + return &ConfigManager{ + configFilePath: configFilePath, + } +} diff --git a/components/mdz/pkg/profile.go b/components/mdz/pkg/profile.go new file mode 100644 index 00000000..e6dd32ee --- /dev/null +++ b/components/mdz/pkg/profile.go @@ -0,0 +1,261 @@ +package pkg + +import ( + "context" + "encoding/json" + "net/http" + "sort" + "strings" + "time" + + "github.com/golang-jwt/jwt" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/zitadel/oidc/v2/pkg/client/rp" + "github.com/zitadel/oidc/v2/pkg/oidc" + "golang.org/x/oauth2" +) + +// ErrInvalidAuthentication is an error that occurs when the authentication token is invalid. +type ErrInvalidAuthentication struct { + err error +} + +// Error returns the error message for ErrInvalidAuthentication. +// +// No parameters. +// Returns a string. +func (e ErrInvalidAuthentication) Error() string { + return e.err.Error() +} + +// Unwrap returns the underlying error of ErrInvalidAuthentication. +// +// No parameters. +// Returns an error. +func (e ErrInvalidAuthentication) Unwrap() error { + return e.err +} + +// Is checks if the provided error is of type ErrInvalidAuthentication. +func (e ErrInvalidAuthentication) Is(err error) bool { + _, ok := err.(*ErrInvalidAuthentication) + return ok +} + +// IsInvalidAuthentication checks if the provided error is an instance of ErrInvalidAuthentication. +func IsInvalidAuthentication(err error) bool { + return errors.Is(err, &ErrInvalidAuthentication{}) +} + +func newErrInvalidAuthentication(err error) *ErrInvalidAuthentication { + return &ErrInvalidAuthentication{ + err: err, + } +} + +// AuthClient is the name of the OIDC client. +const AuthClient = "mdz" + +type persistedProfile struct { + MembershipURI string `json:"membershipURI"` + Token *oidc.AccessTokenResponse `json:"token"` + DefaultOrganization string `json:"defaultOrganization"` +} + +// Profile represents a user profile. +type Profile struct { + membershipURI string + token *oidc.AccessTokenResponse + defaultOrganization string + config *Config +} + +// UpdateToken updates the token for the Profile. +// +// token *oidc.AccessTokenResponse - The new access token to be set. +func (p *Profile) UpdateToken(token *oidc.AccessTokenResponse) { + p.token = token +} + +// SetMembershipURI sets the membership URI for the Profile. +// +// Takes a string parameter. +func (p *Profile) SetMembershipURI(v string) { + p.membershipURI = v +} + +// MarshalJSON generates the JSON encoding for the Profile struct. +// +// No parameters. +// Returns a byte slice and an error. +func (p *Profile) MarshalJSON() ([]byte, error) { + return json.Marshal(persistedProfile{ + MembershipURI: p.membershipURI, + Token: p.token, + DefaultOrganization: p.defaultOrganization, + }) +} + +// UnmarshalJSON parses the JSON-encoded data and stores the result in the Profile struct. +// +// It takes a byte slice data as a parameter. +// Returns an error. +func (p *Profile) UnmarshalJSON(data []byte) error { + cfg := &persistedProfile{} + if err := json.Unmarshal(data, cfg); err != nil { + return err + } + + *p = Profile{ + membershipURI: cfg.MembershipURI, + token: cfg.Token, + defaultOrganization: cfg.DefaultOrganization, + } + + return nil +} + +// GetMembershipURI returns the membership URI of the Profile. +// +// No parameters. +// Returns a string. +func (p *Profile) GetMembershipURI() string { + return p.membershipURI +} + +// GetDefaultOrganization returns the default organization for the Profile. +// +// No parameters. +// Returns a string. +func (p *Profile) GetDefaultOrganization() string { + return p.defaultOrganization +} + +// GetToken retrieves and refreshes the OAuth2 token if needed. +// +// Parameters: +// - ctx: the context for the HTTP request +// - httpClient: the HTTP client to use for making HTTP requests +// +// Returns: +// - *oauth2.Token: the OAuth2 token retrieved or refreshed +// - error: an error if any occurred during the token retrieval or refresh +func (p *Profile) GetToken(ctx context.Context, httpClient *http.Client) (*oauth2.Token, error) { + if p.token == nil { + return nil, errors.New("not authenticated") + } + + if p.token != nil { + claims := &oidc.AccessTokenClaims{} + + if _, err := oidc.ParseToken(p.token.AccessToken, claims); err != nil { + return nil, newErrInvalidAuthentication(errors.Wrap(err, "parsing token")) + } + + if claims.Expiration.AsTime().Before(time.Now()) { + relyingParty, err := GetAuthRelyingParty(httpClient, p.membershipURI) + if err != nil { + return nil, err + } + + newToken, err := rp.RefreshAccessToken(relyingParty, p.token.RefreshToken, "", "") + if err != nil { + return nil, newErrInvalidAuthentication(errors.Wrap(err, "refreshing token")) + } + + p.UpdateToken(&oidc.AccessTokenResponse{ + AccessToken: newToken.AccessToken, + TokenType: newToken.TokenType, + RefreshToken: newToken.RefreshToken, + IDToken: newToken.Extra("id_token").(string), + }) + + if err := p.config.Persist(); err != nil { + return nil, err + } + } + } + + claims := &oidc.AccessTokenClaims{} + if _, err := oidc.ParseToken(p.token.AccessToken, claims); err != nil { + return nil, newErrInvalidAuthentication(err) + } + + return &oauth2.Token{ + AccessToken: p.token.AccessToken, + TokenType: p.token.TokenType, + RefreshToken: p.token.RefreshToken, + Expiry: claims.Expiration.AsTime(), + }, nil +} + +// GetClaims returns the jwt claims and an error. +// +// No parameters. +// Returns jwt.MapClaims and error. +func (p *Profile) GetClaims() (jwt.MapClaims, error) { + claims := jwt.MapClaims{} + parser := jwt.Parser{} + + if _, _, err := parser.ParseUnverified(p.token.AccessToken, claims); err != nil { + return nil, err + } + + return claims, nil +} + +func (p *Profile) getUserInfo() (*userClaims, error) { + claims := &userClaims{} + if p.token != nil && p.token.IDToken != "" { + _, err := oidc.ParseToken(p.token.IDToken, claims) + if err != nil { + return nil, err + } + } + + return claims, nil +} + +// SetDefaultOrganization sets the default organization +func (p *Profile) SetDefaultOrganization(o string) { + p.defaultOrganization = o +} + +// IsConnected is connected +func (p *Profile) IsConnected() bool { + return p.token != nil +} + +// CurrentProfile profile +type CurrentProfile Profile + +// ListProfiles generates a list of profiles based on the toComplete string. +// +// Parameters: +// +// cmd: *cobra.Command - the command object +// toComplete: string - the string to complete +// +// Return type: +// +// []string: list of profile strings +// error: any error that occurred +func ListProfiles(cmd *cobra.Command, toComplete string) ([]string, error) { + config, err := GetConfig(cmd) + if err != nil { + return []string{}, err + } + + ret := make([]string, 0) + + for p := range config.GetProfiles() { + if strings.HasPrefix(p, toComplete) { + ret = append(ret, p) + } + } + + sort.Strings(ret) + + return ret, nil +} diff --git a/components/mdz/pkg/relyingparty.go b/components/mdz/pkg/relyingparty.go new file mode 100644 index 00000000..7542e36f --- /dev/null +++ b/components/mdz/pkg/relyingparty.go @@ -0,0 +1,13 @@ +package pkg + +import ( + "net/http" + + "github.com/zitadel/oidc/v2/pkg/client/rp" +) + +// GetAuthRelyingParty returns a NewRelyingPartyOIDC that creates an (OIDC) +func GetAuthRelyingParty(httpClient *http.Client, membershipURI string) (rp.RelyingParty, error) { + return rp.NewRelyingPartyOIDC(membershipURI, AuthClient, "", + "", []string{"openid", "email", "offline_access", "supertoken", "accesses"}, rp.WithHTTPClient(httpClient)) +} diff --git a/components/mdz/pkg/utils.go b/components/mdz/pkg/utils.go new file mode 100644 index 00000000..184fdf7a --- /dev/null +++ b/components/mdz/pkg/utils.go @@ -0,0 +1,54 @@ +package pkg + +import ( + "errors" +) + +// Map func that appends slices +func Map[SRC, DST any](srcs []SRC, mapper func(SRC) DST) []DST { + ret := make([]DST, 0) + for _, src := range srcs { + ret = append(ret, mapper(src)) + } + + return ret +} + +// MapMap func that compare and appends slices +func MapMap[KEY comparable, VALUE, DST any](srcs map[KEY]VALUE, mapper func(KEY, VALUE) DST) []DST { + ret := make([]DST, 0) + for k, v := range srcs { + ret = append(ret, mapper(k, v)) + } + + return ret +} + +// MapKeys func that compare and appends slices +func MapKeys[K comparable, V any](m map[K]V) []K { + ret := make([]K, 0) + for k := range m { + ret = append(ret, k) + } + + return ret +} + +// Prepend func that return two append items +func Prepend[V any](array []V, items ...V) []V { + return append(items, array...) +} + +// ContainValue func that valid if contains value +func ContainValue[V comparable](array []V, value V) bool { + for _, v := range array { + if v == value { + return true + } + } + + return false +} + +// ErrOpenningBrowser is a struct that return an error when opening browser +var ErrOpenningBrowser = errors.New("opening browser") diff --git a/config/auth/hydra.yml b/config/auth/hydra.yml new file mode 100644 index 00000000..f7d17fb5 --- /dev/null +++ b/config/auth/hydra.yml @@ -0,0 +1,32 @@ +version: v2.2.0 + +serve: + public: + cors: + enabled: true + +oidc: + subject_identifiers: + supported_types: + - public + - pairwise + +urls: + login: http://localhost:4455/login + consent: http://localhost:4455/auth/consent + logout: http://localhost:4455/logout + error: http://localhost:4455/error + self: + public: http://localhost:4444/ + issuer: http://localhost:4444/ + +ttl: + access_token: 1h + refresh_token: 1h + id_token: 1h + auth_code: 1h + +oauth2: + expose_internal_errors: true + session: + encrypt_at_rest: true diff --git a/config/auth/kratos.yml b/config/auth/kratos.yml new file mode 100644 index 00000000..b639b31a --- /dev/null +++ b/config/auth/kratos.yml @@ -0,0 +1,84 @@ +version: v1.1.0 + +serve: + public: + base_url: http://localhost:4433/ + cors: + enabled: true + admin: + base_url: http://localhost:4434/ + +selfservice: + default_browser_return_url: http://localhost:4455/ + allowed_return_urls: + - http://localhost:4455 + - http://localhost:4444 + + methods: + password: + enabled: true + totp: + config: + issuer: Kratos + enabled: true + link: + enabled: true + config: + lifespan: 15m + + flows: + error: + ui_url: http://localhost:4455/error + + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 15m + + recovery: + enabled: true + ui_url: http://localhost:4455/recovery + after: + hooks: + - hook: revoke_active_sessions + + verification: + enabled: true + ui_url: http://localhost:4455/verification + + logout: + after: + default_browser_return_url: http://localhost:4455/logout + + login: + lifespan: 10m + ui_url: http://localhost:4455/login + after: + default_browser_return_url: http://localhost:4455/dashboard + password: + hooks: + - hook: require_verified_address + + registration: + lifespan: 10m + ui_url: http://localhost:4455/registration + after: + default_browser_return_url: http://localhost:4455/registered + +ciphers: + algorithm: xchacha20-poly1305 + +hashers: + argon2: + parallelism: 1 + memory: 128MB + iterations: 2 + salt_length: 16 + key_length: 16 + +identity: + default_schema_id: organization_user + schemas: + - id: organization_user + url: file:///etc/kratos/identity-schemas/organization_user.schema.json + - id: default_user + url: file:///etc/kratos/identity-schemas/default_user.schema.json diff --git a/config/identity-schemas/default_user.schema.json b/config/identity-schemas/default_user.schema.json new file mode 100644 index 00000000..352b6bd2 --- /dev/null +++ b/config/identity-schemas/default_user.schema.json @@ -0,0 +1,46 @@ +{ + "$id": "identity.default_user.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Default User", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "totp": { + "account_name": true + } + }, + "recovery": { + "via": "email" + }, + "verification": { + "via": "email" + } + } + }, + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "email", + "name" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/config/identity-schemas/organization_user.schema.json b/config/identity-schemas/organization_user.schema.json new file mode 100644 index 00000000..e3bcecfd --- /dev/null +++ b/config/identity-schemas/organization_user.schema.json @@ -0,0 +1,91 @@ +{ + "$id": "identity.organization_user.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Organization User", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "totp": { + "account_name": true + } + }, + "recovery": { + "via": "email" + }, + "verification": { + "via": "email" + } + } + }, + "name": { + "title": "Name", + "type": "string" + }, + "company": { + "type": "object", + "properties": { + "name": { + "title": "Company Name", + "type": "string" + }, + "trade_name": { + "title": "Trade Name", + "type": "string" + }, + "document": { + "title": "Business Document", + "type": "string" + }, + "address": { + "title": "Address", + "type": "string" + }, + "additional_address": { + "title": "Additional Address", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "state": { + "title": "State", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + } + }, + "required": [ + "name", + "document", + "address", + "country", + "state", + "city" + ] + } + }, + "required": [ + "email", + "name" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..13055939 --- /dev/null +++ b/go.mod @@ -0,0 +1,113 @@ +module github.com/LerianStudio/midaz + +go 1.21.3 + +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.4 + github.com/gofiber/swagger v1.0.0 + github.com/jackc/pgx/v5 v5.5.5 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.9.0 + go.mongodb.org/mongo-driver v1.15.0 + go.uber.org/mock v0.4.0 + golang.org/x/oauth2 v0.20.0 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect + github.com/swaggo/swag v1.16.3 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/tools v0.20.0 // indirect +) + +require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect + github.com/containerd/console v1.0.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/fatih/color v1.14.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/gorilla/schema v1.2.0 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/muhlemmer/gu v0.3.1 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/term v0.19.0 // indirect + golang.org/x/text v0.15.0 + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/bxcodec/dbresolver/v2 v2.1.0 + github.com/go-playground/validator v9.31.0+incompatible + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/golang-migrate/migrate/v4 v4.17.1 + github.com/google/uuid v1.6.0 + github.com/google/wire v0.6.0 + github.com/iancoleman/strcase v0.3.0 + github.com/klauspost/compress v1.17.8 // indirect + github.com/lestrrat-go/jwx v1.2.29 + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pterm/pterm v0.12.79 + github.com/rivo/uniseg v0.4.7 // indirect + github.com/segmentio/ksuid v1.0.4 + github.com/spf13/cobra v1.8.0 + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.52.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/zitadel/oidc/v2 v2.12.0 + go.uber.org/zap v1.27.0 + golang.org/x/sys v0.19.0 // indirect + gopkg.in/go-playground/validator.v9 v9.31.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..54013494 --- /dev/null +++ b/go.sum @@ -0,0 +1,384 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/bxcodec/dbresolver/v2 v2.1.0 h1:G67HT/wPuspLiUVAZ0gKx7eA+hJrsjIvjypwUZMdY7Y= +github.com/bxcodec/dbresolver/v2 v2.1.0/go.mod h1:s9HgSiWo28jBUGvwMMwX8630n/np3O8OBCj8GDEQZ80= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= +github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg= +github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= +github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM= +github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gofiber/swagger v1.0.0 h1:BzUzDS9ZT6fDUa692kxmfOjc1DZiloLiPK/W5z1H1tc= +github.com/gofiber/swagger v1.0.0/go.mod h1:QrYNF1Yrc7ggGK6ATsJ6yfH/8Zi5bu9lA7wB8TmCecg= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= +github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +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/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +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/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= +github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= +github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ= +github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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-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-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= +github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= +github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= +github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zitadel/oidc/v2 v2.12.0 h1:4aMTAy99/4pqNwrawEyJqhRb3yY3PtcDxnoDSryhpn4= +github.com/zitadel/oidc/v2 v2.12.0/go.mod h1:LrRav74IiThHGapQgCHZOUNtnqJG0tcZKHro/91rtLw= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/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.1.0/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= +gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/image/README/docker-ps.png b/image/README/docker-ps.png new file mode 100644 index 0000000000000000000000000000000000000000..ac52232048b4e41b756b698958b495c71c026f6b GIT binary patch literal 177968 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 literal 0 HcmV?d00001 diff --git a/image/README/midaz-banner.png b/image/README/midaz-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..676fff894e69c52a427865a98e0b9cd155614423 GIT binary patch literal 13968 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 E0jxsauK)l5 literal 0 HcmV?d00001 diff --git a/make.sh b/make.sh new file mode 100755 index 00000000..cde42ee7 --- /dev/null +++ b/make.sh @@ -0,0 +1,163 @@ +#!/bin/bash + +LOGO=$(cat "$PWD"/common/shell/logo.txt) +GITHOOKS_PATH="$PWD"/.githooks +GIT_HOOKS_PATH="$PWD"/.git/hooks + +source "$PWD"/common/shell/colors.sh +source "$PWD"/common/shell/ascii.sh + +echo "${bold}${blue}$LOGO${normal}" + +makeCmd() { + cmd=$1 + for DIR in "$PWD"/*; do + FILE="$DIR"/Makefile + if [ -f "$FILE" ]; then + if grep -q "$cmd:" "$FILE"; then + ( + cd "$DIR" || exit + echo "" + border "########### Executing ${magenta}make $1${normal} command in package ${bold}${blue}$DIR${normal} ###########" + make $cmd + ) + err=$? + if [ $err -ne 0 ]; then + echo -e "\n${bold}${red}An error has occurred during test process ${bold}[FAIL]${norma}\n" + exit 1 + fi + fi + fi + done +} + +setupGitHooks() { + title1 "Setting up git hooks..." + + find $GITHOOKS_PATH -type f -exec cp {} $GIT_HOOKS_PATH \; + chmod +x .git/hooks/* + + lineOk "All hooks installed and updated" +} + +checkHooks() { + err=0 + echo "Checking git hooks..." + for FILE in "$GITHOOKS_PATH"/*; do + f="$(basename -- $FILE)" + FILE2="$GIT_HOOKS_PATH"/$f + if [ -f "$FILE2" ]; then + if cmp -s "$FILE" "$FILE2"; then + lineOk "Hook file ${underline}$f${normal} installed and updated" + else + lineError "Hook file ${underline}$f${normal} ${red}installed but out-of-date [OUT-OF-DATE]" + err=1 + fi + else + lineError "Hook file ${underline}$f${normal} ${red}not installed [NOT INSTALLED]" + err=1 + fi + if [ $err -ne 0 ]; then + echo -e "\nRun ${bold}make setup-git-hooks${normal} to setup your development environment, then try again.\n" + exit 1 + fi + done + + lineOk "\nAll good" +} + +lint() { + title1 "STARTING LINT" + out=$(golangci-lint run --fix ./... 2>&1) + out_err=$? + perf_out=$(perfsprint ./... 2>&1) + perf_err=$? + + echo "$out" + echo "$perf_out" + + if [ $out_err -ne 0 ]; then + echo -e "\n${bold}${red}An error has occurred during the lint process: \n $out\n" + exit 1 + fi + if [ $perf_err -ne 0 ]; then + echo -e "\n${bold}${red}An error has occurred during the performance check: \n $perf_out\n" + exit 1 + fi + + lineOk "Lint and performance checks passed successfully" +} + +format() { + title1 "Formatting all golang source code" + gofmt -w ./ + lineOk "All go files formatted" +} + +checkLogs() { + title1 "STARTING LOGS ANALYZER" + err=0 + while IFS= read -r path; do + if grep -q 'err != nil' "$path" && ! grep -qE '(logger\.Error|log\.Error)' "$path" && [[ "$path" != *"_test"* ]]; then + err=1 + echo "$path" + fi + done < <(find . -type f -path '*usecase*/*' -name '*.go') + + if [ $err -eq 1 ]; then + echo -e "\n${red}You need to log all errors inside usecases after they are handled. ${bold}[WARNING]${normal}\n" + exit 1 + else + lineOk "All good" + fi +} + +checkTests() { + title1 "STARTING TESTS ANALYZER" + err=false + subdirs="components/*/internal/app/query components/*/internal/app/command" + + for subdir in $subdirs; do + while IFS= read -r file; do + if [[ "$file" != *"_test.go" ]]; then + test_file="${file%.go}_test.go" + if [ ! -f "$test_file" ]; then + echo "Error: There is no test for the file $file" + err=true + fi + fi + done < <(find "$subdir" -type f -name "*.go") + done + + if [ "$err" = true ]; then + echo -e "\n${red}There are files without corresponding test files.${normal}\n" + exit 1 + else + lineOk "All tests are in place" + fi +} + +case "$1" in +"setupGitHooks") + setupGitHooks + ;; +"checkHooks") + checkHooks + ;; +"lint") + lint + ;; +"format") + format + ;; +"checkLogs") + checkLogs + ;; +"checkTests") + checkTests + ;; +*) + echo -e "\n\n\nExecuting with parameter $1" + makeCmd "$1" + ;; +esac diff --git a/postman/MIDAZ DEV.postman_environment.json b/postman/MIDAZ DEV.postman_environment.json new file mode 100644 index 00000000..7c6723b9 --- /dev/null +++ b/postman/MIDAZ DEV.postman_environment.json @@ -0,0 +1,51 @@ +{ + "id": "4226f211-ca04-48d4-a479-2326811f4ab0", + "name": "MIDAZ DEV", + "values": [ + { + "key": "url", + "value": "http://127.0.0.1:3000", + "type": "default", + "enabled": true + }, + { + "key": "organization_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "ledger_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "product_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "portfolio_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "instrument_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "account_id", + "value": "", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2024-05-09T13:40:41.474Z", + "_postman_exported_using": "Postman/11.0.6" +} \ No newline at end of file diff --git a/postman/MIDAZ.postman_collection.json b/postman/MIDAZ.postman_collection.json new file mode 100644 index 00000000..d992f7f8 --- /dev/null +++ b/postman/MIDAZ.postman_collection.json @@ -0,0 +1,1200 @@ +{ + "info": { + "_postman_id": "4af684d1-4e4d-4e08-89c8-97988f119d9d", + "name": "MIDAZ", + "description": "# ๐Ÿš€ Get started here\n\nThis template guides you through CRUD operations (GET, POST, PUT, DELETE), variables, and tests.\n\n## ๐Ÿ”– **How to use this template**\n\n#### **Step 1: Send requests**\n\nRESTful APIs allow you to perform CRUD operations using the POST, GET, PUT, and DELETE HTTP methods.\n\nThis collection contains each of these [request](https://learning.postman.com/docs/sending-requests/requests/) types. Open each request and click \"Send\" to see what happens.\n\n#### **Step 2: View responses**\n\nObserve the response tab for status code (200 OK), response time, and size.\n\n#### **Step 3: Send new Body data**\n\nUpdate or add new data in \"Body\" in the POST request. Typically, Body data is also used in PUT request.\n\n```\n{\n \"name\": \"Add your name in the body\"\n}\n\n ```\n\n#### **Step 4: Update the variable**\n\nVariables enable you to store and reuse values in Postman. We have created a [variable](https://learning.postman.com/docs/sending-requests/variables/) called `base_url` with the sample request [https://postman-api-learner.glitch.me](https://postman-api-learner.glitch.me). Replace it with your API endpoint to customize this collection.\n\n#### **Step 5: Add tests in the \"Tests\" tab**\n\nTests help you confirm that your API is working as expected. You can write test scripts in JavaScript and view the output in the \"Test Results\" tab.\n\n<img src=\"https://content.pstmn.io/b5f280a7-4b09-48ec-857f-0a7ed99d7ef8/U2NyZWVuc2hvdCAyMDIzLTAzLTI3IGF0IDkuNDcuMjggUE0ucG5n\">\n\n## ๐Ÿ’ช Pro tips\n\n- Use folders to group related requests and organize the collection.\n- Add more [scripts](https://learning.postman.com/docs/writing-scripts/intro-to-scripts/) in \"Tests\" to verify if the API works as expected and execute workflows.\n \n\n## ๐Ÿ’กRelated templates\n\n[API testing basics](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9a37a28-055b-49cd-8c7e-97494a21eb54&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[API documentation](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9c28f47-1253-44af-a2f3-20dce4da1f18&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[Authorization methods](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=31a9a6ed-4cdf-4ced-984c-d12c9aec1c27&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "32592108" + }, + "item": [ + { + "name": "Organizations", + "item": [ + { + "name": "Organizations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "if (jsonData.hasOwnProperty('id')) {", + " console.log(\"organization_id antes: \" + postman.getEnvironmentVariable(\"organization_id\"));", + " postman.setEnvironmentVariable(\"organization_id\", jsonData.id);", + " console.log(\"organization_id depois: \" + postman.getEnvironmentVariable(\"organization_id\"));", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"legalName\": \"Midaz Tech LTDA\",\n //\"parentOrganizationId\": \"{{organization_id}}\",\n \"doingBusinessAs\": \"The ledger.io\", //opcional\n \"legalDocument\": \"48784548000104\",\n \"status\": {\n \"code\": \"ACTIVE\",\n \"description\": \"Teste Ledger\"\n },\n \"address\": {\n \"line1\": \"Avenida Paulista, 1234\",\n \"line2\": \"CJ 203\",\n \"neighborhood\": \"Jardim Paulista\",\n \"zipCode\": \"04696040\",\n \"city\": \"Sรฃo Paulo\",\n \"state\": \"SP\",\n \"country\": \"BR\" //de acordo com a ISO 3166-1 alpha2\n },\n \"metadata\": {\n \"chave\": \"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlf\",\n \"boolean\": true,\n \"double\": 10.5,\n \"int\": 1\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations" + ] + } + }, + "response": [] + }, + { + "name": "Organizations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"legalName\": \"Midaz Tech LTDA\",\n \"doingBusinessAs\": \"The ledger.io\", //opcional\n \"status\": {\n \"code\": \"BLOCKED\",\n \"description\": \"BLOCKD BY McGregor\"\n },\n \"address\": {\n \"line1\": \"Avenida Paulista, 1234\",\n \"line2\": \"CJ 203\",\n \"neighborhood\": \"Jardim Paulista\",\n \"zipCode\": \"04696040\",\n \"city\": \"Sรฃo Paulo\",\n \"state\": \"SP\",\n \"country\": \"BR\" //de acordo com a ISO 3166-1 alpha2\n },\n \"metadata\": {\n \"chave\": \"asaasdsdsadwqdqd\",\n \"boolean\": false,\n \"double\": 10.5,\n \"int\": 2\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Organizations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{url}}/v1/organizations", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations" + ], + "query": [ + { + "key": "metadata.chave", + "value": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlf", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Organizations by Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Organizations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Ledgers", + "item": [ + { + "name": "Ledgers", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "if (jsonData.hasOwnProperty('id')) {", + " console.log(\"ledger_id antes: \" + postman.getEnvironmentVariable(\"ledger_id\"));", + " postman.setEnvironmentVariable(\"ledger_id\", jsonData.id);", + " console.log(\"ledger_id depois: \" + postman.getEnvironmentVariable(\"ledger_id\"));", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"McGregor Tech LTDA\",\n \"status\": {\n \"code\": \"ACTIVE\",\n \"description\": \"Teste Ledger\"\n },\n \"metadata\": {\n \"chave\": \"McGregor\",\n \"boolean\": true,\n \"double\": 10.5,\n \"int\": 1\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers" + ] + } + }, + "response": [] + }, + { + "name": "Ledgers", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"BLOCKED Tech LTDA\",\n \"status\": {\n \"code\": \"BLOCKED\",\n \"description\": \"Teste BLOCKED Ledger\"\n },\n \"metadata\": {\n \"chave\": \"mudei\",\n \"boolean\": false,\n \"double\": 90.5,\n \"int\": 1000\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Ledgers", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers" + ], + "query": [ + { + "key": "metadata.chave", + "value": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlf", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Ledgers by Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Ledgers", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Instruments", + "item": [ + { + "name": "Instruments", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "if (jsonData.hasOwnProperty('id')) {", + " console.log(\"instrument_id antes: \" + postman.getEnvironmentVariable(\"instrument_id\"));", + " postman.setEnvironmentVariable(\"instrument_id\", jsonData.id);", + " console.log(\"instrument_id depois: \" + postman.getEnvironmentVariable(\"instrument_id\"));", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Brazilian Real\",\n \"type\": \"FIAT\",\n \"code\": \"BRL\",\n \"status\": {\n \"code\": \"ACTIVE\",\n \"description\": \"Teste Ledger\"\n },\n \"metadata\": {\n \"chave\": \"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlf\",\n \"boolean\": true,\n \"double\": 10.5,\n \"int\": 1\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/instruments", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "instruments" + ] + } + }, + "response": [] + }, + { + "name": "Instruments", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Bitcoin\",\n \"status\": {\n \"code\": \"BLOCKED\",\n \"description\": \"McGregor 2 BLOCKED INSTRUMENT\"\n },\n \"metadata\": {\n \"chave\": \"jacare\",\n \"boolean\": true,\n \"double\": 10.5,\n \"int\": 1\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/instruments/{{instrument_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "instruments", + "{{instrument_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Instruments", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/instruments", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "instruments" + ], + "query": [ + { + "key": "metadata.internalExchangeAddress", + "value": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlf", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Instruments by Id", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/instruments/{{instrument_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "instruments", + "{{instrument_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Accounts", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/instruments/{{instrument_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "instruments", + "{{instrument_id}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Portfolios", + "item": [ + { + "name": "Portfolios", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "if (jsonData.hasOwnProperty('id')) {", + " console.log(\"portfolio_id antes: \" + postman.getEnvironmentVariable(\"portfolio_id\"));", + " postman.setEnvironmentVariable(\"portfolio_id\", jsonData.id);", + " console.log(\"portfolio_id depois: \" + postman.getEnvironmentVariable(\"portfolio_id\"));", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"entityId\": \"{{$randomUUID}}\",\n \"name\": \"McGregor Portfolio 3\",\n \"status\": {\n \"code\": \"ACTIVE\",\n \"description\": \"Teste Portfolio 3\"\n },\n \"metadata\": {\n \"chave\": \"xuxa\",\n \"boolean\": true,\n \"double\": 10.5,\n \"int\": 1\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/portfolios", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "portfolios" + ] + } + }, + "response": [] + }, + { + "name": "Portfolios", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"McGregor Portfolio 3 UPDATE\",\n \"status\": {\n \"code\": \"BLOCKED\",\n \"description\": \"Teste Portfolio 3 Update\"\n },\n \"metadata\": {\n \"chave\": \"Change Update\",\n \"boolean\": true,\n \"double\": 10.5,\n \"int\": 1\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/portfolios/{{portfolio_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "portfolios", + "{{portfolio_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Portfolios", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/portfolios?metadata.chave=xuxa", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "portfolios" + ], + "query": [ + { + "key": "metadata.chave", + "value": "xuxa" + } + ] + } + }, + "response": [] + }, + { + "name": "Portfolios by Id", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/portfolios/{{portfolio_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "portfolios", + "{{portfolio_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Portfolios", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/portfolios/{{portfolio_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "portfolios", + "{{portfolio_id}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Products", + "item": [ + { + "name": "Products", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "if (jsonData.hasOwnProperty('id')) {", + " console.log(\"product_id antes: \" + postman.getEnvironmentVariable(\"product_id\"));", + " postman.setEnvironmentVariable(\"product_id\", jsonData.id);", + " console.log(\"product_id depois: \" + postman.getEnvironmentVariable(\"product_id\"));", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"McGregor Product 1\",\n \"status\": {\n \"code\": \"ACTIVE\",\n \"description\": \"Teste Produc3\"\n },\n \"metadata\": {\n \"chave\": \"eliana\",\n \"boolean\": true,\n \"double\": 10.5,\n \"int\": 1\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/products", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "products" + ] + } + }, + "response": [] + }, + { + "name": "Products", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"McGregor Product 1 BLOCKED\",\n \"status\": {\n \"code\": \"BLOCKED\",\n \"description\": \"Teste Product BLOCKED\"\n },\n \"metadata\": {\n \"chave\": \"elianas\",\n \"boolean\": false,\n \"double\": 100.5,\n \"int\": 2\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/products/{{product_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "products", + "{{product_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Products", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/products?metadata.chave=xuxa", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "products" + ], + "query": [ + { + "key": "metadata.chave", + "value": "xuxa" + } + ] + } + }, + "response": [] + }, + { + "name": "Products by Id", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/products/{{product_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "products", + "{{product_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Products", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/products/{{product_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "products", + "{{product_id}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Accounts", + "item": [ + { + "name": "Accounts", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = JSON.parse(responseBody);", + "if (jsonData.hasOwnProperty('id')) {", + " console.log(\"account_id antes: \" + postman.getEnvironmentVariable(\"account_id\"));", + " postman.setEnvironmentVariable(\"account_id\", jsonData.id);", + " console.log(\"account_id depois: \" + postman.getEnvironmentVariable(\"account_id\"));", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"instrumentCode\": \"BRL\",\n \"name\": \"My BRL Account\",\n \"alias\": \"McGregor\",\n \"type\": \"deposit\",\n //\"parentAccountId\": \"{{account_id}}\",\n //\"entityId\": \"{{$randomUUID}}\", //optional\n \"productId\": \"{{product_id}}\",\n \"balance\": {\n \"available\": 1000,\n \"onHold\": 10000,\n \"scale\": 2\n },\n \"status\": {\n \"code\": \"ACTIVE\",\n \"description\": \"Teste Account\"\n },\n \"metadata\": {\n \"chave\": \"xuxa\",\n \"boolean\": true,\n \"double\": 10.5,\n \"int\": 1\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/portfolios/{{portfolio_id}}/accounts", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "portfolios", + "{{portfolio_id}}", + "accounts" + ] + } + }, + "response": [] + }, + { + "name": "Accounts", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Sanchez Account\", //opcional\n \"alias\": \"Sanchez\",\n \"allowSending\": false,\n \"allowReceiving\": false,\n \"productId\": \"{{product_id}}\",\n \"status\": {\n \"code\": \"ACTIVE\",\n \"description\": \"Teste Account\"\n },\n \"metadata\": {\n \"chave\": \"cracatua\",\n \"boolean\": false,\n \"double\": 15.5,\n \"int\": 10\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/portfolios/{{portfolio_id}}/accounts/{{account_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "portfolios", + "{{portfolio_id}}", + "accounts", + "{{account_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Accounts", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/portfolios/{{portfolio_id}}/accounts?metadata.chave=cracatua", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "portfolios", + "{{portfolio_id}}", + "accounts" + ], + "query": [ + { + "key": "metadata.chave", + "value": "cracatua" + } + ] + } + }, + "response": [] + }, + { + "name": "Accounts by Id", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/portfolios/{{portfolio_id}}/accounts/{{account_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "portfolios", + "{{portfolio_id}}", + "accounts", + "{{account_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Accounts", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{url}}/v1/organizations/{{organization_id}}/ledgers/{{ledger_id}}/portfolios/{{portfolio_id}}/accounts/{{account_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "organizations", + "{{organization_id}}", + "ledgers", + "{{ledger_id}}", + "portfolios", + "{{portfolio_id}}", + "accounts", + "{{account_id}}" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "id", + "value": "1" + }, + { + "key": "base_url", + "value": "https://postman-rest-api-learner.glitch.me/" + } + ] +} \ No newline at end of file diff --git a/revive.toml b/revive.toml new file mode 100644 index 00000000..03f5c911 --- /dev/null +++ b/revive.toml @@ -0,0 +1,29 @@ +ignoreGeneratedHeader = false +severity = "warning" +confidence = 0.8 +errorCode = 0 +warningCode = 0 + +[rule.blank-imports] +[rule.context-as-argument] +[rule.context-keys-type] +[rule.dot-imports] +[rule.empty-block] +[rule.error-naming] +[rule.error-return] +[rule.error-strings] +[rule.errorf] +[rule.exported] +[rule.increment-decrement] +[rule.indent-error-flow] +[rule.package-comments] +[rule.range] +[rule.receiver-naming] +[rule.redefines-builtin-id] +[rule.superfluous-else] +[rule.time-naming] +[rule.unexported-return] +[rule.unreachable-code] +[rule.unused-parameter] +[rule.var-declaration] +[rule.var-naming] \ No newline at end of file From 80d1cd469ddf65b918155901e50534f4b3c78909 Mon Sep 17 00:00:00 2001 From: lerian-studio <srv.iam@lerian.studio> Date: Fri, 17 May 2024 20:00:23 +0000 Subject: [PATCH 2/4] chore(release): 1.0.0-beta.1 ## 1.0.0-beta.1 (2024-05-17) ### Features * Open tech for all ([cd4cf48](https://github.com/LerianStudio/midaz/commit/cd4cf4874503756b6b051723f512fde41323e609)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef486bec..97f7eaa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.0.0-beta.1 (2024-05-17) + + +### Features + +* Open tech for all ([cd4cf48](https://github.com/LerianStudio/midaz/commit/cd4cf4874503756b6b051723f512fde41323e609)) + ## [1.17.0](https://github.com/LerianStudio/midaz-private/compare/v1.16.0...v1.17.0) (2024-05-17) From 2fd77c298a1aa4c74dbfa5e030ec65ca3628afd4 Mon Sep 17 00:00:00 2001 From: MartinezAvellan <martinez.avellan@hotmail.com> Date: Fri, 17 May 2024 17:29:25 -0300 Subject: [PATCH 3/4] fix: change conversion of a signed 64-bit integer to int --- components/mdz/pkg/flags.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/mdz/pkg/flags.go b/components/mdz/pkg/flags.go index 77308d9c..2167cf51 100644 --- a/components/mdz/pkg/flags.go +++ b/components/mdz/pkg/flags.go @@ -67,12 +67,12 @@ func GetInt(cmd *cobra.Command, flagName string) int { if err != nil { v := os.Getenv(strcase.ToScreamingSnake(flagName)) if v != "" { - v, err := strconv.ParseInt(v, 10, 64) + v, err := strconv.Atoi(v) if err != nil { return 0 } - return int(v) + return v } return 0 From 90fb1ec2d00a655545a797b53ef1eafd2ff024a9 Mon Sep 17 00:00:00 2001 From: lerian-studio <srv.iam@lerian.studio> Date: Fri, 17 May 2024 20:36:38 +0000 Subject: [PATCH 4/4] chore(release): 1.0.0-beta.2 ## [1.0.0-beta.2](https://github.com/LerianStudio/midaz/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2024-05-17) ### Bug Fixes * change conversion of a signed 64-bit integer to int ([2fd77c2](https://github.com/LerianStudio/midaz/commit/2fd77c298a1aa4c74dbfa5e030ec65ca3628afd4)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97f7eaa9..55bc9567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [1.0.0-beta.2](https://github.com/LerianStudio/midaz/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2024-05-17) + + +### Bug Fixes + +* change conversion of a signed 64-bit integer to int ([2fd77c2](https://github.com/LerianStudio/midaz/commit/2fd77c298a1aa4c74dbfa5e030ec65ca3628afd4)) + ## 1.0.0-beta.1 (2024-05-17)