From 5bd7358b7b3657d4cf0f88e279f4315ba3be18b6 Mon Sep 17 00:00:00 2001 From: Rohit Sachdeva Date: Wed, 4 Sep 2024 17:10:28 -0500 Subject: [PATCH] Initial release v0.10.0: drive-deposits Financial System - Integrated asynchronous components : AWS Lambdas Writer and Reader (Axum), DynamoDB, EventBridge - Integrated AWS Rust SDK for seamless AWS service interactions - Implemented synchronous components: gRPC (Tonic) and REST (Axum) - Established event-driven architecture with AWS EventBridge and EventBus - Utilized Tokio for efficient concurrency management - Leveraged Rust's built-in performance and safety features - Implemented high-performance computing with gRPC, REST and AWS Serverless Components - Set up SAM deployment for serverless components - Implemented domain-driven design with financial terminology - Added sorting capabilities with Top 'k' based on delta growth and maturity date - Integrated LocalStack for development and testing along with cargo lambda - Implemented comprehensive error handling and logging - Implemented base functionality with room for future enhancements - README has more detailed information about the system architecture and usage --- .cargo/config.toml | 14 + .gitignore | 14 + Cargo.lock | 3909 +++++++++++++++++ Cargo.toml | 30 + Dockerfile.localstack | 14 + LICENSE | 21 + README.md | 354 ++ drive-deposits-cal-types/Cargo.toml | 28 + drive-deposits-cal-types/src/cal_types.rs | 99 + drive-deposits-cal-types/src/convert.rs | 3 + .../src/convert/from_cal_event_source.rs | 100 + .../src/convert/from_cal_grpc_response.rs | 103 + .../src/convert/from_grpc_cal_request.rs | 58 + drive-deposits-cal-types/src/lib.rs | 3 + drive-deposits-cal-types/src/math.rs | 9 + .../src/math/accumulator.rs | 89 + .../src/math/compound_interest.rs | 41 + drive-deposits-cal-types/src/math/engine.rs | 200 + drive-deposits-cal-types/src/math/growth.rs | 37 + .../src/math/individual_calculation_error.rs | 34 + .../src/math/maturity_date.rs | 22 + drive-deposits-cal-types/src/math/outcome.rs | 149 + .../src/math/simple_interest.rs | 35 + drive-deposits-cal-types/src/math/total.rs | 5 + .../tests/helper/enable_tracing.rs | 31 + drive-deposits-cal-types/tests/helper/mod.rs | 3 + .../tests/helper/test_data.rs | 5 + .../tests/test_calculate_portfolio.rs | 25 + .../tests/test_compound_interest.rs | 169 + .../tests/test_maturity_date.rs | 31 + .../tests/test_simple_interest.rs | 167 + drive-deposits-check-cmd/Cargo.toml | 32 + drive-deposits-check-cmd/src/lib.rs | 1 + drive-deposits-check-cmd/src/main.rs | 52 + drive-deposits-check-cmd/src/portfolio.rs | 1 + .../src/portfolio/calculate.rs | 83 + .../data/two_banks_json_request_invalid.json | 82 + .../data/two_banks_json_request_valid.json | 82 + .../tests/enable_tracing.rs | 32 + .../tests/test_delta_calculator_cli.rs | 70 + drive-deposits-event-source/Cargo.toml | 22 + drive-deposits-event-source/samconfig.toml | 9 + drive-deposits-event-source/src/eb.rs | 177 + drive-deposits-event-source/src/lib.rs | 2 + .../src/payload_types.rs | 9 + drive-deposits-event-source/template.yaml | 9 + drive-deposits-grpc-server/Cargo.toml | 20 + ...an_calculate_for_period_request_valid.json | 82 + .../data/grpc_postman_response_for_valid.json | 268 ++ drive-deposits-grpc-server/src/lib.rs | 2 + drive-deposits-grpc-server/src/main.rs | 31 + drive-deposits-grpc-server/src/portfolio.rs | 31 + .../src/portfolio/calculate.rs | 86 + .../src/portfolio/grpc_status_handler.rs | 56 + .../src/service_router.rs | 29 + drive-deposits-lambda-db-types/Cargo.toml | 15 + drive-deposits-lambda-db-types/src/convert.rs | 2 + .../src/convert/reader.rs | 9 + ...rom_bank_level_item_to_by_bank_response.rs | 31 + ..._deposit_level_item_to_deposit_response.rs | 43 + .../from_hashmap_av_to_bank_level_item.rs | 32 + .../from_hashmap_av_to_deposit_level_item.rs | 49 + ...from_hashmap_av_to_portfolio_level_item.rs | 36 + ...tfolio_level_item_to_portfolio_response.rs | 23 + .../src/convert/reader/with_level_context.rs | 117 + .../src/convert/writer.rs | 7 + .../writer/from_bank_item_to_hashmap_av.rs | 27 + .../from_deposit_level_item_to_hashmap_av.rs | 48 + ...from_portfolio_level_item_to_hashmap_av.rs | 21 + .../from_rest_to_bank_level_items_wrapper.rs | 58 + ...rom_rest_to_deposit_level_items_wrapper.rs | 107 + .../from_rest_to_portfolio_level_item.rs | 55 + .../src/convert/writer/with_level_context.rs | 79 + .../src/db_item_types.rs | 74 + drive-deposits-lambda-db-types/src/lib.rs | 3 + .../src/query_response_types.rs | 112 + .../.gitignore | 3 + .../Cargo.toml | 37 + ...-request-query-for-banks-delta-growth.json | 135 + ...quest-query-for-deposits-delta-growth.json | 135 + ...uest-query-for-deposits-maturity-date.json | 135 + ...est-query-for-portfolios-delta-growth.json | 135 + .../request.http | 59 + .../samconfig.toml | 10 + .../src/bin/by_level_lambda_reader.rs | 230 + .../src/dynamodb.rs | 78 + .../src/dynamodb/query.rs | 296 ++ .../src/handler_error.rs | 56 + .../src/lib.rs | 4 + .../src/request_error.rs | 7 + .../template.yaml | 68 + drive-deposits-logs-lambda-target/.gitignore | 3 + drive-deposits-logs-lambda-target/Cargo.toml | 23 + .../drive-deposits-event-banks-level.json | 264 ++ .../samconfig.toml | 9 + .../src/bin/by_level_lambda_writer.rs | 55 + .../src/dynamodb.rs | 68 + .../src/dynamodb/add.rs | 118 + drive-deposits-logs-lambda-target/src/lib.rs | 1 + .../template.yaml | 154 + drive-deposits-proto-grpc-types/Cargo.toml | 16 + drive-deposits-proto-grpc-types/build.rs | 13 + .../src/convert.rs | 2 + .../src/convert/from_grpc_rest_response.rs | 112 + .../src/convert/from_rest_grpc_request.rs | 64 + drive-deposits-proto-grpc-types/src/lib.rs | 9 + .../v1/drivedeposits.proto | 114 + drive-deposits-rest-gateway-server/Cargo.toml | 24 + .../data/rest_request_invalid_decimal.json | 82 + .../rest_request_invalid_json_structure.json | 82 + ...count_type_decimal_bank_tz_start_date.json | 82 + .../data/rest_request_valid.json | 82 + ...t_request_valid_6_months_delta_period.json | 82 + ...st_request_valid_90_days_delta_period.json | 82 + ...uest_valid_greater_amount_investments.json | 80 + ...quest_valid_lesser_amount_investments.json | 58 + .../data/rest_response_for_valid.json | 254 ++ ...come_maturity_calculation_not_present.json | 177 + .../request.http | 846 ++++ .../src/drive_deposits_client.rs | 48 + .../src/drive_deposits_client/app_error.rs | 52 + .../drive_deposits_client/request_error.rs | 55 + drive-deposits-rest-gateway-server/src/lib.rs | 2 + .../src/main.rs | 43 + .../src/service_router.rs | 51 + drive-deposits-rest-types/Cargo.toml | 14 + drive-deposits-rest-types/src/lib.rs | 1 + drive-deposits-rest-types/src/rest_types.rs | 262 ++ justfile | 611 +++ 129 files changed, 13501 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile.localstack create mode 100644 LICENSE create mode 100644 README.md create mode 100644 drive-deposits-cal-types/Cargo.toml create mode 100644 drive-deposits-cal-types/src/cal_types.rs create mode 100644 drive-deposits-cal-types/src/convert.rs create mode 100644 drive-deposits-cal-types/src/convert/from_cal_event_source.rs create mode 100644 drive-deposits-cal-types/src/convert/from_cal_grpc_response.rs create mode 100644 drive-deposits-cal-types/src/convert/from_grpc_cal_request.rs create mode 100644 drive-deposits-cal-types/src/lib.rs create mode 100644 drive-deposits-cal-types/src/math.rs create mode 100644 drive-deposits-cal-types/src/math/accumulator.rs create mode 100644 drive-deposits-cal-types/src/math/compound_interest.rs create mode 100644 drive-deposits-cal-types/src/math/engine.rs create mode 100644 drive-deposits-cal-types/src/math/growth.rs create mode 100644 drive-deposits-cal-types/src/math/individual_calculation_error.rs create mode 100644 drive-deposits-cal-types/src/math/maturity_date.rs create mode 100644 drive-deposits-cal-types/src/math/outcome.rs create mode 100644 drive-deposits-cal-types/src/math/simple_interest.rs create mode 100644 drive-deposits-cal-types/src/math/total.rs create mode 100644 drive-deposits-cal-types/tests/helper/enable_tracing.rs create mode 100644 drive-deposits-cal-types/tests/helper/mod.rs create mode 100644 drive-deposits-cal-types/tests/helper/test_data.rs create mode 100644 drive-deposits-cal-types/tests/test_calculate_portfolio.rs create mode 100644 drive-deposits-cal-types/tests/test_compound_interest.rs create mode 100644 drive-deposits-cal-types/tests/test_maturity_date.rs create mode 100644 drive-deposits-cal-types/tests/test_simple_interest.rs create mode 100644 drive-deposits-check-cmd/Cargo.toml create mode 100644 drive-deposits-check-cmd/src/lib.rs create mode 100644 drive-deposits-check-cmd/src/main.rs create mode 100644 drive-deposits-check-cmd/src/portfolio.rs create mode 100644 drive-deposits-check-cmd/src/portfolio/calculate.rs create mode 100644 drive-deposits-check-cmd/tests/data/two_banks_json_request_invalid.json create mode 100644 drive-deposits-check-cmd/tests/data/two_banks_json_request_valid.json create mode 100644 drive-deposits-check-cmd/tests/enable_tracing.rs create mode 100644 drive-deposits-check-cmd/tests/test_delta_calculator_cli.rs create mode 100644 drive-deposits-event-source/Cargo.toml create mode 100644 drive-deposits-event-source/samconfig.toml create mode 100644 drive-deposits-event-source/src/eb.rs create mode 100644 drive-deposits-event-source/src/lib.rs create mode 100644 drive-deposits-event-source/src/payload_types.rs create mode 100644 drive-deposits-event-source/template.yaml create mode 100644 drive-deposits-grpc-server/Cargo.toml create mode 100644 drive-deposits-grpc-server/data/grpc_postman_calculate_for_period_request_valid.json create mode 100644 drive-deposits-grpc-server/data/grpc_postman_response_for_valid.json create mode 100644 drive-deposits-grpc-server/src/lib.rs create mode 100644 drive-deposits-grpc-server/src/main.rs create mode 100644 drive-deposits-grpc-server/src/portfolio.rs create mode 100644 drive-deposits-grpc-server/src/portfolio/calculate.rs create mode 100644 drive-deposits-grpc-server/src/portfolio/grpc_status_handler.rs create mode 100644 drive-deposits-grpc-server/src/service_router.rs create mode 100644 drive-deposits-lambda-db-types/Cargo.toml create mode 100644 drive-deposits-lambda-db-types/src/convert.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/reader.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/reader/from_bank_level_item_to_by_bank_response.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/reader/from_deposit_level_item_to_deposit_response.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_bank_level_item.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_deposit_level_item.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_portfolio_level_item.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/reader/from_portfolio_level_item_to_portfolio_response.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/reader/with_level_context.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/writer.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/writer/from_bank_item_to_hashmap_av.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/writer/from_deposit_level_item_to_hashmap_av.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/writer/from_portfolio_level_item_to_hashmap_av.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_bank_level_items_wrapper.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_deposit_level_items_wrapper.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_portfolio_level_item.rs create mode 100644 drive-deposits-lambda-db-types/src/convert/writer/with_level_context.rs create mode 100644 drive-deposits-lambda-db-types/src/db_item_types.rs create mode 100644 drive-deposits-lambda-db-types/src/lib.rs create mode 100644 drive-deposits-lambda-db-types/src/query_response_types.rs create mode 100644 drive-deposits-lambda-dynamodb-reader/.gitignore create mode 100644 drive-deposits-lambda-dynamodb-reader/Cargo.toml create mode 100644 drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-banks-delta-growth.json create mode 100644 drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-deposits-delta-growth.json create mode 100644 drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-deposits-maturity-date.json create mode 100644 drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-portfolios-delta-growth.json create mode 100644 drive-deposits-lambda-dynamodb-reader/request.http create mode 100644 drive-deposits-lambda-dynamodb-reader/samconfig.toml create mode 100644 drive-deposits-lambda-dynamodb-reader/src/bin/by_level_lambda_reader.rs create mode 100644 drive-deposits-lambda-dynamodb-reader/src/dynamodb.rs create mode 100644 drive-deposits-lambda-dynamodb-reader/src/dynamodb/query.rs create mode 100644 drive-deposits-lambda-dynamodb-reader/src/handler_error.rs create mode 100644 drive-deposits-lambda-dynamodb-reader/src/lib.rs create mode 100644 drive-deposits-lambda-dynamodb-reader/src/request_error.rs create mode 100644 drive-deposits-lambda-dynamodb-reader/template.yaml create mode 100644 drive-deposits-logs-lambda-target/.gitignore create mode 100644 drive-deposits-logs-lambda-target/Cargo.toml create mode 100644 drive-deposits-logs-lambda-target/data/drive-deposits-event-banks-level.json create mode 100644 drive-deposits-logs-lambda-target/samconfig.toml create mode 100644 drive-deposits-logs-lambda-target/src/bin/by_level_lambda_writer.rs create mode 100644 drive-deposits-logs-lambda-target/src/dynamodb.rs create mode 100644 drive-deposits-logs-lambda-target/src/dynamodb/add.rs create mode 100644 drive-deposits-logs-lambda-target/src/lib.rs create mode 100644 drive-deposits-logs-lambda-target/template.yaml create mode 100644 drive-deposits-proto-grpc-types/Cargo.toml create mode 100644 drive-deposits-proto-grpc-types/build.rs create mode 100644 drive-deposits-proto-grpc-types/src/convert.rs create mode 100644 drive-deposits-proto-grpc-types/src/convert/from_grpc_rest_response.rs create mode 100644 drive-deposits-proto-grpc-types/src/convert/from_rest_grpc_request.rs create mode 100644 drive-deposits-proto-grpc-types/src/lib.rs create mode 100644 drive-deposits-proto-grpc-types/v1/drivedeposits.proto create mode 100644 drive-deposits-rest-gateway-server/Cargo.toml create mode 100644 drive-deposits-rest-gateway-server/data/rest_request_invalid_decimal.json create mode 100644 drive-deposits-rest-gateway-server/data/rest_request_invalid_json_structure.json create mode 100644 drive-deposits-rest-gateway-server/data/rest_request_invalid_period_unit_account_type_decimal_bank_tz_start_date.json create mode 100644 drive-deposits-rest-gateway-server/data/rest_request_valid.json create mode 100644 drive-deposits-rest-gateway-server/data/rest_request_valid_6_months_delta_period.json create mode 100644 drive-deposits-rest-gateway-server/data/rest_request_valid_90_days_delta_period.json create mode 100644 drive-deposits-rest-gateway-server/data/rest_request_valid_greater_amount_investments.json create mode 100644 drive-deposits-rest-gateway-server/data/rest_request_valid_lesser_amount_investments.json create mode 100644 drive-deposits-rest-gateway-server/data/rest_response_for_valid.json create mode 100644 drive-deposits-rest-gateway-server/data/rest_response_with_processing_errors_no_panic_outcome_maturity_calculation_not_present.json create mode 100644 drive-deposits-rest-gateway-server/request.http create mode 100644 drive-deposits-rest-gateway-server/src/drive_deposits_client.rs create mode 100644 drive-deposits-rest-gateway-server/src/drive_deposits_client/app_error.rs create mode 100644 drive-deposits-rest-gateway-server/src/drive_deposits_client/request_error.rs create mode 100644 drive-deposits-rest-gateway-server/src/lib.rs create mode 100644 drive-deposits-rest-gateway-server/src/main.rs create mode 100644 drive-deposits-rest-gateway-server/src/service_router.rs create mode 100644 drive-deposits-rest-types/Cargo.toml create mode 100644 drive-deposits-rest-types/src/lib.rs create mode 100644 drive-deposits-rest-types/src/rest_types.rs create mode 100644 justfile diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..e5f1b98 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,14 @@ +[env] +# https://doc.rust-lang.org/cargo/reference/config.html#env +# by_level_lambda_writer=debug for bindary being by_level_lambda_writer for lambda +RUST_LOG = { value = "drive_deposits_rest_types=debug,drive_deposits_proto_grpc_types=debug,drive_deposits_event_source=debug,drive_deposits_lambda_db_types=debug,drive_deposits_logs_lambda_target=debug,by_level_lambda_writer=debug,drive_deposits_lambda_dynamodb_reader=debug,by_level_lambda_reader=debug,drive_deposits_cal_types=debug,drive_deposits_check_cmd=debug,drive_deposits_grpc_server=debug,drive_deposits_rest_gateway_server=debug", force = true } + + +# default settings -- can be changed by setting these env. variables on command line +SEND_CAL_EVENTS = "true" +USE_LOCALSTACK = "false" + + +[alias] +# https://doc.rust-lang.org/cargo/reference/config.html#alias +ddcheck = "run --package drive-deposits-check-cmd --bin drive-deposits-check-cmd" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7be897a --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +debug/ +target/ +/drive-deposits-event-source/.aws-sam +/drive-deposits-logs-lambda-target/.aws-sam +/volume +/private + +# some extra files +# These are backup files generated by rustfmt +**/*.rs.bk +# Working on MacOS still per Github keeping this +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2254dae --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3909 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "assert_cmd" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "async-compression" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd", + "zstd-safe", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "aws-config" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 0.2.12", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-runtime" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42c2d4218de4dcd890a109461e2f799a1a2ba3bcd2cde9af88360f5df9266c6" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-dynamodb" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aadafd673822026e7ae6be7900c7886f609514b620874c9e3054f4ae38ab82f" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-eventbridge" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4fb65d775433ba494cc8b67584129989ae31316b5f0a204d7638b5592fb2570" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11822090cf501c316c6f75711d77b96fba30658e3867a7762e5e2f5d32d31e81" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78a2a06ff89176123945d1bbe865603c4d7101bea216a550bb4d2e4e9ba74d74" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20a91795850826a6f456f4a48eff1dfa59a0e69bdbf5b8c50518fd372106574" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df1b0fa6be58efe9d4ccc257df0a53b89cd8909e86591a13ca54817c87517be" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.1.0", + "once_cell", + "p256", + "percent-encoding", + "ring", + "sha2", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.60.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9cd0ae3d97daa0a2bf377a4d8e8e1362cae590c4a1aad0d40058ebca18eb91e" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abbf454960d0db2ad12684a1640120e7557294b0ff8e2f11236290a1b293225" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "http-body 1.0.1", + "httparse", + "hyper 0.14.30", + "hyper-rustls", + "once_cell", + "pin-project-lite", + "pin-utils", + "rustls", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.1.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cee7cadb433c781d3299b916fbf620fea813bf38f49db282fb6858141a05cc8" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.1.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "aws_lambda_events" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7319a086b79c3ff026a33a61e80f04fd3885fbb73237981ea080d21944e1cb1c" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "flate2", + "http 1.1.0", + "http-body 1.0.1", + "http-serde", + "query_map", + "serde", + "serde_dynamo", + "serde_json", + "serde_with", +] + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.74", + "syn_derive", +] + +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "regex-automata 0.4.7", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +dependencies = [ + "serde", +] + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cc" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", + "serde", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "clap" +version = "4.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.74", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "drive-deposits-cal-types" +version = "0.10.0" +dependencies = [ + "anyhow", + "chrono", + "chrono-tz", + "drive-deposits-event-source", + "drive-deposits-proto-grpc-types", + "heck 0.5.0", + "once_cell", + "pretty_assertions", + "rust_decimal", + "rust_decimal_macros", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "drive-deposits-check-cmd" +version = "0.10.0" +dependencies = [ + "anyhow", + "assert_cmd", + "clap", + "drive-deposits-cal-types", + "drive-deposits-event-source", + "drive-deposits-proto-grpc-types", + "drive-deposits-rest-types", + "once_cell", + "predicates", + "rust_decimal", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", + "validator", +] + +[[package]] +name = "drive-deposits-event-source" +version = "0.10.0" +dependencies = [ + "anyhow", + "aws-config", + "aws-sdk-eventbridge", + "drive-deposits-rest-types", + "log", + "pretty_assertions", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "drive-deposits-grpc-server" +version = "0.10.0" +dependencies = [ + "drive-deposits-cal-types", + "drive-deposits-event-source", + "drive-deposits-proto-grpc-types", + "drive-deposits-rest-types", + "tokio", + "tonic", + "tonic-reflection", + "tonic-types", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "drive-deposits-lambda-db-types" +version = "0.10.0" +dependencies = [ + "aws-sdk-dynamodb", + "drive-deposits-rest-types", + "rust_decimal", + "rust_decimal_macros", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "drive-deposits-lambda-dynamodb-reader" +version = "0.10.0" +dependencies = [ + "aws-config", + "aws-sdk-dynamodb", + "axum", + "drive-deposits-lambda-db-types", + "lambda_http", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "drive-deposits-logs-lambda-target" +version = "0.10.0" +dependencies = [ + "aws-config", + "aws-sdk-dynamodb", + "aws_lambda_events", + "drive-deposits-lambda-db-types", + "drive-deposits-rest-types", + "lambda_runtime", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "drive-deposits-proto-grpc-types" +version = "0.10.0" +dependencies = [ + "drive-deposits-rest-types", + "heck 0.5.0", + "prost", + "tonic", + "tonic-build", + "tracing", +] + +[[package]] +name = "drive-deposits-rest-gateway-server" +version = "0.10.0" +dependencies = [ + "axum", + "drive-deposits-proto-grpc-types", + "drive-deposits-rest-types", + "serde", + "serde_json", + "thiserror", + "tokio", + "tonic", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "validator", +] + +[[package]] +name = "drive-deposits-rest-types" +version = "0.10.0" +dependencies = [ + "chrono", + "chrono-tz", + "rust_decimal", + "serde", + "serde_json", + "strum", + "strum_macros", + "validator", +] + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint 0.4.9", + "der", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.4.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.4.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "byteorder", + "num-traits", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" + +[[package]] +name = "http-serde" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f056c8559e3757392c8d091e796416e4649d8e49e88b8d76df6c002f05027fd" +dependencies = [ + "http 1.1.0", + "serde", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.5", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.30", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.4.1", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "iri-string" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f5f6c2df22c009ac44f6f1499308e7a3ac7ba42cd2378475cc691510e1eef1b" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lambda_http" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fe279be7f89f5f72c97c3a96f45c43db8edab1007320ecc6a5741273b4d6db" +dependencies = [ + "aws_lambda_events", + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "lambda_runtime", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio-stream", + "url", +] + +[[package]] +name = "lambda_runtime" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed49669d6430292aead991e19bf13153135a884f916e68f32997c951af637ebe" +dependencies = [ + "async-stream", + "base64 0.22.1", + "bytes", + "futures", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "http-serde", + "hyper 1.4.1", + "hyper-util", + "lambda_runtime_api_client", + "pin-project", + "serde", + "serde_json", + "serde_path_to_error", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tracing", +] + +[[package]] +name = "lambda_runtime_api_client" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c90a10f094475a34a04da2be11686c4dcfe214d93413162db9ffdff3d3af293a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "tokio", + "tower", + "tower-service", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.156" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.4.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn 2.0.74", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13db3d3fde688c61e2446b4d843bc27a7e8af269a69440c0308021dc92333cc" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb182580f71dd070f88d01ce3de9f4da5021db7115d2e1c3605a754153b77c1" +dependencies = [ + "bytes", + "heck 0.5.0", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.74", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "prost-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee5168b05f49d4b0ca581206eb14a7b22fafd963efe729ac48eb03266e25cc2" +dependencies = [ + "prost", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "query_map" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eab6b8b1074ef3359a863758dae650c7c0c6027927a085b7af911c8e0bf3a15" +dependencies = [ + "form_urlencoded", + "serde", + "serde_derive", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a05bf7103af0797dbce0667c471946b29b9eaea34652eff67324f360fec027de" +dependencies = [ + "quote", + "rust_decimal", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.208" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.208" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "serde_dynamo" +version = "4.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36c1b1792cfd9de29eb373ee6a4b74650369c096f55db7198ceb7b8921d1f7f" +dependencies = [ + "base64 0.21.7", + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.4.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.74", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.4.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tonic" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38659f4a91aba8598d27821589f5db7dddd94601e7a01b1e485a50e5484c7401" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2 0.4.5", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568392c5a2bd0020723e3f387891176aabafe36fd9fcd074ad309dfa0c8eb964" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "tonic-reflection" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b742c83ad673e9ab5b4ce0981f7b9e8932be9d60e8682cbf9120494764dbc173" +dependencies = [ + "prost", + "prost-types", + "tokio", + "tokio-stream", + "tonic", +] + +[[package]] +name = "tonic-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5563899ec5aa5f0ec48e37457461ffbbc184c9a0f413f715dacd154f46408a10" +dependencies = [ + "prost", + "prost-types", + "tonic", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "hdrhistogram", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "async-compression", + "base64 0.21.7", + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", + "rand", + "serde", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1cd046f83ea2c4e920d6ee9f7c3537ef928d75dce5d84a87c2c5d6b3999a3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "validator" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55591299b7007f551ed1eb79a684af7672c19c3193fb9e0a31936987bb2438ec" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.74", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2d8d77c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[workspace] +members = [ + # -- Libraries + "drive-deposits-event-source", + "drive-deposits-cal-types", + "drive-deposits-rest-types", + "drive-deposits-proto-grpc-types", + "drive-deposits-lambda-db-types", + # -- Binaries + "drive-deposits-check-cmd", + "drive-deposits-grpc-server", + "drive-deposits-rest-gateway-server", + "drive-deposits-logs-lambda-target", + "drive-deposits-lambda-dynamodb-reader", +] +default-members = [ + # -- Libraries + "drive-deposits-event-source", + "drive-deposits-cal-types", + "drive-deposits-rest-types", + "drive-deposits-proto-grpc-types", + "drive-deposits-lambda-db-types", + # -- Binaries + "drive-deposits-check-cmd", + "drive-deposits-grpc-server", + "drive-deposits-rest-gateway-server", + "drive-deposits-logs-lambda-target", + "drive-deposits-lambda-dynamodb-reader", +] +resolver = "2" diff --git a/Dockerfile.localstack b/Dockerfile.localstack new file mode 100644 index 0000000..1d68ecb --- /dev/null +++ b/Dockerfile.localstack @@ -0,0 +1,14 @@ +FROM localstack/localstack + +ARG LOCALSTACK_DEBUG=0 +ENV LOCALSTACK_DEBUG=${LOCALSTACK_DEBUG} + + +ARG LOCALSTACK_VOLUME_DIR=./volume +ENV LOCALSTACK_VOLUME_DIR=${LOCALSTACK_VOLUME_DIR} +# Define mount points for volumes +VOLUME ["${LOCALSTACK_VOLUME_DIR}", "/var/run/docker.sock"] + +ENV LOCALSTACK_DYNAMODB_SHARE_DB=1 + +EXPOSE 4566 4510-4559 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3bb8138 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Rohit Sachdeva + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c6efce --- /dev/null +++ b/README.md @@ -0,0 +1,354 @@ +## DriveDeposits: Building a Robust Financial System with a Scalable, Synchronous and Asynchronous Rust Backend + +## Table of Contents + +- [DriveDeposits: Architectural Pillars](#drivedeposits-architectural-pillars) +- [Synchronous Components](#synchronous-components) +- [Asynchronous Components](#asynchronous-components) +- [Domain Driven Terminology](#domain-driven-terminology) +- [Bridging Synchronous and Asynchronous Components In DriveDeposits](#bridging-synchronous-and-asynchronous-components-in-drivedeposits) +- [AWS EventBridge, EventBus and Targets include Cloudwatch Log group and Lambda Deployment](#aws-eventbridge-eventbus-and-targets-include-cloudwatch-log-group-and-lambda-deployment) +- [Hybrid Integration Testing Tool](#hybrid-integration-testing-tool) +- [Development Tool: LocalStack](#development-tool-localstack) +- [Data population and Querying](#data-population-and-querying) +- [Development Tool: cargo lambda](#development-tool-cargo-lambda) +- [Clean](#clean) +- [Configurations for DriveDeposits](#configurations-for-drivedeposits) +- [Member crates in workspace](#member-crates-in-workspace) + +### DriveDeposits: Architectural Pillars + +* **Event-Driven Architecture:** Built on AWS EventBridge and EventBus, DriveDeposits ensures seamless communication and + real-time data processing for dynamic financial calculations. +* **Scalable and Reliable:** AWS Lambda scales automatically to handle fluctuating workloads, ensuring consistent + performance and availability. +* **Rust-Powered Performance:** Built with Rust, DriveDeposits benefits from the language's renowned speed, safety, and + concurrency, resulting in a highly efficient and reliable system. +* **AWS Rust SDK Integration:** DriveDeposits leverages the official AWS SDK for Rust to interact seamlessly with + essential AWS services. This includes using the Lambda runtime for serverless function execution and DynamoDB for + efficient and scalable data storage and retrieval. +* **High-Performance Computing:** gRPC (Tonic) provides blazing-fast communication between services, while REST (Axum) + offers a user-friendly API gateway for external integrations. +* **Seamless Integration:** DriveDeposits integrates seamlessly with essential AWS services like CloudWatch for + monitoring based on EventBridge Rules. +* **Tokio-Powered Concurrency:** DriveDeposits leverages Tokio's powerful concurrency primitives to manage asynchronous + tasks efficiently. This includes sending events to AWS EventBridge for further + processing and analysis, ensuring a responsive and non-blocking system. +* **SAM Deployment:** The serverless components in this project are deployed using AWS Serverless Application Model ( + SAM). This includes AWS Lambda functions for writer and reader, as well as DynamoDB and EventBridge rules. + +**DriveDeposits offers a powerful and flexible solution for:** + +* **Financial institutions** looking to modernize their calculation infrastructure. +* **FinTech companies** seeking a scalable and reliable platform for real-time financial data processing. +* **Developers** building innovative financial applications that demand high performance and reliability. + +**Experience the future of financial calculations with DriveDeposits!** +Documentation for Drive Deposits is work in progress. More details will be added. + +### Synchronous Components + +- gRPC (using ***Tonic***) +- REST (using ***Axum***) + +### Asynchronous Components + +Using the ***AWS SDK for Rust*** + +- AWS Serverless Lambda Reader (uses ***Axum*** for Routing) +- AWS Serverless Lambda Writer +- DynamoDB +- EventBridge + +### Domain Driven Terminology: + +* **Portfolio:** A collection of investments, such as stocks, bonds, and mutual funds, owned by an individual or an + organization. +* **Bank:** A financial institution that accepts deposits for investment. In this context, "bank" refers to any + financial institution. +* **Deposit:** A sum of money placed in a bank account or other investment vehicle. +* **Levels (Portfolio, Bank, Deposit):** DriveDeposits allows query of data at different levels - portfolios, banks, + or deposits - to make informed financial decisions. +* **Delta:** Represents the growth or change in value over a specific period. It measures investment performance at + various levels (portfolios, banks, deposits). See + this [CalculationRequest](drive-deposits-rest-gateway-server/data/rest_request_valid.json) example: + + ```json + { + "new_delta": { + "period": "1", + "period_unit": "Month" + } + } + +* **Delta Growth:** The fluctuation in value over a user-specified period, as specified within the Calculation Request. + This fluctuation is calculated at the portfolio, bank, and deposit levels. +* **Sorting Capabilities with Top K Based on Delta Growth:** DriveDeposits allows sorting based on delta growth, + retrieving the top 'k' portfolios in ascending or descending order. For example: + +```http +{{drive_deposits_lambda_reader}}/by-level-for-portfolios/delta-growth?&order=desc&top_k=3 +``` + +```http +{{drive_deposits_lambda_reader}}/portfolios/{{aws_portfolio_uuid}}/by-level-for-banks/delta-growth?order=asc&top_k=9 +``` + +* **Maturity date:** The date when a deposit or investment reaches its full value or the end of its term. +* **Sorting capabilities with top_k based on maturity date:** DriveDeposits allows sorting by maturity date, + retrieving the top 'k' portfolios (where 'k' is a number defined by the user in the query) in ascending or descending + order. For + example: + +```http +{{drive_deposits_lambda_reader}}/by-level-for-deposits/maturity-date?&order=desc&top_k=3 +``` + +### Bridging Synchronous and Asynchronous Components In DriveDeposits + +DriveDeposits is a cutting-edge financial calculation platform built on a robust architecture that combines synchronous +and asynchronous components. The synchronous gRPC server utilizes Tokio to asynchronously send events to the EventBridge +service, seamlessly integrating with the asynchronous components of the system. +The gRPC server spawns asynchronous Tokio tasks to send events to +EventBridge, bridging the synchronous and asynchronous parts of the architecture. + +Lambda Reader with Axum Endpoints component serves as a serverless API built with AWS Lambda and the Axum web framework +in Rust. It provides a set of endpoints to query and retrieve financial data from a DynamoDB table. The endpoints are +designed to fetch data at different levels of granularity, such as portfolios, banks, and deposits, based on specific +criteria like delta growth or maturity date. + +The main functionalities include: + +- **Portfolio Level Endpoint**: Retrieves a list of portfolios based on the delta growth criteria. +- **Bank Level Endpoint**: Fetches a list of banks for a given portfolio UUID, sorted by the delta growth criteria. +- **Deposit Level Endpoints**: + - **Delta Growth**: Retrieves a list of deposits for a given portfolio UUID, sorted by the delta growth criteria. + - **Maturity Date**: Fetches a list of deposits for a given portfolio UUID, sorted by the maturity date. + +The Lambda function interacts with an AWS DynamoDB table to read and query the required data. It utilizes +the `aws-sdk-rust` crate to communicate with the DynamoDB service. The Axum web framework is used to define the API +routes and handle HTTP requests and responses. + +### AWS EventBridge, EventBus and Targets include Cloudwatch Log group and Lambda Deployment + +This project utilizes the [Justfile](https://github.com/casey/just) for managing project-level tasks. + +#### deploy everything -- aws deployment related commands for EventBridge, EventBus, Cloudwatch log groups and Lambda target function for writing to DynamoDB and Lambda DynamoDB reader + +`just deploy-drive-deposits-dynamodb-queries` + +calls dependent recipes - deploys [event bus and then event rules with lambda target] and then lambda function for +queries + +#### delete everything -- aws deployment related commands for EventBridge, EventBus, Cloudwatch log groups and Lambda target function for writing to DynamoDB and Lambda DynamoDB reader + +`just deployed-delete-drive-deposits-event-bus` + +calls dependent recipes - deletes [event rule with lambda target and then event bus] and then queries from dependent +recipes + +#### create aws deployment related commands for EventBridge, EventBus, Cloudwatch log groups and Lambda Write DynamoDB function + +`just deploy-drive-deposits-event-rules` + +calls dependent recipe and also creates event bus from dependent recipe + +#### AWS Deployment for Lambda Function with API Gateway and Lambda DynamoDB Reader + +`just deploy-drive-deposits-dynamodb-queries-only` + +#### Run REST gateway and gRPC Servers + +`just run-drive-deposits-grpc-server` + +`just run-drive-deposits-rest-grpc-gateway-server` + +#### Send http request to rest gateway server + +This sends the full Portfolio request body to the REST gateway server that sends the request to the gRPC server. The +gRPC server then performs calculations and sends calculation events to AWS EventBridge with targets for log groups, and +a Lambda function connected to DynamoDB. + +`just post-calculate-portfolio-valid` + +#### Command for AWS Lambda Invoke Check Directly + +`just aws-invoke-drive-deposits-event-rules-lambda` + +### Hybrid Integration Testing Tool + +#### Efficient Command for Synchronous Calculation Flow and Asynchronous AWS Serverless Event Testing + +The command drive-deposits-check-cmd is a powerful hybrid integration testing tool that bridges both synchronous and +asynchronous aspects of the system. It efficiently mimics the synchronous flow of REST and gRPC servers while +interacting with asynchronous EventBridge components, all without the need for full server deployment. +Key features: + +* Performs identical type transformations as REST and gRPC servers +* Enables rapid calculation validation and event routing verification +* Sends calculations to EventBridge for comprehensive sanity testing +* Validates event routing to appropriate destinations (log groups, AWS Lambda functions, DynamoDB) +* Allows developers to verify end-to-end flow of calculations, event handling, and data persistence + Execute the tool with: + `just run-drive-deposits-check-cmd-valid-send-events` + This streamlined approach significantly enhances development efficiency and system reliability testing. + +###### Alias + +.cargo/config.toml has alias for command line [drive-deposits-check-cmd](drive-deposits-check-cmd) so can be run using +`cargo ddcheck` For help see `cargo ddcheck -- --help` + +### Development Tool: LocalStack + +Localstack, being an ephemeral service, can be started and stopped as needed. This means that unless the state is +persisted, LocalStack provides a clean slate every time it's restarted. This feature can expedite the development and +deployment process as it eliminates the need to manually delete resources. + +Following is convenience so that in development can iterate faster: + +`just localstack-start` + +Should see "Ready." -- There is a Terminal now in Docker Desktop itself so that is a good place to run this command. + +#### deploy everything in localstack -- aws deployment related commands for EventBridge, EventBus, Cloudwatch log groups and Lambda target function for writing to DynamoDB and Lambda DynamoDB reader + +`just localstack-deploy-drive-deposits-dynamodb-queries` + +### Data population and Querying + +#### AWS Populated with Basic Data for Queries Lambda + +##### Using REST gateway and gRPC Servers: start + +`just run-drive-deposits-grpc-server` + +`just run-drive-deposits-rest-grpc-gateway-server` + +##### send curl post requests to populate + +`just post-calculate-portfolio-valid` + +`just post-calculate-portfolio-valid-lesser-amount` + +`just post-calculate-portfolio-valid-greater-amount` + +##### Alternatively Using check command without servers + +`just run-drive-deposits-check-cmd-valid-send-events` + +##### For actual AWS lambda query request + +`just get-query-by-portfolios-level` + +#### LocalStack populated with data + +Sanity check with LocalStack/ Working with queries lambda directly with cargo lambda and + +`just localstack-start` + +`just localstack-deploy-drive-deposits-event-rules` + +Populate with +`just localstack-run-drive-deposits-check-cmd-valid-send-events` +and then +`just localstack-run-drive-deposits-check-cmd-valid-send-events-lesser-amount-investments` +and then +`just localstack-run-drive-deposits-check-cmd-valid-send-events-greater-amount-investments` +and can also repeat these commands + +There is also a watch command that can be used to watch for changes in the code and automatically run the check command +`just localstack-watch-run-drive-deposits-check-cmd-valid-send-events` + +For queries lambda; replace table name in localstack in +cargo-lambda-watch-drive-deposits-lambda-dynamodb-reader-localstack-for-dynamodb + +Then can use cargo-lambda with dynamodb in localstack already populated with data: +`just cargo-lambda-watch-drive-deposits-lambda-dynamodb-reader-localstack-for-dynamodb` + +And for actual cargo lambda apigw events +`just cargo-lambda-invoke-drive-deposits-lambda-dynamodb-reader-apigw-event-query-for-portfolios` + +Replace proper portfolio uuid + +`just cargo-lambda-invoke-drive-deposits-lambda-dynamodb-reader-apigw-event-query-for-banks` + +`just cargo-lambda-invoke-drive-deposits-lambda-dynamodb-reader-apigw-event-query-for-deposits` + +##### command for awslocal localstack lambda invoke check directly + +Following is convenience so that in development can iterate faster: + +`just awslocal-invoke-drive-deposits-event-rules-lambda` + +### Development Tool: cargo lambda + +Following is convenience so that in development can iterate faster when skipping sam resources: + +for build watching lambda without using sam +`just cargo-lambda-build-watching-drive-deposits-event-rules-lambda` + +this is different from cargo lambda watch used with invoke +`just cargo-lambda-watch-drive-deposits-event-rules-lambda` + +#### command for cargo lambda invoke check directly + +`just cargo-lambda-invoke-drive-deposits-event-rules-lambda` + +### Clean + +cargo clean is used but since there are lambda we have .aws-sam folders created by sam also that we have a clean + +so we have +`just clean-with-lambdas` + +for quick build only that can be used after cleaning +`just build-with-lambdas` + +### Configurations for DriveDeposits + +The project uses custom configurations defined in `.cargo/config.toml`: + +- `SEND_CAL_EVENTS`: This environment variable is set to "true" by default in the config file. It can be overridden in + specific commands as needed. +- `USE_LOCALSTACK`: This environment variable is set to "false" by default in the config file. It can be overridden for + local development with LocalStack. +- Alias: The project includes an alias for the `drive-deposits-check-cmd`. It can be run using `cargo ddcheck`. For + help, use `cargo ddcheck -- --help`. + +These configurations allow for flexible development and testing environments, enabling easy switching between local and +AWS deployments. + +### Member crates in workspace + +See cargo workspace members: +[Cargo.toml](Cargo.toml) + +#### Naming: why keeping prefix drive-deposits + +Using the "drive-deposits-" prefix for crate names clearly distinguishes these as separate crates within +the workspace, not just modules within a single crate. This distinction is crucial for understanding the project +structure and for managing dependencies. It also allows for more flexibility in terms of versioning and publishing each +crate independently if needed. This naming convention effectively communicates the relationship between the crates while +maintaining their individual identities within the Rust ecosystem. + +#### Summary of the Responsibilities for crates drive-deposits-logs-lambda-target and drive-deposits-lambda-dynamodb-reader in Workspace + +###### drive-deposits-logs-lambda-target: + +* Triggered by EventBridge +* Handles log groups based on event rules +* Writes data to DynamoDB +* Error handling when adding items in DynamoDB indicates the source of the error and the level context in which it + occurred. +* Error logs can be seen in CloudWatch Logs + +###### drive-deposits-lambda-dynamodb-reader: + +* Responsible for querying data from DynamoDB +* Uses Axum in Lambda +* Exposed through an API Gateway +* Error handling when reading items from DynamoDB and creating Response for Query requests indicates the source of the + error and the level context in which it occurred. +* Error logs can be seen in CloudWatch Logs + +[Copyright (c) 2024 Rohit Sachdeva](LICENSE) diff --git a/drive-deposits-cal-types/Cargo.toml b/drive-deposits-cal-types/Cargo.toml new file mode 100644 index 0000000..3d2d7b6 --- /dev/null +++ b/drive-deposits-cal-types/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "drive-deposits-cal-types" +version = "0.10.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0.203", features = ["derive"] } +serde_json = "1.0.117" +tracing = "0.1.40" +chrono = { version = "0.4.38", features = ["serde"] } +chrono-tz = { version = "0.9.0", features = ["serde"] } +rust_decimal = { version = "1.35.0", features = ["maths"] } +rust_decimal_macros = "1.34.2" +uuid = { version = "1.9.1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } +once_cell = "1.19.0" +thiserror = "1.0.61" +tokio = { version = "1.38.0", features = ["full"] } +heck = "0.5.0" +# workspace member depdenencies +# proto generated dependency here the drive-deposits-proto-grpc-types is still package +# name so with dashes +drive-deposits-proto-grpc-types = { path = "../drive-deposits-proto-grpc-types" } +drive-deposits-event-source = { path = "../drive-deposits-event-source" } + +[dev-dependencies] +pretty_assertions = "1.4.0" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +anyhow = "1.0.86" \ No newline at end of file diff --git a/drive-deposits-cal-types/src/cal_types.rs b/drive-deposits-cal-types/src/cal_types.rs new file mode 100644 index 0000000..a837b62 --- /dev/null +++ b/drive-deposits-cal-types/src/cal_types.rs @@ -0,0 +1,99 @@ +use chrono::NaiveDate; +use chrono_tz::Tz; +use rust_decimal::Decimal; +use uuid::Uuid; + +use drive_deposits_proto_grpc_types::generated::{AccountType, PeriodUnit}; + +// Request sections +#[derive(Debug)] +pub struct PortfolioRequest { + pub new_banks: Vec, + pub new_delta: NewDelta, +} + +#[derive(Debug, Clone)] +pub struct NewDelta { + pub period: Decimal, + pub period_unit: PeriodUnit, +} + +#[derive(Debug)] +pub struct NewBank { + pub name: String, + pub bank_tz: Tz, + pub new_deposits: Vec, +} + +#[derive(Debug)] +pub struct NewDeposit { + pub account: String, + pub account_type: AccountType, + pub apy: Decimal, + pub years: Decimal, + pub amount: Decimal, + pub start_date_in_bank_tz: NaiveDate, +} + +// Response sections +#[derive(Debug, Clone)] +pub struct PortfolioResponse { + pub uuid: Uuid, + pub banks: Vec, + pub outcome: Option, + pub created_at: String, +} + +#[derive(Debug, Clone)] +pub struct Delta { + pub period: Decimal, + pub period_unit: PeriodUnit, + pub growth: Decimal, +} + +#[derive(Debug, Clone)] +pub struct Maturity { + pub amount: Decimal, + pub interest: Decimal, + pub total: Decimal, +} + +#[derive(Debug, Clone)] +pub struct Bank { + pub uuid: Uuid, + pub name: String, + pub bank_tz: Tz, + pub deposits: Vec, + pub outcome: Option, +} + +#[derive(Debug, Clone)] +pub struct Deposit { + pub uuid: Uuid, + pub account: String, + pub account_type: AccountType, + pub apy: Decimal, + pub years: Decimal, + pub outcome: Option, + pub outcome_with_dates: Option, +} + +#[derive(Debug, Clone)] +pub struct Outcome { + pub delta: Option, + pub maturity: Option, + pub errors: Vec, +} + +#[derive(Debug, Clone)] +pub struct OutcomeWithDates { + pub start_date_in_bank_tz: NaiveDate, + pub maturity_date_in_bank_tz: Option, + pub errors: Vec, +} + +#[derive(Debug, Clone)] +pub struct ProcessingError { + pub uuid: Uuid, + pub message: String, +} diff --git a/drive-deposits-cal-types/src/convert.rs b/drive-deposits-cal-types/src/convert.rs new file mode 100644 index 0000000..43fef19 --- /dev/null +++ b/drive-deposits-cal-types/src/convert.rs @@ -0,0 +1,3 @@ +mod from_cal_event_source; +pub mod from_cal_grpc_response; +pub mod from_grpc_cal_request; diff --git a/drive-deposits-cal-types/src/convert/from_cal_event_source.rs b/drive-deposits-cal-types/src/convert/from_cal_event_source.rs new file mode 100644 index 0000000..97ce7e3 --- /dev/null +++ b/drive-deposits-cal-types/src/convert/from_cal_event_source.rs @@ -0,0 +1,100 @@ +use heck::ToUpperCamelCase; + +use drive_deposits_event_source::payload_types::{ + Bank as EventSourceBank, CalculatePortfolioResponse as EventSourceCalculatePortfolioResponse, + Delta as EventSourceDelta, Deposit as EventSourceDeposit, Maturity as EventSourceMaturity, + Outcome as EventSourceOutcome, OutcomeWithDates as EventSourceOutcomeWithDates, + ProcessingError as EventSourceProcessingError, +}; + +use crate::cal_types::{ + Bank as CalBank, Delta as CalDelta, Deposit as CalDeposit, Maturity as CalMaturity, + Outcome as CalOutcome, OutcomeWithDates as CalOutcomeWithDates, + PortfolioResponse as CalBankResponse, ProcessingError as cal_ProcessingError, +}; + +impl From for EventSourceProcessingError { + fn from(cal: cal_ProcessingError) -> Self { + Self { + uuid: cal.uuid.to_string(), + message: cal.message, + } + } +} + +impl From for EventSourceMaturity { + fn from(cal: CalMaturity) -> Self { + Self { + amount: cal.amount.to_string(), + interest: cal.interest.to_string(), + total: cal.total.to_string(), + } + } +} +impl From for EventSourceDelta { + fn from(cal: CalDelta) -> Self { + Self { + period: cal.period.to_string(), + period_unit: cal.period_unit.as_str_name().to_upper_camel_case(), + growth: cal.growth.to_string(), + } + } +} + +impl From for EventSourceOutcomeWithDates { + fn from(cal: CalOutcomeWithDates) -> Self { + Self { + start_date_in_bank_tz: cal.start_date_in_bank_tz.to_string(), + maturity_date_in_bank_tz: cal.maturity_date_in_bank_tz.map(|x| x.to_string()), + errors: cal.errors.into_iter().map(|x| x.into()).collect(), + } + } +} +impl From for EventSourceOutcome { + fn from(cal: CalOutcome) -> Self { + Self { + delta: cal.delta.map(|x| x.into()), + maturity: cal.maturity.map(|x| x.into()), + errors: cal.errors.into_iter().map(|x| x.into()).collect(), + } + } +} + +impl From for EventSourceDeposit { + fn from(cal: CalDeposit) -> Self { + Self { + uuid: cal.uuid.to_string(), + account: cal.account, + account_type: cal.account_type.as_str_name().to_upper_camel_case(), + apy: cal.apy.to_string(), + years: cal.years.to_string(), + outcome: cal.outcome.map(|cal_outcome| cal_outcome.into()), + outcome_with_dates: cal + .outcome_with_dates + .map(|cal_outcome_with_dates| cal_outcome_with_dates.into()), + } + } +} + +impl From for EventSourceBank { + fn from(cal: CalBank) -> Self { + Self { + uuid: cal.uuid.to_string(), + name: cal.name, + bank_tz: cal.bank_tz.to_string(), + deposits: cal.deposits.into_iter().map(|x| x.into()).collect(), + outcome: cal.outcome.map(|x| x.into()), + } + } +} + +impl From for EventSourceCalculatePortfolioResponse { + fn from(cal: CalBankResponse) -> Self { + Self { + banks: cal.banks.into_iter().map(|x| x.into()).collect(), + uuid: cal.uuid.to_string(), + outcome: cal.outcome.map(|x| x.into()), + created_at: cal.created_at, + } + } +} diff --git a/drive-deposits-cal-types/src/convert/from_cal_grpc_response.rs b/drive-deposits-cal-types/src/convert/from_cal_grpc_response.rs new file mode 100644 index 0000000..44131db --- /dev/null +++ b/drive-deposits-cal-types/src/convert/from_cal_grpc_response.rs @@ -0,0 +1,103 @@ +use drive_deposits_proto_grpc_types::generated::{ + Bank as GrpcBank, CalculatePortfolioResponse as GrpcCalculatePortfolioResponse, + Delta as GrpcDelta, Deposit as GrpcDeposit, Maturity as GrpcMaturity, Outcome as GrpcOutcome, + OutcomeWithDates as GrpcOutcomeWithDates, ProcessingError as GrpcProcessingError, +}; + +use crate::cal_types::{ + Bank as CalBank, Delta as CalDelta, Deposit as CalDeposit, Maturity as CalMaturity, + Outcome as CalOutcome, OutcomeWithDates as CalOutcomeWithDates, + PortfolioResponse as CalBankResponse, ProcessingError as cal_ProcessingError, +}; + +impl From for GrpcProcessingError { + fn from(cal: cal_ProcessingError) -> Self { + Self { + uuid: cal.uuid.to_string(), + message: cal.message, + } + } +} + +impl From for GrpcMaturity { + fn from(cal: CalMaturity) -> Self { + Self { + amount: cal.amount.to_string(), + interest: cal.interest.to_string(), + total: cal.total.to_string(), + } + } +} +impl From for GrpcDelta { + fn from(cal: CalDelta) -> Self { + Self { + period: cal.period.to_string(), + period_unit: cal.period_unit as i32, + growth: cal.growth.to_string(), + } + } +} + +impl From for GrpcOutcomeWithDates { + fn from(cal: CalOutcomeWithDates) -> Self { + Self { + start_date_in_bank_tz: cal.start_date_in_bank_tz.to_string(), + maturity_date_in_bank_tz: cal.maturity_date_in_bank_tz.map(|x| x.to_string()), + errors: cal.errors.into_iter().map(|x| x.into()).collect(), + } + } +} +impl From for GrpcOutcome { + fn from(cal: CalOutcome) -> Self { + Self { + delta: cal.delta.map(|x| x.into()), + maturity: cal.maturity.map(|x| x.into()), + errors: cal.errors.into_iter().map(|x| x.into()).collect(), + } + } +} +// Orphan rule helps not to implement very generic From> for String unless wrap in our own types using new type pattern +// impl From> for String { +// fn from(date: Option) -> Self { +// date.map(|date| date.to_string()) +// .unwrap_or("None".to_string()) +// } +// } +impl From for GrpcDeposit { + fn from(cal: CalDeposit) -> Self { + Self { + uuid: cal.uuid.to_string(), + account: cal.account, + account_type: cal.account_type as i32, + apy: cal.apy.to_string(), + years: cal.years.to_string(), + outcome: cal.outcome.map(|cal_outcome| cal_outcome.into()), + outcome_with_dates: cal + .outcome_with_dates + .map(|cal_outcome_with_dates| cal_outcome_with_dates.into()), + } + } +} + +impl From for GrpcBank { + fn from(cal: CalBank) -> Self { + Self { + uuid: cal.uuid.to_string(), + name: cal.name, + bank_tz: cal.bank_tz.to_string(), + deposits: cal.deposits.into_iter().map(|x| x.into()).collect(), + outcome: cal.outcome.map(|x| x.into()), + } + } +} + +impl From for GrpcCalculatePortfolioResponse { + fn from(cal: CalBankResponse) -> Self { + Self { + banks: cal.banks.into_iter().map(|x| x.into()).collect(), + uuid: cal.uuid.to_string(), + outcome: cal.outcome.map(|x| x.into()), + created_at: cal.created_at, + } + } +} diff --git a/drive-deposits-cal-types/src/convert/from_grpc_cal_request.rs b/drive-deposits-cal-types/src/convert/from_grpc_cal_request.rs new file mode 100644 index 0000000..95328c4 --- /dev/null +++ b/drive-deposits-cal-types/src/convert/from_grpc_cal_request.rs @@ -0,0 +1,58 @@ +use chrono::NaiveDate; +use rust_decimal::Decimal; + +use drive_deposits_proto_grpc_types::generated::{ + AccountType as GrpcAccountType, CalculatePortfolioRequest as GrpcCalculatePortfolioRequest, + NewBank as GrpcNewBank, NewDelta as GrpcNewDelta, NewDeposit as GrpcNewDeposit, + PeriodUnit as GrpcPeriodUnit, +}; + +use crate::cal_types::{ + NewBank as CalNewBank, NewDelta as CalNewDelta, NewDeposit as CalNewDeposit, + PortfolioRequest as CalBankRequest, +}; + +impl From for CalNewDeposit { + fn from(grpc: GrpcNewDeposit) -> Self { + Self { + account: grpc.account, + account_type: GrpcAccountType::try_from(grpc.account_type).unwrap_or_default(), + apy: grpc.apy.parse::().unwrap_or_default(), + years: grpc.years.parse::().unwrap_or_default(), + amount: grpc.amount.parse::().unwrap_or_default(), + start_date_in_bank_tz: NaiveDate::parse_from_str( + &grpc.start_date_in_bank_tz, + "%Y-%m-%d", + ) + .unwrap_or_default(), + } + } +} + +impl From for CalNewBank { + fn from(grpc: GrpcNewBank) -> Self { + Self { + name: grpc.name, + bank_tz: grpc.bank_tz.parse().unwrap_or_default(), + new_deposits: grpc.new_deposits.into_iter().map(|x| x.into()).collect(), + } + } +} + +impl From for CalNewDelta { + fn from(grpc: GrpcNewDelta) -> Self { + Self { + period: grpc.period.parse::().unwrap_or_default(), + period_unit: GrpcPeriodUnit::try_from(grpc.period_unit).unwrap_or_default(), + } + } +} + +impl From for CalBankRequest { + fn from(grpc: GrpcCalculatePortfolioRequest) -> Self { + Self { + new_banks: grpc.new_banks.into_iter().map(|x| x.into()).collect(), + new_delta: grpc.new_delta.unwrap_or_default().into(), + } + } +} diff --git a/drive-deposits-cal-types/src/lib.rs b/drive-deposits-cal-types/src/lib.rs new file mode 100644 index 0000000..b4e919e --- /dev/null +++ b/drive-deposits-cal-types/src/lib.rs @@ -0,0 +1,3 @@ +pub mod cal_types; +pub mod convert; +pub mod math; diff --git a/drive-deposits-cal-types/src/math.rs b/drive-deposits-cal-types/src/math.rs new file mode 100644 index 0000000..8798fc6 --- /dev/null +++ b/drive-deposits-cal-types/src/math.rs @@ -0,0 +1,9 @@ +pub mod accumulator; +pub mod compound_interest; +pub mod engine; +pub mod growth; +pub mod individual_calculation_error; +pub mod maturity_date; +pub mod outcome; +pub mod simple_interest; +pub mod total; diff --git a/drive-deposits-cal-types/src/math/accumulator.rs b/drive-deposits-cal-types/src/math/accumulator.rs new file mode 100644 index 0000000..ea0b939 --- /dev/null +++ b/drive-deposits-cal-types/src/math/accumulator.rs @@ -0,0 +1,89 @@ +use rust_decimal::Decimal; +use thiserror::Error; +use uuid::Uuid; + +use crate::cal_types::{Bank, Deposit, ProcessingError}; + +#[derive(Default, Debug, Error)] +pub enum AccumulatorError { + #[default] + #[error("Missing outcome in deposit")] + MissingOutcome, + #[error("Missing delta in deposit")] + MissingDelta, + #[error("Missing maturity in deposit")] + MissingMaturity, +} + +impl From for ProcessingError { + fn from(error: AccumulatorError) -> Self { + Self { + uuid: Uuid::new_v4(), + message: error.to_string(), + } + } +} + +#[derive(Debug, Default)] +pub struct Accumulator { + pub(crate) growth: Decimal, + pub(crate) amount: Decimal, + pub(crate) interest: Decimal, + pub(crate) total: Decimal, +} + +pub fn accumulate_deposits(deposits: &[Deposit]) -> Result { + deposits + .iter() + .try_fold(Accumulator::default(), |acc, deposit| { + let deposit = deposit + .outcome + .as_ref() + .ok_or(AccumulatorError::MissingOutcome)?; + let deposit_maturity = deposit + .maturity + .as_ref() + .ok_or(AccumulatorError::MissingMaturity)?; + let deposit_delta = deposit + .delta + .as_ref() + .ok_or(AccumulatorError::MissingDelta)?; + + let deposit_growth = deposit_delta.growth; + let deposit_amount = deposit_maturity.amount; + let deposit_interest = deposit_maturity.interest; + let deposit_total = deposit_maturity.total; + + Ok(Accumulator { + growth: acc.growth + deposit_growth, + amount: acc.amount + deposit_amount, + interest: acc.interest + deposit_interest, + total: acc.total + deposit_total, + }) + }) +} + +pub fn accumulate_banks(banks: &[Bank]) -> Result { + banks.iter().try_fold(Accumulator::default(), |acc, bank| { + let bank = bank + .outcome + .as_ref() + .ok_or(AccumulatorError::MissingOutcome)?; + let bank_maturity = bank + .maturity + .as_ref() + .ok_or(AccumulatorError::MissingMaturity)?; + let bank_delta = bank.delta.as_ref().ok_or(AccumulatorError::MissingDelta)?; + + let bank_growth = bank_delta.growth; + let bank_amount = bank_maturity.amount; + let bank_interest = bank_maturity.interest; + let bank_total = bank_maturity.total; + Ok(Accumulator { + growth: acc.growth + bank_growth, + amount: acc.amount + bank_amount, + interest: acc.interest + bank_interest, + total: acc.total + bank_total, + }) + }) +} diff --git a/drive-deposits-cal-types/src/math/compound_interest.rs b/drive-deposits-cal-types/src/math/compound_interest.rs new file mode 100644 index 0000000..0da350a --- /dev/null +++ b/drive-deposits-cal-types/src/math/compound_interest.rs @@ -0,0 +1,41 @@ +use rust_decimal::{Decimal, MathematicalOps}; +use rust_decimal_macros::dec; +use tracing::{debug, instrument}; + +use crate::cal_types::NewDeposit; + +/// Computes the compound math for a deposit. +/// +/// Compound math is calculated using the formula: +/// +/// A = P(1 + r/n)^(nt) +/// +/// Where: +/// P is the principal amount (initial deposit) +/// r is the annual math rate (in decimal form, e.g., 5% is 0.05) +/// n is the number of times that the math is compounded per year (In this case it is 1) +/// t is the number of years the money is invested for +/// +/// The order of operations is as follows: +/// First compute `r/n` (math rate divided by number of times compounded per year) +/// Second, add 1 to the result +/// Third, raise the resulting sum to the power of `n*t` (n times t) +/// Finally, multiply the result with P (principal). So the P times happens last in the calculation. +#[instrument] +pub fn compute_interest(deposit: &NewDeposit) -> Decimal { + debug!("Calculating compound math for deposit: {:?}", deposit); + let principal = deposit.amount; + let rate = deposit.apy / dec!(100); + let years = deposit.years; + + //Assuming the math is compounded annually + let n = dec!(1); + + let total_amount = principal * ((dec!(1) + rate / n).powd(n * years)); + + // Subtract the principal to get only the math + let interest = total_amount - principal; + + debug!("Compound math overall: {}", interest); + interest.round_dp(2) +} diff --git a/drive-deposits-cal-types/src/math/engine.rs b/drive-deposits-cal-types/src/math/engine.rs new file mode 100644 index 0000000..bf7723c --- /dev/null +++ b/drive-deposits-cal-types/src/math/engine.rs @@ -0,0 +1,200 @@ +use std::sync::Arc; + +use chrono::SecondsFormat; +use serde_json::to_string; +use thiserror::Error; +use tokio::task::{spawn_blocking, JoinSet}; +use tracing::{debug, debug_span, info, instrument, Instrument, Span}; +use uuid::Uuid; + +use drive_deposits_event_source::{ + eb::{ + send_bank_level_event_to_event_bridge, send_portfolio_level_event_to_event_bridge, + DriveDepositsEventBridge, DriveDepositsEventBridgeError, + }, + payload_types::{ + Bank as EventSourceBank, + CalculatePortfolioResponse as EventSourceCalculatePortfolioResponse, + }, +}; + +use crate::cal_types::{ + Bank, Deposit, NewBank, NewDelta, NewDeposit, PortfolioRequest, PortfolioResponse, +}; +use crate::math::outcome::{ + build_outcome_from_banks, build_outcome_from_deposits, build_outcome_from_new_deposit, + build_outcome_with_dates_from_new_deposit, +}; + +#[derive(Default, Debug, Error)] +pub enum CalculationHaltError { + #[default] + #[error("Internal error All Calculations could not proceed")] + Internal, + + #[error("Join error all calculations could not proceed: {0}")] + Join(#[from] tokio::task::JoinError), + + #[error("Drive Deposits SEND_CAL_EVENTS is true but could not send events for processing as desired: {0}")] + DriveDepositsEventBridgeError(#[from] DriveDepositsEventBridgeError), + + #[error("Drive Deposits SEND_CAL_EVENTS is true but could not serialize events for sending as desired: {0}")] + EventSourceJsonSerializationError(#[from] serde_json::Error), +} + +// same from style not directly using though since more complex with async function + +fn build_from_new_deposit( + new_deposit: NewDeposit, + new_delta: Arc, +) -> Result { + let outcome_with_dates = build_outcome_with_dates_from_new_deposit(&new_deposit); + let outcome = build_outcome_from_new_deposit(&new_deposit, new_delta.as_ref()); + let deposit = Deposit { + uuid: Uuid::new_v4(), + account: new_deposit.account, + account_type: new_deposit.account_type, + apy: new_deposit.apy, + years: new_deposit.years, + outcome_with_dates, + outcome, + }; + Ok(deposit) +} + +fn build_from_new_deposits( + new_deposits: Vec, + new_delta: Arc, +) -> Result, CalculationHaltError> { + let mut deposits = vec![]; + for new_deposit in new_deposits { + let deposit = build_from_new_deposit(new_deposit, new_delta.clone())?; + debug!( + "build_from_new_deposits calling build_from_new_deposit Deposit: {:?}", + deposit + ); + deposits.push(deposit); + } + Ok(deposits) +} +async fn build_from_new_bank( + new_bank: NewBank, + new_delta: Arc, + eb: Arc>, +) -> Result { + // using spawn blocking for synchronous calculation code + let bank_with_outcome = spawn_blocking(move || -> Result { + info!("task spawned for new_bank: {:?}", new_bank.name); + let deposits = build_from_new_deposits(new_bank.new_deposits, new_delta.clone())?; + let outcome = build_outcome_from_deposits(&deposits, new_delta.clone().as_ref()); + let bank = Bank { + uuid: Uuid::new_v4(), + name: new_bank.name, + bank_tz: new_bank.bank_tz, + deposits, + outcome, + }; + Ok(bank) + }) + .await??; + + // this method is executed in joinset spawn task already + if eb.is_some() { + debug!("send event to event bridge at the bank level"); + let event_source_bank: EventSourceBank = bank_with_outcome.clone().into(); + let event_source_bank_json = to_string(&event_source_bank)?; + let eb_access = eb.as_ref().as_ref(); + + send_bank_level_event_to_event_bridge(eb_access, event_source_bank_json).await? + } + + Ok(bank_with_outcome) +} + +async fn build_from_new_banks( + new_banks: Vec, + new_delta: Arc, + eb: Arc>, +) -> Result, CalculationHaltError> { + let mut banks: Vec = Vec::new(); + let mut join_set = JoinSet::new(); + + for new_bank in new_banks { + // Correctly create a new span with the bank name + let bank_span = debug_span!(parent: &Span::current(), "bank_level_spawned_task_for_processing_all_deposits", bank_name = %new_bank.name); + let delta_clone = new_delta.clone(); + let eb_clone = eb.clone(); + join_set.spawn( + async move { + info!("task spawned for new_bank: {:?}", new_bank.name); + let bank = build_from_new_bank(new_bank, delta_clone, eb_clone); + bank.await + } + .instrument(bank_span), + ); + } + + while let Some(res) = join_set.join_next().await { + let bank = res??; + banks.push(bank); + } + + Ok(banks) +} + +async fn build_from_bank_request( + bank_req: PortfolioRequest, + eb: Arc>, +) -> Result { + let uuid = Uuid::new_v4(); + info!("build_from_bank_request uuid created: {:?}", uuid); + let created_at = chrono::Utc::now(); + let created_at_iso8061 = created_at.to_rfc3339_opts(SecondsFormat::Micros, true); + let eb_clone = eb.clone(); + let new_delta = Arc::new(bank_req.new_delta); + let banks = build_from_new_banks(bank_req.new_banks, new_delta.clone(), eb).await?; + let outcome = build_outcome_from_banks(&banks, new_delta.clone().as_ref()); + + let bank_response = PortfolioResponse { + uuid, + banks, + outcome, + created_at: created_at_iso8061, + }; + + if eb_clone.is_some() { + debug!("send event to event bridge at the banks level"); + let event_source_response: EventSourceCalculatePortfolioResponse = + bank_response.clone().into(); + let event_source_response_json = to_string(&event_source_response)?; + + let handle = tokio::spawn(async move { + send_portfolio_level_event_to_event_bridge( + eb_clone.as_ref().as_ref(), + event_source_response_json, + ) + .await + }); + handle.await??; + } + + Ok(bank_response) +} + +#[instrument(skip(bank_req, eb))] +pub async fn calculate_portfolio( + bank_req: PortfolioRequest, + eb: Option, +) -> Result { + debug!( + "Starting calculation by period per BankRequest overall: {:?}", + bank_req + ); + if eb.is_none() { + info!("Drive Deposits SEND_CAL_EVENTS is false, so no events will be sent"); + } + let eb_access = Arc::new(eb); + let bank_resp = build_from_bank_request(bank_req, eb_access).await?; + + Ok(bank_resp) +} diff --git a/drive-deposits-cal-types/src/math/growth.rs b/drive-deposits-cal-types/src/math/growth.rs new file mode 100644 index 0000000..386ad11 --- /dev/null +++ b/drive-deposits-cal-types/src/math/growth.rs @@ -0,0 +1,37 @@ +use rust_decimal::Decimal; +use rust_decimal_macros::dec; +use tracing::debug; + +use drive_deposits_proto_grpc_types::generated::PeriodUnit; + +use crate::cal_types::{NewDelta, NewDeposit}; +use crate::math::individual_calculation_error::Error as IndividualCalculationError; + +pub fn compute( + deposit: &NewDeposit, + interest: Decimal, + delta: &NewDelta, +) -> Result { + if deposit.years.is_zero() { + return Err(IndividualCalculationError::ZeroYears( + "cannot calculate growth for zero years".to_string(), + )); + } + let interest_per_smallest_unit_day = interest / (dec!(365.0) * deposit.years); + debug!( + "interest_per_smallest_unit_day: {}", + interest_per_smallest_unit_day + ); + debug!("delta.period_unit is: {:?}", delta.period_unit); + debug!("delta.period: {}", delta.period); + let period_in_smallest_unit_days = match delta.period_unit { + PeriodUnit::Day => delta.period, + PeriodUnit::Week => delta.period * dec!(7.0), + PeriodUnit::Month => delta.period * dec!(30.0), + PeriodUnit::Year => delta.period * dec!(365.0), + _ => delta.period, + }; + let delta = (interest_per_smallest_unit_day * period_in_smallest_unit_days).round_dp(2); + debug!("delta: {}", delta); + Ok(delta) +} diff --git a/drive-deposits-cal-types/src/math/individual_calculation_error.rs b/drive-deposits-cal-types/src/math/individual_calculation_error.rs new file mode 100644 index 0000000..96874f7 --- /dev/null +++ b/drive-deposits-cal-types/src/math/individual_calculation_error.rs @@ -0,0 +1,34 @@ +use std::num::ParseIntError; + +use thiserror::Error; +use uuid::Uuid; + +use crate::cal_types::ProcessingError; + +#[derive(Default, Debug, Error)] +pub enum Error { + #[default] + #[error("Internal error Individual Calculation is incomplete")] + Internal, + + #[error("ParseInt error Individual Calculation is incomplete: {0}")] + ParseInt(#[from] ParseIntError), + + #[error("ZeroYears error Individual Calculation is incomplete: {0}")] + ZeroYears(String), +} + +impl From for ProcessingError { + fn from(error: Error) -> Self { + match error { + Error::ParseInt(e) => ProcessingError { + uuid: Uuid::new_v4(), + message: e.to_string(), + }, + _ => ProcessingError { + uuid: Uuid::new_v4(), + message: error.to_string(), + }, + } + } +} diff --git a/drive-deposits-cal-types/src/math/maturity_date.rs b/drive-deposits-cal-types/src/math/maturity_date.rs new file mode 100644 index 0000000..6515dcc --- /dev/null +++ b/drive-deposits-cal-types/src/math/maturity_date.rs @@ -0,0 +1,22 @@ +use chrono::NaiveDate; +use rust_decimal::Decimal; +use rust_decimal_macros::dec; +use tracing::debug; + +use crate::math::individual_calculation_error::Error as IndividualCalculationError; + +pub fn maturity_date( + start_date: NaiveDate, + years: Decimal, +) -> Result { + debug!("start_date: {:?}", start_date); + // round is basically self.round_dp(0) which rounds to the nearest integer + // follows "Bankers Rounding" rules. e.g. 6.5 -> 6, 7.5 -> 8 + let days = (years * dec!(365.0)).round().to_string(); + debug!("days in years rounded is: {:?}", days); + // convert string to int64 + let days = days.parse::()?; + let date = start_date + chrono::Duration::days(days); + debug!("maturity_date: {:?}", date); + Ok(date) +} diff --git a/drive-deposits-cal-types/src/math/outcome.rs b/drive-deposits-cal-types/src/math/outcome.rs new file mode 100644 index 0000000..a3db780 --- /dev/null +++ b/drive-deposits-cal-types/src/math/outcome.rs @@ -0,0 +1,149 @@ +use rust_decimal::Decimal; +use tracing::debug; +use uuid::Uuid; + +use drive_deposits_proto_grpc_types::generated::AccountType; + +use crate::{ + cal_types::{ + Bank, Delta, Deposit, Maturity, NewDelta, NewDeposit, Outcome, OutcomeWithDates, + ProcessingError, + }, + math::{ + accumulator::{accumulate_banks, accumulate_deposits}, + compound_interest::compute_interest as compute_compound_interest, + growth::compute as compute_growth, + maturity_date::maturity_date, + simple_interest::compute_interest as compute_simple_interest, + total::compute as compute_total, + }, +}; +use crate::math::accumulator::Accumulator; + +fn outcome_with_growth( + new_deposit: &NewDeposit, + new_delta: &NewDelta, + interest: Decimal, +) -> Option { + let total = compute_total(new_deposit.amount, interest); + let growth = compute_growth(new_deposit, interest, new_delta); + debug!("outcome_with_growth growth: {:?}", growth); + let outcome = growth.map_or_else( + |err| { + Some(Outcome { + delta: None, + maturity: None, + errors: vec![err.into()], + }) + }, + |growth| { + Some(Outcome { + delta: Some(Delta { + period: new_delta.period, + period_unit: new_delta.period_unit, + growth, + }), + maturity: Some(Maturity { + amount: new_deposit.amount, + interest, + total, + }), + errors: vec![], + }) + }, + ); + debug!("outcome_with_growth outcome: {:?}", outcome); + outcome +} +pub fn build_outcome_from_new_deposit( + new_deposit: &NewDeposit, + new_delta: &NewDelta, +) -> Option { + // at deposit level + // to resume work on this function, we need to implement the logic + match new_deposit.account_type { + AccountType::Checking | AccountType::Savings | AccountType::CertificateOfDeposit => { + let interest = compute_compound_interest(new_deposit); + outcome_with_growth(new_deposit, new_delta, interest) + } + AccountType::BrokerageCertificateOfDeposit => { + let interest = compute_simple_interest(new_deposit); + outcome_with_growth(new_deposit, new_delta, interest) + } + _ => { + Some(Outcome { + delta: None, + maturity: None, + errors: vec![ProcessingError { + uuid: Uuid::new_v4(), + message: format!( + "Unspecified account type...Error calculating outcome for account type {:?} deposit: {:?}", + new_deposit.account_type, new_deposit.account + ), + }], + }) + } + } +} + +pub fn build_outcome_with_dates_from_new_deposit( + new_deposit: &NewDeposit, +) -> Option { + maturity_date(new_deposit.start_date_in_bank_tz, new_deposit.years).map_or_else( + |err| { + Some(OutcomeWithDates { + start_date_in_bank_tz: new_deposit.start_date_in_bank_tz, + maturity_date_in_bank_tz: None, + errors: vec![err.into()], + }) + }, + |calculated_maturity_date| { + Some(OutcomeWithDates { + start_date_in_bank_tz: new_deposit.start_date_in_bank_tz, + maturity_date_in_bank_tz: Some(calculated_maturity_date), + errors: vec![], + }) + }, + ) +} + +fn outcome_from_accumulator(accumulator: Accumulator, new_delta: &NewDelta) -> Option { + Some(Outcome { + delta: Some(Delta { + period: new_delta.period, + period_unit: new_delta.period_unit, + growth: accumulator.growth, + }), + maturity: Some(Maturity { + amount: accumulator.amount, + interest: accumulator.interest, + total: accumulator.total, + }), + errors: vec![], + }) +} +pub fn build_outcome_from_deposits(deposits: &[Deposit], new_delta: &NewDelta) -> Option { + accumulate_deposits(deposits).map_or_else( + |err| { + Some(Outcome { + delta: None, + maturity: None, + errors: vec![err.into()], + }) + }, + |accumulator| outcome_from_accumulator(accumulator, new_delta), + ) +} + +pub fn build_outcome_from_banks(banks: &[Bank], new_delta: &NewDelta) -> Option { + accumulate_banks(banks).map_or_else( + |err| { + Some(Outcome { + delta: None, + maturity: None, + errors: vec![err.into()], + }) + }, + |accumulator| outcome_from_accumulator(accumulator, new_delta), + ) +} diff --git a/drive-deposits-cal-types/src/math/simple_interest.rs b/drive-deposits-cal-types/src/math/simple_interest.rs new file mode 100644 index 0000000..7e33a97 --- /dev/null +++ b/drive-deposits-cal-types/src/math/simple_interest.rs @@ -0,0 +1,35 @@ +use rust_decimal::Decimal; +use rust_decimal_macros::dec; +use tracing::{debug, instrument}; + +use crate::cal_types::NewDeposit; + +/// Computes the simple math for a deposit. +/// +/// Simple math is calculated using the formula: +/// +/// I = PRT +/// +/// Where: +/// P is the principal amount (initial amount) +/// R is the annual math rate (in decimal form, e.g., 5% is 0.05) +/// T is the time the money is invested for in years +#[instrument] +pub fn compute_interest(deposit: &NewDeposit) -> Decimal { + // The principal amount + let principal = deposit.amount; + + // The annual math rate in decimal + let rate = deposit.apy / dec!(100); + + // The time the money is invested for in years + let years = deposit.years; + + // Calculate simple math + let simple = principal * rate * years; + + debug!("simple math overall: {}", simple); + + // Return the calculated simple math + simple.round_dp(2) +} diff --git a/drive-deposits-cal-types/src/math/total.rs b/drive-deposits-cal-types/src/math/total.rs new file mode 100644 index 0000000..84e3c59 --- /dev/null +++ b/drive-deposits-cal-types/src/math/total.rs @@ -0,0 +1,5 @@ +use rust_decimal::Decimal; + +pub fn compute(initial_amount: Decimal, interest: Decimal) -> Decimal { + (initial_amount + interest).round_dp(2) +} diff --git a/drive-deposits-cal-types/tests/helper/enable_tracing.rs b/drive-deposits-cal-types/tests/helper/enable_tracing.rs new file mode 100644 index 0000000..62d16a0 --- /dev/null +++ b/drive-deposits-cal-types/tests/helper/enable_tracing.rs @@ -0,0 +1,31 @@ +use anyhow::Result; +use once_cell::sync::OnceCell; +use tracing::{debug, Span}; +use tracing_subscriber::{EnvFilter, registry}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +static INIT: OnceCell<()> = OnceCell::new(); + +pub fn setup_tracing_subscriber() -> Result<()> { + let rust_log = std::env::var("RUST_LOG")?; + println!("RUST_LOG is {}", rust_log); + // can change level to debug to see all debug messages in tests + // or to info not to see + // for test span added test=debug here + registry() + .with(EnvFilter::try_from_default_env()?.add_directive("test=debug".parse()?)) + .with(tracing_subscriber::fmt::layer()) + .init(); + debug!("tracing subscriber initialized"); + Ok(()) +} + +pub fn initialize_test_span(test_name: &str) -> Span { + INIT.get_or_init(|| { + setup_tracing_subscriber().expect("Failed to set up tracing subscriber"); + }); + debug!("Running the test: {}", test_name); + let span = tracing::info_span!("test", test_name = test_name); + span +} diff --git a/drive-deposits-cal-types/tests/helper/mod.rs b/drive-deposits-cal-types/tests/helper/mod.rs new file mode 100644 index 0000000..08503c3 --- /dev/null +++ b/drive-deposits-cal-types/tests/helper/mod.rs @@ -0,0 +1,3 @@ +pub mod enable_tracing; + +pub mod test_data; diff --git a/drive-deposits-cal-types/tests/helper/test_data.rs b/drive-deposits-cal-types/tests/helper/test_data.rs new file mode 100644 index 0000000..5c8f61f --- /dev/null +++ b/drive-deposits-cal-types/tests/helper/test_data.rs @@ -0,0 +1,5 @@ +use chrono::NaiveDate; + +pub fn naive_date_2023_11_23() -> NaiveDate { + NaiveDate::from_ymd_opt(2023, 11, 23).expect("unable to create NaiveDate from year, month, day") +} diff --git a/drive-deposits-cal-types/tests/test_calculate_portfolio.rs b/drive-deposits-cal-types/tests/test_calculate_portfolio.rs new file mode 100644 index 0000000..69e9994 --- /dev/null +++ b/drive-deposits-cal-types/tests/test_calculate_portfolio.rs @@ -0,0 +1,25 @@ +use tracing::{debug, Instrument}; + +use drive_deposits_cal_types::cal_types::NewDelta; +use drive_deposits_cal_types::cal_types::PortfolioRequest; +use drive_deposits_cal_types::math::engine::calculate_portfolio; +use drive_deposits_proto_grpc_types::generated::PeriodUnit; +use helper::enable_tracing::initialize_test_span; + +mod helper; +#[tokio::test] +async fn test_calculate_by_period_empty_no_new_banks_without_events() { + let span = initialize_test_span("test_calculate_by_period_empty_no_new_banks"); + + let bank_req = PortfolioRequest { + new_banks: vec![], + new_delta: NewDelta { + period: Default::default(), + period_unit: PeriodUnit::Day, + }, + }; + + // don't have to spawn a task necessarily or even async move since test is async already + let result = calculate_portfolio(bank_req, None).instrument(span).await; + debug!("finally result: {:?}", result); +} diff --git a/drive-deposits-cal-types/tests/test_compound_interest.rs b/drive-deposits-cal-types/tests/test_compound_interest.rs new file mode 100644 index 0000000..c6d0c87 --- /dev/null +++ b/drive-deposits-cal-types/tests/test_compound_interest.rs @@ -0,0 +1,169 @@ +use pretty_assertions::assert_eq; +use rust_decimal_macros::dec; +use tracing::instrument; + +use drive_deposits_cal_types::cal_types::NewDelta; +use drive_deposits_cal_types::cal_types::NewDeposit; +use drive_deposits_cal_types::math::{ + compound_interest::compute_interest, growth::compute as compute_growth, +}; +use drive_deposits_proto_grpc_types::generated::{AccountType, PeriodUnit}; +use helper::enable_tracing::initialize_test_span; +use helper::test_data::naive_date_2023_11_23; + +mod helper; + +#[test] +fn test_compound_interest_calculation_delta_years() { + initialize_test_span("test_compound_interest_calculation_delta_years").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::BrokerageCertificateOfDeposit, + apy: dec!(5.0), + years: dec!(2.0), + amount: dec!(1000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(1.0), + period_unit: PeriodUnit::Year, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(102.50)); + assert_eq!(delta_interest, dec!(51.25)); + }); +} + +#[test] +fn test_compound_interest_calculation_delta_several_years() { + initialize_test_span("test_compound_interest_calculation_delta_several_years").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::BrokerageCertificateOfDeposit, + apy: dec!(7.0), + years: dec!(5.0), + amount: dec!(5000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(15), + period_unit: PeriodUnit::Day, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(2012.76)); + assert_eq!(delta_interest, dec!(16.54)); + }); +} + +#[test] +fn test_compound_interest_calculation_delta_mid_level_decimal_interest() { + initialize_test_span("test_compound_interest_calculation_delta_mid_level_decimal_interest") + .in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::CertificateOfDeposit, + apy: dec!(3.4), + years: dec!(5.0), + amount: dec!(10000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(15), + period_unit: PeriodUnit::Day, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(1819.60)); + assert_eq!(delta_interest, dec!(14.96)); + }); +} + +#[test] +fn test_compound_interest_calculation_delta_months() { + initialize_test_span("test_compound_interest_calculation_delta_months").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::CertificateOfDeposit, + apy: dec!(5.0), + years: dec!(2.0), + amount: dec!(1000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(1.0), + period_unit: PeriodUnit::Month, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(102.50)); + assert_eq!(delta_interest, dec!(4.21)); + }); +} + +#[test] +#[instrument] +fn test_compound_interest_calculation_delta_weeks() { + initialize_test_span("test_compound_interest_calculation_delta_weeks").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::BrokerageCertificateOfDeposit, + apy: dec!(5.0), + years: dec!(2.0), + amount: dec!(1000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(2.0), + period_unit: PeriodUnit::Week, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(102.50)); + assert_eq!(delta_interest, dec!(1.97)); + }); +} + +#[test] +fn test_compound_interest_calculation_delta_days() { + initialize_test_span("test_compound_interest_calculation_delta_days").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::BrokerageCertificateOfDeposit, + apy: dec!(5.0), + years: dec!(2.0), + amount: dec!(1000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(30.0), + period_unit: PeriodUnit::Day, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(102.50)); + assert_eq!(delta_interest, dec!(4.21)); + }); +} + +#[test] +#[should_panic] +fn test_growth_with_compound_interest_zero_years() { + initialize_test_span("test_growth_with_compound_interest_zero_years").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::BrokerageCertificateOfDeposit, + apy: dec!(5.0), + years: dec!(0.0), + amount: dec!(1000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(30.0), + period_unit: PeriodUnit::Day, + }; + let interest = compute_interest(&deposit); + compute_growth(&deposit, interest, &delta).unwrap(); + }); +} diff --git a/drive-deposits-cal-types/tests/test_maturity_date.rs b/drive-deposits-cal-types/tests/test_maturity_date.rs new file mode 100644 index 0000000..bdfac44 --- /dev/null +++ b/drive-deposits-cal-types/tests/test_maturity_date.rs @@ -0,0 +1,31 @@ +use rust_decimal_macros::dec; + +use drive_deposits_cal_types::math::maturity_date::maturity_date; + +use crate::helper::enable_tracing::initialize_test_span; +use crate::helper::test_data::naive_date_2023_11_23; + +mod helper; +#[test] +fn test_maturity_date_after_one_and_a_half_years() { + initialize_test_span("test_maturity_date_after_5_days").in_scope(|| { + let start_date = naive_date_2023_11_23(); + let years = dec!(1.5); + let maturity = maturity_date(start_date, years); + assert!(maturity.is_ok()); + let maturity = maturity.unwrap().to_string(); + assert_eq!(maturity, "2025-05-24"); + }); +} + +#[test] +fn test_maturity_date_after_five_years() { + initialize_test_span("test_maturity_date_after_5_days").in_scope(|| { + let start_date = naive_date_2023_11_23(); + let years = dec!(5); + let maturity = maturity_date(start_date, years); + assert!(maturity.is_ok()); + let maturity = maturity.unwrap().to_string(); + assert_eq!(maturity, "2028-11-21"); + }); +} diff --git a/drive-deposits-cal-types/tests/test_simple_interest.rs b/drive-deposits-cal-types/tests/test_simple_interest.rs new file mode 100644 index 0000000..e4d43b4 --- /dev/null +++ b/drive-deposits-cal-types/tests/test_simple_interest.rs @@ -0,0 +1,167 @@ +use pretty_assertions::assert_eq; +use rust_decimal_macros::dec; +use tracing::instrument; + +use drive_deposits_cal_types::cal_types::{NewDelta, NewDeposit}; +use drive_deposits_cal_types::math::{ + growth::compute as compute_growth, simple_interest::compute_interest, +}; +use drive_deposits_proto_grpc_types::generated::{AccountType, PeriodUnit}; +use helper::enable_tracing::initialize_test_span; +use helper::test_data::naive_date_2023_11_23; + +mod helper; +#[test] +fn test_simple_interest_calculation_delta_years() { + initialize_test_span("test_simple_interest_calculation_delta_years").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::BrokerageCertificateOfDeposit, + apy: dec!(5.0), + years: dec!(2.0), + amount: dec!(1000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(1.0), + period_unit: PeriodUnit::Year, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(100.0)); + assert_eq!(delta_interest, dec!(50.0)); + }); +} + +#[test] +fn test_simple_interest_calculation_delta_several_years() { + initialize_test_span("test_simple_interest_calculation_delta_several_years").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::BrokerageCertificateOfDeposit, + apy: dec!(7.0), + years: dec!(5.0), + amount: dec!(5000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(15), + period_unit: PeriodUnit::Day, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(1750.0)); + assert_eq!(delta_interest, dec!(14.38)); + }); +} + +#[test] +fn test_simple_interest_calculation_delta_mid_level_decimal_interest() { + initialize_test_span("test_simple_interest_calculation_delta_mid_level_decimal_interests") + .in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::CertificateOfDeposit, + apy: dec!(3.4), + years: dec!(5.0), + amount: dec!(10000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(1), + period_unit: PeriodUnit::Month, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(1700.00)); + assert_eq!(delta_interest, dec!(27.95)); + }); +} + +#[test] +fn test_simple_interest_calculation_delta_months() { + initialize_test_span("test_simple_interest_calculation_delta_months").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::BrokerageCertificateOfDeposit, + apy: dec!(5.0), + years: dec!(2.0), + amount: dec!(1000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(1.0), + period_unit: PeriodUnit::Month, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(100.0)); + assert_eq!(delta_interest, dec!(4.11)); + }); +} + +#[test] +#[instrument] +fn test_simple_interest_calculation_delta_weeks() { + initialize_test_span("test_simple_interest_calculation_delta_weeks").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::BrokerageCertificateOfDeposit, + apy: dec!(5.0), + years: dec!(2.0), + amount: dec!(1000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(2.0), + period_unit: PeriodUnit::Week, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(100.0)); + assert_eq!(delta_interest, dec!(1.92)); + }); +} + +#[test] +fn test_simple_interest_calculation_delta_days() { + initialize_test_span("test_simple_interest_calculation_delta_days").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::BrokerageCertificateOfDeposit, + apy: dec!(5.0), + years: dec!(2.0), + amount: dec!(1000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(30.0), + period_unit: PeriodUnit::Day, + }; + let interest = compute_interest(&deposit); + let delta_interest = compute_growth(&deposit, interest, &delta).unwrap(); + assert_eq!(interest, dec!(100.0)); + assert_eq!(delta_interest, dec!(4.11)); + }); +} + +#[test] +#[should_panic] +fn test_growth_with_simple_interest_zero_years() { + initialize_test_span("test_growth_with_simple_interest_zero_years").in_scope(|| { + let deposit = NewDeposit { + account: "test_account".to_string(), + account_type: AccountType::BrokerageCertificateOfDeposit, + apy: dec!(5.0), + years: dec!(0.0), + amount: dec!(1000.0), + start_date_in_bank_tz: naive_date_2023_11_23(), + }; + let delta = NewDelta { + period: dec!(30.0), + period_unit: PeriodUnit::Day, + }; + let interest = compute_interest(&deposit); + compute_growth(&deposit, interest, &delta).unwrap(); + }); +} diff --git a/drive-deposits-check-cmd/Cargo.toml b/drive-deposits-check-cmd/Cargo.toml new file mode 100644 index 0000000..dbcaf5f --- /dev/null +++ b/drive-deposits-check-cmd/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "drive-deposits-check-cmd" +version = "0.10.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0.203", features = ["derive"] } +serde_json = "1.0.117" +thiserror = "1.0.61" +anyhow = "1.0.86" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +clap = { version = "4.5.8", features = ["derive"] } +rust_decimal = { version = "1.35.0", features = ["maths"] } +once_cell = "1.19.0" +validator = { version = "0.18.1", features = ["derive"] } +tokio = { version = "1.38.0", features = ["full"] } + +# workspace member depdenencies +drive-deposits-rest-types = { path = "../drive-deposits-rest-types" } +# proto generated dependency here the drive-deposits-proto-grpc-types is still package +# name so with dashes +drive-deposits-proto-grpc-types = { path = "../drive-deposits-proto-grpc-types" } +drive-deposits-cal-types = { path = "../drive-deposits-cal-types" } +drive-deposits-event-source = { path = "../drive-deposits-event-source" } + + + +[dev-dependencies] +assert_cmd = "2.0.14" +predicates = "3.1.0" +#pretty_assertions = "1.4.0" diff --git a/drive-deposits-check-cmd/src/lib.rs b/drive-deposits-check-cmd/src/lib.rs new file mode 100644 index 0000000..f2f1876 --- /dev/null +++ b/drive-deposits-check-cmd/src/lib.rs @@ -0,0 +1 @@ +pub mod portfolio; diff --git a/drive-deposits-check-cmd/src/main.rs b/drive-deposits-check-cmd/src/main.rs new file mode 100644 index 0000000..54577b7 --- /dev/null +++ b/drive-deposits-check-cmd/src/main.rs @@ -0,0 +1,52 @@ +use anyhow::Result; +use clap::Parser; +use tracing::{debug, info, info_span, Instrument}; +use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; + +use drive_deposits_check_cmd::portfolio::calculate::process_input; + +#[derive(Debug, Parser)] +#[command(author, version, about)] +/// The `drive-deposits-cal-types` command performs comprehensive local calculations. It mirrors the calculations done by the REST gateway, which forwards requests to the gRPC deposits service for identical processing. This dual functionality allows `drive-deposits-cal-types` to serve as a reliable tool for verifying calculations from REST and gRPC submissions. +/// +/// The data types used in this command are consistent with those in the actual services, ensuring a dependable method for rapid local verification of calculations. +/// Use this command to verify calculations locally. +/// +/// ### Sample Commands +/// Run the following commands through Cargo or directly with the binary: +/// +/// - To check the version: `cargo run -- --version` +/// +/// - For help: `cargo run -- --help` +/// +/// - To run with a sample input file: `cargo run -- tests/data/input.json` +/// +struct Args { + /// Input text + #[arg(required(true))] + json_request_file_path: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + let rust_log = std::env::var("RUST_LOG")?; + println!("in drive-deposits-check-cmd RUST_LOG is {}", rust_log); + + registry() + .with(EnvFilter::try_from_default_env()?) + // added in .cargo/config.tom .add_directive("drive_deposits_local=debug".parse()?)) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let args = Args::parse(); + debug!("args: {:?}", args); + let span = info_span!( + "drive_deposits_check_cmd", + json_request_file_path = args.json_request_file_path.as_str() + ); + let response = process_input(args.json_request_file_path) + .instrument(span) + .await?; + info!("response after processing request locally is: {}", response); + Ok(()) +} diff --git a/drive-deposits-check-cmd/src/portfolio.rs b/drive-deposits-check-cmd/src/portfolio.rs new file mode 100644 index 0000000..ed2498d --- /dev/null +++ b/drive-deposits-check-cmd/src/portfolio.rs @@ -0,0 +1 @@ +pub mod calculate; diff --git a/drive-deposits-check-cmd/src/portfolio/calculate.rs b/drive-deposits-check-cmd/src/portfolio/calculate.rs new file mode 100644 index 0000000..6fade68 --- /dev/null +++ b/drive-deposits-check-cmd/src/portfolio/calculate.rs @@ -0,0 +1,83 @@ +use thiserror::Error; +use tracing::{debug, instrument}; +use validator::Validate; + +use drive_deposits_cal_types::cal_types::PortfolioRequest as CalBankRequest; +use drive_deposits_cal_types::math::engine::calculate_portfolio; +use drive_deposits_event_source::eb::create_eb; +use drive_deposits_proto_grpc_types::generated::{ + CalculatePortfolioRequest as GrpcCalculatePortfolioRequest, + CalculatePortfolioResponse as GrpcCalculatePortfolioResponse, +}; +use drive_deposits_rest_types::rest_types::{ + CalculatePortfolioRequest as RestCalculatePortfolioRequest, + CalculatePortfolioResponse as RestCalculatePortfolioResponse, +}; + +#[derive(Default, Debug, Error)] +pub enum Error { + #[default] + #[error("Internal server error")] + InternalServer, + + #[error("IO error: {0}")] + IO(#[from] std::io::Error), + + #[error("JSON parsing error: {0}")] + JsonParse(#[from] serde_json::Error), + + #[error("Validation errors: {0}")] + Validation(#[from] validator::ValidationErrors), + + #[error("Calculation halt error could not progress, so had to halt: errors: {0}")] + CalculationHalt(#[from] drive_deposits_cal_types::math::engine::CalculationHaltError), + + #[error("Drive Deposits EventBridge error: {0}")] + DriveDepositsEventBridgeError( + #[from] drive_deposits_event_source::eb::DriveDepositsEventBridgeError, + ), +} + +#[instrument] +pub async fn process_input(file_path: String) -> Result { + let data = std::fs::read_to_string(file_path)?; + + // deserialize file into RestCalculatePortfolioRequest request and validate input data + let rest_req: RestCalculatePortfolioRequest = serde_json::from_str(&data)?; + debug!("Deserialized rest: {:?}", rest_req); + validate(&rest_req)?; + debug!("Validated rest: {:?}", rest_req); + + // convert from rest CalculatePortfolioRequest to grpc CalculatePortfolioRequest + let grpc_req: GrpcCalculatePortfolioRequest = rest_req.into(); + debug!("Converted from rest to grpc: {:?}", grpc_req); + + // convert grpc CalculatePortfolioRequest to calculator CalculatePortfolioRequest + let cal_req: CalBankRequest = grpc_req.into(); + debug!("Converted from grpc to cal: {:?}", cal_req); + + let drive_deposits_eb = create_eb().await?; + + // process calculation for calculator CalculatePortfolioRequest + let cal_resp = calculate_portfolio(cal_req, drive_deposits_eb).await?; + debug!("calculated response: {:?}", cal_resp); + + // convert response fom calculator CalculatePortfolioResponse to grpc CalculatePortfolioResponse + let grpc_resp: GrpcCalculatePortfolioResponse = cal_resp.into(); + debug!("grpc response: {:?}", grpc_resp); + // convert response from grpc CalculatePortfolioResponse to rest CalculatePortfolioResponse + let rest_resp: RestCalculatePortfolioResponse = grpc_resp.into(); + + debug!("rest response: {:?}", rest_resp); + + let json_resp = serde_json::to_string(&rest_resp)?; + // pretty print serialized json + let pretty_json_resp = serde_json::to_string_pretty(&rest_resp)?; + debug!("pretty serialized json response: {}", pretty_json_resp); + Ok(json_resp) +} + +fn validate(rest: &RestCalculatePortfolioRequest) -> Result<(), Error> { + rest.validate()?; + Ok(()) +} diff --git a/drive-deposits-check-cmd/tests/data/two_banks_json_request_invalid.json b/drive-deposits-check-cmd/tests/data/two_banks_json_request_invalid.json new file mode 100644 index 0000000..7eda651 --- /dev/null +++ b/drive-deposits-check-cmd/tests/data/two_banks_json_request_invalid.json @@ -0,0 +1,82 @@ +{ + "new_delta": { + "period": "1", + "period_unit": "Moth" + }, + "new_banks": [ + { + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "new_deposits": [ + { + "account": "1234", + "account_type": "Checking", + "apy": "0", + "years": "1", + "amount": "100", + "start_date_in_bank_tz": "2019-01-01" + }, + { + "account": "1256", + "account_type": "CertificateOfDeposit", + "apy": "5.40", + "years": "2", + "amount": "50000", + "start_date_in_bank_tz": "2018-04-07" + }, + { + "account": "1111", + "account_type": "CertificateOfDeposit", + "apy": "1.01", + "years": "10", + "amount": "21000", + "start_date_in_bank_tz": "2018-08-14" + } + ] + }, + { + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "2.4", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2024-02-16" + } + ] + }, + { + "name": "VISION-BANK", + "bank_tz": "America/LosAngeles", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "5", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2023-02-16" + }, + { + "account": "9898", + "account_type": "CertificateOfDeposit", + "apy": "2.22", + "years": "1", + "amount": "5500", + "start_date_in_bank_tz": "2020-02-16" + }, + { + "account": "3833", + "account_type": "Savings", + "apy": "3.75", + "years": "20", + "amount": "10000.50", + "start_date_in_bank_tz": "2024-02-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/drive-deposits-check-cmd/tests/data/two_banks_json_request_valid.json b/drive-deposits-check-cmd/tests/data/two_banks_json_request_valid.json new file mode 100644 index 0000000..3e67ddf --- /dev/null +++ b/drive-deposits-check-cmd/tests/data/two_banks_json_request_valid.json @@ -0,0 +1,82 @@ +{ + "new_delta": { + "period": "1", + "period_unit": "Month" + }, + "new_banks": [ + { + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "new_deposits": [ + { + "account": "1234", + "account_type": "Checking", + "apy": "0", + "years": "1", + "amount": "100", + "start_date_in_bank_tz": "2019-01-01" + }, + { + "account": "1256", + "account_type": "CertificateOfDeposit", + "apy": "5.40", + "years": "2", + "amount": "50000", + "start_date_in_bank_tz": "2018-04-07" + }, + { + "account": "1111", + "account_type": "CertificateOfDeposit", + "apy": "1.01", + "years": "10", + "amount": "21000", + "start_date_in_bank_tz": "2018-08-14" + } + ] + }, + { + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "2.4", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2024-02-16" + } + ] + }, + { + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "5", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2023-02-16" + }, + { + "account": "9898", + "account_type": "CertificateOfDeposit", + "apy": "2.22", + "years": "1", + "amount": "5500", + "start_date_in_bank_tz": "2020-02-16" + }, + { + "account": "3833", + "account_type": "Savings", + "apy": "3.75", + "years": "20", + "amount": "10000.50", + "start_date_in_bank_tz": "2024-02-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/drive-deposits-check-cmd/tests/enable_tracing.rs b/drive-deposits-check-cmd/tests/enable_tracing.rs new file mode 100644 index 0000000..95ad1bc --- /dev/null +++ b/drive-deposits-check-cmd/tests/enable_tracing.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use once_cell::sync::OnceCell; +use tracing::{debug, Span}; +use tracing_subscriber::{EnvFilter, registry}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +static INIT: OnceCell<()> = OnceCell::new(); + +pub fn setup_tracing_subscriber() -> Result<()> { + // can change level to debug to see all debug messages in tests + // or to info not to see + // for test span added test=debug here + registry() + // the test=debug is for debug! statements inside tests itself + .with(EnvFilter::try_from_default_env()?.add_directive("test=debug".parse()?)) + .with(tracing_subscriber::fmt::layer()) + .init(); + debug!("tracing subscriber initialized"); + let rust_log = std::env::var("RUST_LOG")?; + debug!("RUST_LOG is {}", rust_log); + Ok(()) +} + +pub fn initialize_test_span(test_name: &str) -> Span { + INIT.get_or_init(|| { + setup_tracing_subscriber().expect("Failed to set up tracing subscriber"); + }); + debug!("Running the test: {}", test_name); + let span = tracing::info_span!("test", test_name = test_name); + span +} diff --git a/drive-deposits-check-cmd/tests/test_delta_calculator_cli.rs b/drive-deposits-check-cmd/tests/test_delta_calculator_cli.rs new file mode 100644 index 0000000..e8ba621 --- /dev/null +++ b/drive-deposits-check-cmd/tests/test_delta_calculator_cli.rs @@ -0,0 +1,70 @@ +use anyhow::Result; +use assert_cmd::Command; +use predicates::prelude::predicate; +use tracing::debug; + +use enable_tracing::initialize_test_span; + +mod enable_tracing; + +#[test] +fn test_valid_two_banks_json_request() -> Result<()> { + initialize_test_span("test_two_banks_json_request").in_scope(|| { + let json_request_file_path = "tests/data/two_banks_json_request_valid.json"; + let cmd = Command::cargo_bin("drive-deposits-check-cmd")? + // .env("RUST_LOG", "test=debug,drive_deposits_check_cmd=debug") + .arg(json_request_file_path) + .assert() + .success(); + + // let output = cmd.get_output(); + // debug!( + // "Command Stdout is: {}", + // String::from_utf8_lossy(&output.stdout) + // ); + // + // debug!( + // "Command Stderr is: {}", + // String::from_utf8_lossy(&output.stderr) + // ); + // assert!(output.stderr.is_empty()); + + cmd.stdout(predicate::str::contains("VISION-BANK")) + .stdout(predicate::str::contains( + "tests/data/two_banks_json_request_valid.json", + )) + .stdout(predicate::str::contains( + "\"maturity_date_in_bank_tz\":\"2044-02-11\"", + )); + + Ok(()) + }) +} + +#[test] +fn test_invalid_two_banks_json_request() -> Result<()> { + initialize_test_span("test_invalid_two_banks_json_request").in_scope(|| { + let json_request_file_path = "tests/data/two_banks_json_request_invalid.json"; + let cmd = Command::cargo_bin("drive-deposits-check-cmd")? + .arg(json_request_file_path) + .assert() + .failure() + .stderr(predicate::str::contains("Must be a valid timezone")) + .stderr(predicate::str::contains( + "Must be Day, Week, Month, or Year", + )); + + let output = cmd.get_output(); + debug!( + "Command Stdout is: {}", + String::from_utf8_lossy(&output.stdout) + ); + + debug!( + "Command Stderr is: {}", + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) + }) +} diff --git a/drive-deposits-event-source/Cargo.toml b/drive-deposits-event-source/Cargo.toml new file mode 100644 index 0000000..41544b5 --- /dev/null +++ b/drive-deposits-event-source/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "drive-deposits-event-source" +version = "0.10.0" +edition = "2021" + + +[dependencies] +aws-config = { version = "1.5.4", features = ["behavior-version-latest"] } +aws-sdk-eventbridge = "1.37.0" +tokio = { version = "1.39.1", features = ["full"] } +tracing = "0.1.40" +thiserror = "1.0.63" +serde_json = "1.0.117" +serde = { version = "1.0.204", features = ["derive"] } +# workspace member depdenencies +drive-deposits-rest-types = { path = "../drive-deposits-rest-types" } +log = "0.4.22" + +[dev-dependencies] +pretty_assertions = "1.4.0" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +anyhow = "1.0.86" \ No newline at end of file diff --git a/drive-deposits-event-source/samconfig.toml b/drive-deposits-event-source/samconfig.toml new file mode 100644 index 0000000..a03c4f7 --- /dev/null +++ b/drive-deposits-event-source/samconfig.toml @@ -0,0 +1,9 @@ +version = 0.1 +[dev.deploy.parameters] +stack_name = "drive-deposits-event-bus-dev" +resolve_s3 = true +s3_prefix = "drive-deposits-event-bus-dev" +region = "us-west-2" +confirm_changeset = true +capabilities = "CAPABILITY_IAM" +image_repositories = [] diff --git a/drive-deposits-event-source/src/eb.rs b/drive-deposits-event-source/src/eb.rs new file mode 100644 index 0000000..b30c92c --- /dev/null +++ b/drive-deposits-event-source/src/eb.rs @@ -0,0 +1,177 @@ +use std::env::var; + +use aws_config::BehaviorVersion; +use aws_sdk_eventbridge::error::SdkError; +use aws_sdk_eventbridge::operation::list_rules::ListRulesError; +use aws_sdk_eventbridge::operation::put_events::{PutEventsError, PutEventsOutput}; +use aws_sdk_eventbridge::types::builders::PutEventsRequestEntryBuilder; +use aws_sdk_eventbridge::Client; +use thiserror::Error; +use tracing::{debug, error, instrument}; + +const LOCALSTACK_ENDPOINT: &str = "http://localhost.localstack.cloud:4566/"; + +#[derive(Debug, Error)] +pub enum DriveDepositsEventBridgeError { + #[error("Missing EventBridge")] + MissingEventBridge, + #[error("No rule found {0}")] + NoRuleErrorForEventBridgeEventBus(String), + #[error("Failed to send event to event bridge {0}")] + PuEventsSdkError(#[from] SdkError), + #[error("Failed to list event rules {0}")] + ListRulesSdkError(#[from] SdkError), + #[error("Failed Entry Count Error is {0}")] + FailedEntryCountError(String), +} + +#[derive(Debug, Clone)] +pub struct DriveDepositsEventBridge { + pub(crate) eb_client: Client, + pub(crate) bus_name: String, +} + +impl DriveDepositsEventBridge { + pub async fn new(bus_name: String) -> Self { + // let config = aws_config::load_from_env().await; + let mut config_loader = aws_config::defaults(BehaviorVersion::latest()); + if use_localstack() { + config_loader = config_loader.endpoint_url(LOCALSTACK_ENDPOINT); + }; + let config = config_loader.load().await; + + let client = aws_sdk_eventbridge::Client::new(&config); + Self { + eb_client: client, + bus_name, + } + } +} + +fn env_var_bool(name: &str) -> bool { + let env_val = var(name).unwrap_or_else(|_| "false".to_string()); + env_val.eq_ignore_ascii_case("true") +} + +#[instrument] +fn use_localstack() -> bool { + env_var_bool("USE_LOCALSTACK") +} + +#[instrument] +fn should_send_events() -> bool { + env_var_bool("SEND_CAL_EVENTS") +} + +#[instrument] +pub async fn create_eb() -> Result, DriveDepositsEventBridgeError> +{ + let send_events = should_send_events(); + debug!("send_events based on SEND_CAL_EVENTS: {}", send_events); + if !send_events { + return Ok(None); + } + let eb = DriveDepositsEventBridge::new("DriveDepositsEventBus".to_string()).await; + check_rules_exist_for_bus_name(&eb.eb_client, eb.bus_name.as_str()) + .await + .inspect_err(|err| { + error!("check rules exist for bus name err is {}", err); + })?; + Ok(Some(eb)) +} + +pub async fn check_rules_exist_for_bus_name( + client: &Client, + bus_name: &str, +) -> Result<(), DriveDepositsEventBridgeError> { + let list_rules = client + .list_rules() + .event_bus_name(bus_name) + .send() + .await + .inspect_err(|err| { + error!("list_rules send err is {:?}", err); + })?; + // more checks to be sure + let rules = list_rules.rules.unwrap_or_default(); + if rules.is_empty() { + error!( + "No rules found error in EventBridge for event bus {}", + bus_name + ); + return Err( + DriveDepositsEventBridgeError::NoRuleErrorForEventBridgeEventBus(bus_name.to_string()), + ); + } + Ok(()) +} + +pub async fn send_bank_level_event_to_event_bridge( + eb: Option<&DriveDepositsEventBridge>, + json_payload: String, +) -> Result<(), DriveDepositsEventBridgeError> { + let level = "bank-level"; + let put_events_output = send_event_to_event_bridge(eb, json_payload, level).await?; + puts_event_failed_entry_count(put_events_output, level)?; + + Ok(()) +} + +pub async fn send_portfolio_level_event_to_event_bridge( + eb: Option<&DriveDepositsEventBridge>, + json_payload: String, +) -> Result<(), DriveDepositsEventBridgeError> { + let level = "portfolio-level"; + let put_events_output = send_event_to_event_bridge(eb, json_payload, level).await?; + puts_event_failed_entry_count(put_events_output, level)?; + Ok(()) +} + +pub fn puts_event_failed_entry_count( + put_events_output: PutEventsOutput, + level: &str, +) -> Result<(), DriveDepositsEventBridgeError> { + debug!( + "At {} detail-type, PutEventsOutput is {:?}", + level, put_events_output + ); + if put_events_output.failed_entry_count > 0 { + return Err(DriveDepositsEventBridgeError::FailedEntryCountError( + format!( + "{} for sending events to event bridge detail type {}", + put_events_output.failed_entry_count.to_string(), + level + ), + )); + } + Ok(()) +} + +async fn send_event_to_event_bridge( + eb: Option<&DriveDepositsEventBridge>, + json_payload: String, + detail_type: &str, +) -> Result { + let bridge = eb.ok_or_else(|| DriveDepositsEventBridgeError::MissingEventBridge)?; + let bus_name = &bridge.bus_name; + let request = PutEventsRequestEntryBuilder::default() + .set_source(Some(String::from("drive-deposits"))) + .set_detail_type(Some(detail_type.into())) + .set_detail(Some(json_payload)) + .set_event_bus_name(Some(bus_name.into())) + .build(); + debug!("request being sent to event bridge: {:?}", request); + let aws_eb_client = &bridge.eb_client; + let request = aws_eb_client.put_events().entries(request); + + let put_events_output = request.send().await.inspect_err(|err| { + error!("put_events_output send err is {}", err); + })?; + debug!( + "Received response from EventBridge: {:?}", + put_events_output + ); + // put events output is useful example : + // 2024-07-24T23:39:57.059295Z INFO drive_deposits_check_cmd{json_request_file_path="examples/data/rest_request_valid.json"}:process_input{file_path="examples/data/rest_request_valid.json"}:calculate_by_period: drive_deposits_event_source::eb: PuEventsOutput is JMD: PutEventsOutput { failed_entry_count: 1, entries: Some([PutEventsResultEntry { event_id: None, error_code: Some("MalformedDetail"), error_message: Some("Detail is malformed.") }]), _request_id: Some("15fa9ddd-fd7f-4640-ba96-531715b211e5") } + Ok(put_events_output) +} diff --git a/drive-deposits-event-source/src/lib.rs b/drive-deposits-event-source/src/lib.rs new file mode 100644 index 0000000..158157b --- /dev/null +++ b/drive-deposits-event-source/src/lib.rs @@ -0,0 +1,2 @@ +pub mod eb; +pub mod payload_types; diff --git a/drive-deposits-event-source/src/payload_types.rs b/drive-deposits-event-source/src/payload_types.rs new file mode 100644 index 0000000..abbb2c6 --- /dev/null +++ b/drive-deposits-event-source/src/payload_types.rs @@ -0,0 +1,9 @@ +// Re-exporting drive_deposits_rest_types::rest_types +pub use drive_deposits_rest_types::rest_types::Bank; +pub use drive_deposits_rest_types::rest_types::CalculatePortfolioResponse; +pub use drive_deposits_rest_types::rest_types::Delta; +pub use drive_deposits_rest_types::rest_types::Deposit; +pub use drive_deposits_rest_types::rest_types::Maturity; +pub use drive_deposits_rest_types::rest_types::Outcome; +pub use drive_deposits_rest_types::rest_types::OutcomeWithDates; +pub use drive_deposits_rest_types::rest_types::ProcessingError; diff --git a/drive-deposits-event-source/template.yaml b/drive-deposits-event-source/template.yaml new file mode 100644 index 0000000..782aca3 --- /dev/null +++ b/drive-deposits-event-source/template.yaml @@ -0,0 +1,9 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: stack pattern drive-deposits-event-source; CloudFormation template for EventBridge Bus creation for sending events from gRPC calculations that will be consumed by Lambda functions + +Resources: + DriveDepositsEventBus: + Type: AWS::Events::EventBus + Properties: + Name: "DriveDepositsEventBus" diff --git a/drive-deposits-grpc-server/Cargo.toml b/drive-deposits-grpc-server/Cargo.toml new file mode 100644 index 0000000..6c5d76c --- /dev/null +++ b/drive-deposits-grpc-server/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "drive-deposits-grpc-server" +version = "0.10.0" +edition = "2021" + +[dependencies] +tonic = "0.12.0" +tonic-reflection = "0.12.0" +tonic-types = "0.12.0" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +tokio = { version = "1.38.0", features = ["full"] } + +# workspace member depdenencies +drive-deposits-rest-types = { path = "../drive-deposits-rest-types" } +# proto generated dependency here the drive-deposits-proto-grpc-types is still package +# name so with dashes +drive-deposits-proto-grpc-types = { path = "../drive-deposits-proto-grpc-types" } +drive-deposits-cal-types = { path = "../drive-deposits-cal-types" } +drive-deposits-event-source = { path = "../drive-deposits-event-source" } \ No newline at end of file diff --git a/drive-deposits-grpc-server/data/grpc_postman_calculate_for_period_request_valid.json b/drive-deposits-grpc-server/data/grpc_postman_calculate_for_period_request_valid.json new file mode 100644 index 0000000..af15e61 --- /dev/null +++ b/drive-deposits-grpc-server/data/grpc_postman_calculate_for_period_request_valid.json @@ -0,0 +1,82 @@ +{ + "new_delta": { + "period": "1", + "period_unit": "MONTH" + }, + "new_banks": [ + { + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "new_deposits": [ + { + "account": "1234", + "account_type": "CHECKING", + "apy": "0.01", + "years": "1", + "amount": "100", + "start_date_in_bank_tz": "2019-01-01" + }, + { + "account": "1256", + "account_type": "CERTIFICATE_OF_DEPOSIT", + "apy": "5.40", + "years": "2", + "amount": "50000", + "start_date_in_bank_tz": "2018-04-07" + }, + { + "account": "1111", + "account_type": "CERTIFICATE_OF_DEPOSIT", + "apy": "1.01", + "years": "10", + "amount": "21000", + "start_date_in_bank_tz": "2018-08-14" + } + ] + }, + { + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "new_deposits": [ + { + "account": "1234", + "account_type": "BROKERAGE_CERTIFICATE_OF_DEPOSIT", + "apy": "2.4", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2024-02-16" + } + ] + }, + { + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "new_deposits": [ + { + "account": "1234", + "account_type": "BROKERAGE_CERTIFICATE_OF_DEPOSIT", + "apy": "5", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2023-02-16" + }, + { + "account": "9898", + "account_type": "CERTIFICATE_OF_DEPOSIT", + "apy": "2.22", + "years": "1", + "amount": "5500", + "start_date_in_bank_tz": "2020-02-16" + }, + { + "account": "3833", + "account_type": "SAVINGS", + "apy": "3.75", + "years": "20", + "amount": "10000.50", + "start_date_in_bank_tz": "2024-02-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/drive-deposits-grpc-server/data/grpc_postman_response_for_valid.json b/drive-deposits-grpc-server/data/grpc_postman_response_for_valid.json new file mode 100644 index 0000000..d29983c --- /dev/null +++ b/drive-deposits-grpc-server/data/grpc_postman_response_for_valid.json @@ -0,0 +1,268 @@ +{ + "banks": [ + { + "deposits": [ + { + "uuid": "5ed88a99-8c64-4acf-be3c-4719d6b365ad", + "account": "1234", + "account_type": "BROKERAGE_CERTIFICATE_OF_DEPOSIT", + "apy": "2.4", + "years": "7", + "outcome": { + "errors": [], + "delta": { + "period": "1", + "period_unit": "MONTH", + "growth": "21.68" + }, + "maturity": { + "amount": "10990", + "interest": "1846.32", + "total": "12836.32" + } + }, + "outcome_with_dates": { + "errors": [], + "start_date_in_bank_tz": "2024-02-16", + "maturity_date_in_bank_tz": { + "value": "2031-02-14" + } + } + } + ], + "uuid": "9cf3e1ad-06c3-4e36-acc0-103786b44c4d", + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "outcome": { + "errors": [], + "delta": { + "period": "1", + "period_unit": "MONTH", + "growth": "21.68" + }, + "maturity": { + "amount": "10990", + "interest": "1846.32", + "total": "12836.32" + } + } + }, + { + "deposits": [ + { + "uuid": "abe7d3e2-700d-4b73-9871-9d3313644a6e", + "account": "1234", + "account_type": "BROKERAGE_CERTIFICATE_OF_DEPOSIT", + "apy": "5", + "years": "7", + "outcome": { + "errors": [], + "delta": { + "period": "1", + "period_unit": "MONTH", + "growth": "45.16" + }, + "maturity": { + "amount": "10990", + "interest": "3846.50", + "total": "14836.50" + } + }, + "outcome_with_dates": { + "errors": [], + "start_date_in_bank_tz": "2023-02-16", + "maturity_date_in_bank_tz": { + "value": "2030-02-14" + } + } + }, + { + "uuid": "cba953e1-9a31-4cd0-94bb-3b3d471f8821", + "account": "9898", + "account_type": "CERTIFICATE_OF_DEPOSIT", + "apy": "2.22", + "years": "1", + "outcome": { + "errors": [], + "delta": { + "period": "1", + "period_unit": "MONTH", + "growth": "10.04" + }, + "maturity": { + "amount": "5500", + "interest": "122.10", + "total": "5622.10" + } + }, + "outcome_with_dates": { + "errors": [], + "start_date_in_bank_tz": "2020-02-16", + "maturity_date_in_bank_tz": { + "value": "2021-02-15" + } + } + }, + { + "uuid": "6c28bb7c-7040-439c-9cc8-f990316ded55", + "account": "3833", + "account_type": "SAVINGS", + "apy": "3.75", + "years": "20", + "outcome": { + "errors": [], + "delta": { + "period": "1", + "period_unit": "MONTH", + "growth": "44.72" + }, + "maturity": { + "amount": "10000.50", + "interest": "10882.06", + "total": "20882.56" + } + }, + "outcome_with_dates": { + "errors": [], + "start_date_in_bank_tz": "2024-02-16", + "maturity_date_in_bank_tz": { + "value": "2044-02-11" + } + } + } + ], + "uuid": "4e8c8c91-120b-4a28-8d1e-4d5a7d11b9b1", + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "outcome": { + "errors": [], + "delta": { + "period": "1", + "period_unit": "MONTH", + "growth": "99.92" + }, + "maturity": { + "amount": "26490.50", + "interest": "14850.66", + "total": "41341.16" + } + } + }, + { + "deposits": [ + { + "uuid": "88c91edd-c723-400c-84d8-a35da9e8a9c9", + "account": "1234", + "account_type": "CHECKING", + "apy": "0.01", + "years": "1", + "outcome": { + "errors": [], + "delta": { + "period": "1", + "period_unit": "MONTH", + "growth": "0.00" + }, + "maturity": { + "amount": "100", + "interest": "0.01", + "total": "100.01" + } + }, + "outcome_with_dates": { + "errors": [], + "start_date_in_bank_tz": "2019-01-01", + "maturity_date_in_bank_tz": { + "value": "2020-01-01" + } + } + }, + { + "uuid": "8a46bae8-57ce-42ce-b2ab-af643d3704d3", + "account": "1256", + "account_type": "CERTIFICATE_OF_DEPOSIT", + "apy": "5.40", + "years": "2", + "outcome": { + "errors": [], + "delta": { + "period": "1", + "period_unit": "MONTH", + "growth": "227.91" + }, + "maturity": { + "amount": "50000", + "interest": "5545.80", + "total": "55545.80" + } + }, + "outcome_with_dates": { + "errors": [], + "start_date_in_bank_tz": "2018-04-07", + "maturity_date_in_bank_tz": { + "value": "2020-04-06" + } + } + }, + { + "uuid": "42ee05e8-51d4-46c5-a2dc-5a3bab78e1b7", + "account": "1111", + "account_type": "CERTIFICATE_OF_DEPOSIT", + "apy": "1.01", + "years": "10", + "outcome": { + "errors": [], + "delta": { + "period": "1", + "period_unit": "MONTH", + "growth": "18.25" + }, + "maturity": { + "amount": "21000", + "interest": "2220.04", + "total": "23220.04" + } + }, + "outcome_with_dates": { + "errors": [], + "start_date_in_bank_tz": "2018-08-14", + "maturity_date_in_bank_tz": { + "value": "2028-08-11" + } + } + } + ], + "uuid": "4adb036f-bc05-463b-80c3-5ba6ab4a5b15", + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "outcome": { + "errors": [], + "delta": { + "period": "1", + "period_unit": "MONTH", + "growth": "246.16" + }, + "maturity": { + "amount": "71100", + "interest": "7765.85", + "total": "78865.85" + } + } + } + ], + "uuid": "0330cc1a-82f4-4683-b3e8-b4564c8da8ca", + "outcome": { + "errors": [], + "delta": { + "period": "1", + "period_unit": "MONTH", + "growth": "367.76" + }, + "maturity": { + "amount": "108580.50", + "interest": "24462.83", + "total": "133043.33" + } + }, + "created_at": "2024-07-16 01:57:55.939555 UTC" +} \ No newline at end of file diff --git a/drive-deposits-grpc-server/src/lib.rs b/drive-deposits-grpc-server/src/lib.rs new file mode 100644 index 0000000..ac5108b --- /dev/null +++ b/drive-deposits-grpc-server/src/lib.rs @@ -0,0 +1,2 @@ +pub mod portfolio; +pub mod service_router; diff --git a/drive-deposits-grpc-server/src/main.rs b/drive-deposits-grpc-server/src/main.rs new file mode 100644 index 0000000..f36aedc --- /dev/null +++ b/drive-deposits-grpc-server/src/main.rs @@ -0,0 +1,31 @@ +use std::error::Error; + +use tracing::{error, info, info_span, Instrument}; +use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; + +use drive_deposits_grpc_server::service_router::app; + +#[tokio::main] +async fn main() -> Result<(), Box> { + registry() + .with( + EnvFilter::try_from_default_env()?, // added in .cargo/config.toml.add_directive("drive_deposits_grpc=debug".parse()?), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let rust_log = std::env::var("RUST_LOG")?; + println!("RUST_LOG is {}", rust_log); + + let span = info_span!("main"); + let app = app().instrument(span.clone()).await?; + let addr = "[::1]:50052".parse().unwrap(); + + span.in_scope(|| info!("gRPC server running! on {}", addr)); + let serve_result = app.serve(addr).instrument(info_span!("server")).await; + + serve_result.inspect_err(|e| { + span.in_scope(|| error!("gRPC server error is {:?}", e)); + })?; + Ok(()) +} diff --git a/drive-deposits-grpc-server/src/portfolio.rs b/drive-deposits-grpc-server/src/portfolio.rs new file mode 100644 index 0000000..4d3ce7e --- /dev/null +++ b/drive-deposits-grpc-server/src/portfolio.rs @@ -0,0 +1,31 @@ +use tonic::{async_trait, Request, Response, Status}; +use tracing::{debug, error, info_span}; + +use drive_deposits_event_source::eb::DriveDepositsEventBridge; +use drive_deposits_proto_grpc_types::generated::{ + drive_deposits_service_server::DriveDepositsService, CalculatePortfolioRequest, + CalculatePortfolioResponse, +}; + +mod calculate; +mod grpc_status_handler; + +pub struct DriveDepositsCalculator { + pub drive_deposits_eb: Option, +} + +#[async_trait] +impl DriveDepositsService for DriveDepositsCalculator { + async fn calculate_portfolio( + &self, + request: Request, + ) -> Result, Status> { + info_span!("grpc_calculate_portfolio"); + debug!("calculate_portfolio request incoming is : {:#?}", request); + let delta_request = request.into_inner(); + let response = calculate::by_period(delta_request, self.drive_deposits_eb.clone()) + .await + .inspect_err(|err| error!("building response errors : {:?}", err))?; + Ok(Response::new(response)) + } +} diff --git a/drive-deposits-grpc-server/src/portfolio/calculate.rs b/drive-deposits-grpc-server/src/portfolio/calculate.rs new file mode 100644 index 0000000..d457143 --- /dev/null +++ b/drive-deposits-grpc-server/src/portfolio/calculate.rs @@ -0,0 +1,86 @@ +use tonic::Status; +use tracing::{debug, error, info}; + +use drive_deposits_cal_types::cal_types::PortfolioRequest as CalBankRequest; +use drive_deposits_event_source::eb::DriveDepositsEventBridge; +use drive_deposits_proto_grpc_types::generated::{ + CalculatePortfolioRequest as GrpcCalculatePortfolioRequest, + CalculatePortfolioResponse as GrpcCalculatePortfolioResponse, +}; + +use crate::portfolio::grpc_status_handler::CalculationHaltErrorWrapper; +use drive_deposits_cal_types::math::engine::calculate_portfolio; + +use super::grpc_status_handler; + +pub async fn by_period( + delta_request: GrpcCalculatePortfolioRequest, + eb: Option, +) -> Result { + info!("new_banks incoming is : {:?}", delta_request.new_banks); + grpc_status_handler::bad_request_errors(&delta_request.new_banks) + .inspect_err(|err| error!("new_banks checking at the grpc level errors : {:?}", err))?; + + // convert grpc CalculatePortfolioRequest to calculator CalculatePortfolioRequest + let cal_req: CalBankRequest = delta_request.into(); + debug!("Converted from grpc to cal: {:?}", cal_req); + + // process calculation for calculator CalculatePortfolioRequest + let cal_resp = calculate_portfolio(cal_req, eb) + .await + .map_err(CalculationHaltErrorWrapper)?; + debug!("calculated response: {:?}", cal_resp); + + // convert response fom calculator CalculatePortfolioResponse to grpc CalculatePortfolioResponse + let grpc_resp: GrpcCalculatePortfolioResponse = cal_resp.into(); + debug!("grpc response: {:?}", grpc_resp); + + Ok(grpc_resp) +} + +// use drive_deposits_proto_grpc_types::generated::{ +// Bank, CalculatePortfolioRequest, CalculatePortfolioResponse, Delta, Deposit, +// Maturity, NewBank, Outcome, +// }; + +// pub fn build_banks(new_banks: Vec) -> Vec { +// let banks = new_banks +// .into_iter() +// .map(|new_bank| -> Bank { +// Bank { +// uuid: "100".to_string(), +// name: new_bank.name, +// deposits: new_bank +// .new_deposits +// .into_iter() +// .map(|new_deposit| -> Deposit { +// Deposit { +// uuid: "900".to_string(), +// account: new_deposit.account, +// account_type: new_deposit.account_type, +// apy: new_deposit.apy, +// years: new_deposit.years, +// outcome: Some(Outcome { +// delta: Some(Delta { +// period: "23.0".to_string(), +// period_unit: 3, +// growth: "23.0".to_string(), +// }), +// maturity: Some(Maturity { +// amount: "23.0".to_string(), +// interest: "23.0".to_string(), +// total: "23.0".to_string(), +// }), +// errors: vec![], +// }), +// ..Default::default() +// } +// }) +// .collect::>(), +// ..Default::default() +// } +// }) +// .collect::>(); +// info!("banks is : {:?}", banks); +// banks +// } diff --git a/drive-deposits-grpc-server/src/portfolio/grpc_status_handler.rs b/drive-deposits-grpc-server/src/portfolio/grpc_status_handler.rs new file mode 100644 index 0000000..651e6bc --- /dev/null +++ b/drive-deposits-grpc-server/src/portfolio/grpc_status_handler.rs @@ -0,0 +1,56 @@ +use tonic::{Code, Status}; +use tonic_types::{BadRequest, Help, LocalizedMessage, StatusExt}; + +use drive_deposits_cal_types::math::engine::CalculationHaltError; +use drive_deposits_proto_grpc_types::generated::NewBank; + +pub fn bad_request_errors(new_banks: &[NewBank]) -> Result<(), Status> { + let mut bad_request = BadRequest::new(vec![]); + if new_banks.is_empty() { + bad_request.add_violation("new_banks", "name_banks cannot be empty"); + } else if new_banks.len() > 500 { + bad_request.add_violation( + "new_banks", + "too many banks provided; must be less than upper limit of 500", + ); + } + + if !bad_request.is_empty() { + let help = Help::with_link("check your banks list", "https://drinnovations.us"); + let localized_message = LocalizedMessage::new("en-US", "overall validate your banks list"); + let status = Status::with_error_details_vec( + Code::InvalidArgument, + "request contains invalid arguments", + vec![bad_request.into(), help.into(), localized_message.into()], + ); + return Err(status); + } + Ok(()) +} + +pub struct CalculationHaltErrorWrapper(pub CalculationHaltError); +impl From for Status { + fn from(wrapper: CalculationHaltErrorWrapper) -> Self { + match wrapper.0 { + CalculationHaltError::Internal => { + Status::internal("All Calculations could not proceed") + } + CalculationHaltError::Join(e) => Status::internal(format!( + "Join error all calculations could not proceed: {}", + e + )), + CalculationHaltError::DriveDepositsEventBridgeError(e) => { + Status::internal(format!( + "Drive Deposits SEND_CAL_EVENTS is true but could not send events for processing as desired: {}", + e + )) + } + CalculationHaltError::EventSourceJsonSerializationError(e) => { + Status::internal(format!( + "Drive Deposits SEND_CAL_EVENTS is true but could not serialize events for sending as desired: {}", + e + )) + } + } + } +} diff --git a/drive-deposits-grpc-server/src/service_router.rs b/drive-deposits-grpc-server/src/service_router.rs new file mode 100644 index 0000000..fe64b96 --- /dev/null +++ b/drive-deposits-grpc-server/src/service_router.rs @@ -0,0 +1,29 @@ +use tonic::{transport::server::Router, transport::Server}; +use tonic_reflection::server::Builder; +use tracing::{info, info_span, instrument}; + +use drive_deposits_event_source::eb::create_eb; +use drive_deposits_proto_grpc_types::generated::{ + drive_deposits_service_server::DriveDepositsServiceServer, FILE_DESCRIPTOR_SET, +}; + +use crate::portfolio::DriveDepositsCalculator; + +#[instrument] +pub async fn app() -> Result> { + let server_reflection = Builder::configure() + .register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET) + .build()?; + info!("reflection built"); + + let eb = create_eb().await?; + let delta = DriveDepositsCalculator { + drive_deposits_eb: eb, + }; + + let builder = Server::builder() + .trace_fn(|_| info_span!("drivedeposits_server")) + .add_service(server_reflection) + .add_service(DriveDepositsServiceServer::new(delta)); + Ok(builder) +} diff --git a/drive-deposits-lambda-db-types/Cargo.toml b/drive-deposits-lambda-db-types/Cargo.toml new file mode 100644 index 0000000..74e4005 --- /dev/null +++ b/drive-deposits-lambda-db-types/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "drive-deposits-lambda-db-types" +version = "0.10.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0.204", features = ["derive"] } +serde_json = "1.0.121" +thiserror = "1.0.63" +rust_decimal = { version = "1.35.0", features = ["maths"] } +rust_decimal_macros = "1.34.2" +aws-sdk-dynamodb = "1.39.1" +tracing = "0.1.40" +# workspace member depdenencies +drive-deposits-rest-types = { path = "../drive-deposits-rest-types" } \ No newline at end of file diff --git a/drive-deposits-lambda-db-types/src/convert.rs b/drive-deposits-lambda-db-types/src/convert.rs new file mode 100644 index 0000000..c9134a0 --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert.rs @@ -0,0 +1,2 @@ +pub mod reader; +pub mod writer; diff --git a/drive-deposits-lambda-db-types/src/convert/reader.rs b/drive-deposits-lambda-db-types/src/convert/reader.rs new file mode 100644 index 0000000..34bd104 --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/reader.rs @@ -0,0 +1,9 @@ +pub mod from_hashmap_av_to_portfolio_level_item; +pub mod from_portfolio_level_item_to_portfolio_response; + +pub mod from_bank_level_item_to_by_bank_response; +pub mod from_hashmap_av_to_bank_level_item; + +pub mod from_deposit_level_item_to_deposit_response; +pub mod from_hashmap_av_to_deposit_level_item; +pub mod with_level_context; diff --git a/drive-deposits-lambda-db-types/src/convert/reader/from_bank_level_item_to_by_bank_response.rs b/drive-deposits-lambda-db-types/src/convert/reader/from_bank_level_item_to_by_bank_response.rs new file mode 100644 index 0000000..1f46fef --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/reader/from_bank_level_item_to_by_bank_response.rs @@ -0,0 +1,31 @@ +use crate::convert::reader::with_level_context::{ + deserialize_with_bank_level, LevelSpecificResponseReaderError, +}; +use crate::db_item_types::BankLevelItem; +use crate::query_response_types::BankResponse; +use tracing::info; + +impl TryFrom for BankResponse { + type Error = LevelSpecificResponseReaderError; + + fn try_from(item: BankLevelItem) -> Result { + let bank_uuid = item.bank_uuid; + let bank_name = item.bank_name; + let portfolio_uuid = item.portfolio_uuid; + let bank_tz = item.bank_tz; + info!( + "item.outcome_as_json.as_str() is {}", + item.outcome_as_json.as_str() + ); + let outcome = deserialize_with_bank_level(item.outcome_as_json.as_str())?; + let created_at = item.created_at; + Ok(BankResponse { + bank_uuid, + bank_name, + portfolio_uuid, + bank_tz, + outcome, + created_at, + }) + } +} diff --git a/drive-deposits-lambda-db-types/src/convert/reader/from_deposit_level_item_to_deposit_response.rs b/drive-deposits-lambda-db-types/src/convert/reader/from_deposit_level_item_to_deposit_response.rs new file mode 100644 index 0000000..701c0d7 --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/reader/from_deposit_level_item_to_deposit_response.rs @@ -0,0 +1,43 @@ +use crate::convert::reader::with_level_context::{ + deserialize_with_deposit_level, LevelSpecificResponseReaderError, +}; +use crate::db_item_types::DepositLevelItem; +use crate::query_response_types::DepositResponse; + +impl TryFrom for DepositResponse { + type Error = LevelSpecificResponseReaderError; + + fn try_from(item: DepositLevelItem) -> Result { + let bank_uuid = item.bank_uuid; + let bank_name = item.bank_name; + let portfolio_uuid = item.portfolio_uuid; + let deposit_uuid = item.deposit_uuid; + let account = item.account; + let account_type = item.account_type; + let apy = item.apy; + let years = item.years; + let outcome = deserialize_with_deposit_level(item.outcome_as_json.as_str())?; + let outcome_with_dates = + deserialize_with_deposit_level(item.outcome_with_dates_as_json.as_str())?; + + let created_at = item.created_at; + let deposit_delta_growth = item.deposit_delta_growth; + let deposit_maturity_date_in_bank_tz = item.deposit_maturity_date_in_bank_tz; + + Ok(DepositResponse { + bank_uuid, + bank_name, + portfolio_uuid, + deposit_uuid, + account, + account_type, + apy, + years, + deposit_delta_growth, + deposit_maturity_date_in_bank_tz, + outcome, + outcome_with_dates, + created_at, + }) + } +} diff --git a/drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_bank_level_item.rs b/drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_bank_level_item.rs new file mode 100644 index 0000000..112ed8d --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_bank_level_item.rs @@ -0,0 +1,32 @@ +use crate::convert::reader::with_level_context::{ + get_field_with_bank_level, LevelSpecificItemReaderError, +}; +use crate::db_item_types::BankLevelItem; +use aws_sdk_dynamodb::types::AttributeValue; +use std::collections::HashMap; + +impl TryFrom> for BankLevelItem { + type Error = LevelSpecificItemReaderError; + + fn try_from(dynamodb_item: HashMap) -> Result { + let pk_portfolio_uuid = get_field_with_bank_level(&dynamodb_item, "PK")?; + let sk_bank_delta_growth = get_field_with_bank_level(&dynamodb_item, "SK")?; + let bank_uuid = get_field_with_bank_level(&dynamodb_item, "bank_uuid")?; + let bank_name = get_field_with_bank_level(&dynamodb_item, "bank_name")?; + let portfolio_uuid = get_field_with_bank_level(&dynamodb_item, "portfolio_uuid")?; + let bank_tz = get_field_with_bank_level(&dynamodb_item, "bank_tz")?; + let outcome_as_json = get_field_with_bank_level(&dynamodb_item, "outcome")?; + let created_at = get_field_with_bank_level(&dynamodb_item, "created_at")?; + + Ok(BankLevelItem { + pk_format_portfolio_uuid: pk_portfolio_uuid, + sk_format_bank_delta_growth: sk_bank_delta_growth, + bank_uuid, + bank_name, + portfolio_uuid, + bank_tz, + outcome_as_json, + created_at, + }) + } +} diff --git a/drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_deposit_level_item.rs b/drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_deposit_level_item.rs new file mode 100644 index 0000000..4343e11 --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_deposit_level_item.rs @@ -0,0 +1,49 @@ +use crate::convert::reader::with_level_context::{ + get_field_with_deposit_level, LevelSpecificItemReaderError, +}; +use crate::db_item_types::DepositLevelItem; +use aws_sdk_dynamodb::types::AttributeValue; +use std::collections::HashMap; + +impl TryFrom> for DepositLevelItem { + type Error = LevelSpecificItemReaderError; + + fn try_from(dynamodb_item: HashMap) -> Result { + let pk_portfolio_uuid = get_field_with_deposit_level(&dynamodb_item, "PK")?; + let sk_bank_deposit_delta_growth = get_field_with_deposit_level(&dynamodb_item, "SK")?; + let deposit_delta_growth = + get_field_with_deposit_level(&dynamodb_item, "deposit_delta_growth")?; + let deposit_maturity_date_in_bank_tz = + get_field_with_deposit_level(&dynamodb_item, "deposit_maturity_date_in_bank_tz")?; + let bank_uuid = get_field_with_deposit_level(&dynamodb_item, "bank_uuid")?; + let bank_name = get_field_with_deposit_level(&dynamodb_item, "bank_name")?; + let portfolio_uuid = get_field_with_deposit_level(&dynamodb_item, "portfolio_uuid")?; + let deposit_uuid = get_field_with_deposit_level(&dynamodb_item, "deposit_uuid")?; + let account = get_field_with_deposit_level(&dynamodb_item, "account")?; + let account_type = get_field_with_deposit_level(&dynamodb_item, "account_type")?; + let apy = get_field_with_deposit_level(&dynamodb_item, "apy")?; + let years = get_field_with_deposit_level(&dynamodb_item, "years")?; + let outcome_as_json = get_field_with_deposit_level(&dynamodb_item, "outcome")?; + let outcome_with_dates_as_json = + get_field_with_deposit_level(&dynamodb_item, "outcome_with_dates")?; + let created_at = get_field_with_deposit_level(&dynamodb_item, "created_at")?; + + Ok(DepositLevelItem { + pk_format_portfolio_uuid: pk_portfolio_uuid, + sk_format_deposit_sort_criteria: sk_bank_deposit_delta_growth, + deposit_delta_growth, + deposit_maturity_date_in_bank_tz, + bank_uuid, + bank_name, + portfolio_uuid, + deposit_uuid, + account, + account_type, + apy, + years, + outcome_as_json, + outcome_with_dates_as_json, + created_at, + }) + } +} diff --git a/drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_portfolio_level_item.rs b/drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_portfolio_level_item.rs new file mode 100644 index 0000000..4e1180d --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/reader/from_hashmap_av_to_portfolio_level_item.rs @@ -0,0 +1,36 @@ +use crate::convert::reader::with_level_context::{ + get_field_with_portfolio_level, LevelSpecificItemReaderError, +}; +use crate::db_item_types::PortfolioLevelItem; +use aws_sdk_dynamodb::types::AttributeValue; +use std::collections::HashMap; + +// #[derive(Error, Debug)] +// pub enum PortfolioLevelItemReaderError { +// #[error("PortfolioLevelItemReaderError with MissingAttribute error: {0}")] +// MissingAttribute(String), +// +// #[error("PortfolioLevelItemReaderError with ConversionToStringError error: {0}")] +// ConversionToStringError(String), +// } + +impl TryFrom> for PortfolioLevelItem { + type Error = LevelSpecificItemReaderError; + + fn try_from(dynamodb_item: HashMap) -> Result { + let pk_portfolios = get_field_with_portfolio_level(&dynamodb_item, "PK")?; + let sk_response_delta_growth = get_field_with_portfolio_level(&dynamodb_item, "SK")?; + let portfolio_uuid = get_field_with_portfolio_level(&dynamodb_item, "portfolio_uuid")?; + let outcome_as_json = get_field_with_portfolio_level(&dynamodb_item, "outcome")?; + let created_at = get_field_with_portfolio_level(&dynamodb_item, "created_at")?; + + let responses_level_item = PortfolioLevelItem { + pk_portfolios, + sk_format_portfolio_delta_growth: sk_response_delta_growth, + portfolio_uuid, + outcome_as_json, + created_at, + }; + Ok(responses_level_item) + } +} diff --git a/drive-deposits-lambda-db-types/src/convert/reader/from_portfolio_level_item_to_portfolio_response.rs b/drive-deposits-lambda-db-types/src/convert/reader/from_portfolio_level_item_to_portfolio_response.rs new file mode 100644 index 0000000..37b371f --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/reader/from_portfolio_level_item_to_portfolio_response.rs @@ -0,0 +1,23 @@ +use crate::convert::reader::with_level_context::{ + deserialize_with_portfolio_level, LevelSpecificResponseReaderError, +}; +use crate::db_item_types::PortfolioLevelItem; +use crate::query_response_types::PortfolioResponse; + +impl TryFrom for PortfolioResponse { + type Error = LevelSpecificResponseReaderError; + + fn try_from(item: PortfolioLevelItem) -> Result { + let portfolio_uuid = item.portfolio_uuid; + let outcome = deserialize_with_portfolio_level(item.outcome_as_json.as_str())?; + let created_at = item.created_at; + Ok(Self { + portfolio_uuid, + outcome, + created_at, + }) + } +} + +// checked with +// get_json_instance_from_str("{\"delta\": {\"period\":\"1\",\"period_unit\":\"Month\",\"growth\":\"34.43\",\"maturity\":{\"amount\":\"11990.50\",\"interest\":\"6210.02\",\"total\":\"18200.52\",\"errors\":[]}", ReaderErrorLevel::Portfolio)?; diff --git a/drive-deposits-lambda-db-types/src/convert/reader/with_level_context.rs b/drive-deposits-lambda-db-types/src/convert/reader/with_level_context.rs new file mode 100644 index 0000000..6c4f41d --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/reader/with_level_context.rs @@ -0,0 +1,117 @@ +use aws_sdk_dynamodb::types::AttributeValue; +use std::collections::HashMap; +use thiserror::Error; + +#[derive(Debug, Error, Clone)] +pub enum ReaderErrorLevel { + #[error("Bank")] + Bank, + #[error("Deposit")] + Deposit, + #[error("Portfolio")] + Portfolio, +} + +#[derive(Debug, Error, Clone)] +pub enum ItemReaderError { + #[error("Missing attribute: {0}")] + MissingAttribute(String), + #[error("Conversion to string error for attribute: {0}")] + ConversionToStringError(String), +} + +#[derive(Debug, Error, Clone)] +pub enum LevelSpecificItemReaderError { + #[error("{level} level: {source}")] + Error { + #[source] + source: ItemReaderError, + level: ReaderErrorLevel, + }, +} + +pub fn get_field_value_with_level( + dynamodb_item: &HashMap, + field_name: &str, + level: ReaderErrorLevel, +) -> Result { + let field_value = dynamodb_item + .get(field_name) + .ok_or_else(|| LevelSpecificItemReaderError::Error { + source: ItemReaderError::MissingAttribute(field_name.to_string()), + level: level.clone(), + })? + .as_s() + .map_err(|_av| LevelSpecificItemReaderError::Error { + source: ItemReaderError::ConversionToStringError(field_name.to_string()), + level, + })? + .to_owned(); + Ok(field_value) +} + +pub fn get_field_with_portfolio_level( + dynamodb_item: &HashMap, + field_name: &str, +) -> Result { + get_field_value_with_level(dynamodb_item, field_name, ReaderErrorLevel::Portfolio) +} + +pub fn get_field_with_bank_level( + dynamodb_item: &HashMap, + field_name: &str, +) -> Result { + get_field_value_with_level(dynamodb_item, field_name, ReaderErrorLevel::Bank) +} + +pub fn get_field_with_deposit_level( + dynamodb_item: &HashMap, + field_name: &str, +) -> Result { + get_field_value_with_level(dynamodb_item, field_name, ReaderErrorLevel::Deposit) +} + +#[derive(Debug, Error)] +pub enum LevelSpecificResponseReaderError { + #[error("{level} level conversion SerdeJsonError: {source}")] + JsonError { + source: serde_json::Error, + level: ReaderErrorLevel, + }, +} + +pub fn deserialize_with_error_level( + json_str: &str, + level: ReaderErrorLevel, +) -> Result +where + S: serde::de::DeserializeOwned, +{ + serde_json::from_str(json_str) + .map_err(|source| LevelSpecificResponseReaderError::JsonError { source, level }) +} + +pub fn deserialize_with_portfolio_level( + json_str: &str, +) -> Result +where + T: serde::de::DeserializeOwned, +{ + deserialize_with_error_level(json_str, ReaderErrorLevel::Portfolio) +} + +pub fn deserialize_with_bank_level(json_str: &str) -> Result +where + T: serde::de::DeserializeOwned, +{ + deserialize_with_error_level(json_str, ReaderErrorLevel::Bank) +} + +pub fn deserialize_with_deposit_level( + json_str: &str, +) -> Result +where + T: serde::de::DeserializeOwned, +{ + deserialize_with_error_level(json_str, ReaderErrorLevel::Deposit) +} diff --git a/drive-deposits-lambda-db-types/src/convert/writer.rs b/drive-deposits-lambda-db-types/src/convert/writer.rs new file mode 100644 index 0000000..387234e --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/writer.rs @@ -0,0 +1,7 @@ +mod from_bank_item_to_hashmap_av; +mod from_deposit_level_item_to_hashmap_av; +mod from_portfolio_level_item_to_hashmap_av; +pub mod from_rest_to_bank_level_items_wrapper; +pub mod from_rest_to_deposit_level_items_wrapper; +pub mod from_rest_to_portfolio_level_item; +pub mod with_level_context; diff --git a/drive-deposits-lambda-db-types/src/convert/writer/from_bank_item_to_hashmap_av.rs b/drive-deposits-lambda-db-types/src/convert/writer/from_bank_item_to_hashmap_av.rs new file mode 100644 index 0000000..5a4b983 --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/writer/from_bank_item_to_hashmap_av.rs @@ -0,0 +1,27 @@ +use crate::db_item_types::BankLevelItem; +use aws_sdk_dynamodb::types::AttributeValue; +use std::collections::HashMap; + +impl From for HashMap { + fn from(item: BankLevelItem) -> Self { + let pk_portfolio_uuid_av = AttributeValue::S(item.pk_format_portfolio_uuid); + let created_at_av = AttributeValue::S(item.created_at); + let sk_bank_delta_growth_av = AttributeValue::S(item.sk_format_bank_delta_growth); + let bank_uuid_av = AttributeValue::S(item.bank_uuid); + let bank_name_av = AttributeValue::S(item.bank_name); + let portfolio_uuid_av = AttributeValue::S(item.portfolio_uuid); + let bank_tz_av = AttributeValue::S(item.bank_tz); + let outcome_as_json_av = AttributeValue::S(item.outcome_as_json); + + HashMap::from([ + ("PK".to_string(), pk_portfolio_uuid_av), + ("SK".to_string(), sk_bank_delta_growth_av), + ("bank_uuid".to_string(), bank_uuid_av), + ("bank_name".to_string(), bank_name_av), + ("portfolio_uuid".to_string(), portfolio_uuid_av), + ("bank_tz".to_string(), bank_tz_av), + ("outcome".to_string(), outcome_as_json_av), + ("created_at".to_string(), created_at_av), + ]) + } +} diff --git a/drive-deposits-lambda-db-types/src/convert/writer/from_deposit_level_item_to_hashmap_av.rs b/drive-deposits-lambda-db-types/src/convert/writer/from_deposit_level_item_to_hashmap_av.rs new file mode 100644 index 0000000..83c2b74 --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/writer/from_deposit_level_item_to_hashmap_av.rs @@ -0,0 +1,48 @@ +use crate::db_item_types::DepositLevelItem; +use aws_sdk_dynamodb::types::AttributeValue; +use std::collections::HashMap; + +impl From for HashMap { + fn from(item: DepositLevelItem) -> Self { + let pk_portfolio_uuid_av = AttributeValue::S(item.pk_format_portfolio_uuid); + let sk_deposit_sort_criteria = AttributeValue::S(item.sk_format_deposit_sort_criteria); + let deposit_delta_growth = AttributeValue::S(item.deposit_delta_growth); + let deposit_maturity_date_in_bank_tz_av = + AttributeValue::S(item.deposit_maturity_date_in_bank_tz); + let created_at_av = AttributeValue::S(item.created_at); + let bank_uuid_av = AttributeValue::S(item.bank_uuid); + let bank_name_av = AttributeValue::S(item.bank_name); + let portfolio_uuid_av = AttributeValue::S(item.portfolio_uuid); + let deposit_uuid_av = AttributeValue::S(item.deposit_uuid); + let account_av = AttributeValue::S(item.account); + let account_type_av = AttributeValue::S(item.account_type); + let apy_av = AttributeValue::S(item.apy); + let years_av = AttributeValue::S(item.years); + let outcome_as_json_av = AttributeValue::S(item.outcome_as_json); + let outcome_with_dates_as_json_av = AttributeValue::S(item.outcome_with_dates_as_json); + + HashMap::from([ + ("PK".to_string(), pk_portfolio_uuid_av), + ("SK".to_string(), sk_deposit_sort_criteria), + ("deposit_delta_growth".to_string(), deposit_delta_growth), + ( + "deposit_maturity_date_in_bank_tz".to_string(), + deposit_maturity_date_in_bank_tz_av, + ), + ("bank_uuid".to_string(), bank_uuid_av), + ("bank_name".to_string(), bank_name_av), + ("portfolio_uuid".to_string(), portfolio_uuid_av), + ("deposit_uuid".to_string(), deposit_uuid_av), + ("account".to_string(), account_av), + ("account_type".to_string(), account_type_av), + ("apy".to_string(), apy_av), + ("years".to_string(), years_av), + ("outcome".to_string(), outcome_as_json_av), + ( + "outcome_with_dates".to_string(), + outcome_with_dates_as_json_av, + ), + ("created_at".to_string(), created_at_av), + ]) + } +} diff --git a/drive-deposits-lambda-db-types/src/convert/writer/from_portfolio_level_item_to_hashmap_av.rs b/drive-deposits-lambda-db-types/src/convert/writer/from_portfolio_level_item_to_hashmap_av.rs new file mode 100644 index 0000000..7592602 --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/writer/from_portfolio_level_item_to_hashmap_av.rs @@ -0,0 +1,21 @@ +use crate::db_item_types::PortfolioLevelItem; +use aws_sdk_dynamodb::types::AttributeValue; +use std::collections::HashMap; + +impl From for HashMap { + fn from(item: PortfolioLevelItem) -> Self { + let pk_portfolios_av = AttributeValue::S(item.pk_portfolios); + let sk_growth_av = AttributeValue::S(item.sk_format_portfolio_delta_growth); + let outcome_as_json_av = AttributeValue::S(item.outcome_as_json); + let portfolio_uuid_av = AttributeValue::S(item.portfolio_uuid); + let created_at_av = AttributeValue::S(item.created_at); + + HashMap::from([ + ("PK".to_string(), pk_portfolios_av), + ("SK".to_string(), sk_growth_av), + ("portfolio_uuid".to_string(), portfolio_uuid_av), + ("outcome".to_string(), outcome_as_json_av), + ("created_at".to_string(), created_at_av), + ]) + } +} diff --git a/drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_bank_level_items_wrapper.rs b/drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_bank_level_items_wrapper.rs new file mode 100644 index 0000000..e2a5bd5 --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_bank_level_items_wrapper.rs @@ -0,0 +1,58 @@ +use crate::convert::writer::with_level_context::{ + error_with_bank_level, ItemWriterError, LevelSpecificItemWriterError, +}; +use crate::db_item_types::{BankLevelItem, BankLevelItemsWrapper}; +use drive_deposits_rest_types::rest_types::CalculatePortfolioResponse; +use rust_decimal::Decimal; +use serde_json::to_string; + +impl TryFrom<&CalculatePortfolioResponse> for BankLevelItemsWrapper { + type Error = LevelSpecificItemWriterError; + + fn try_from(rest: &CalculatePortfolioResponse) -> Result { + // for a specific response + let portfolio_uuid = rest.uuid.clone(); + let pk_portfolio_uuid = format!("PORTFOLIO#UUID#{}", portfolio_uuid); + let created_at = rest.created_at.clone(); + + let items = rest + .banks + .iter() + .map(|bank| { + let outcome = bank.outcome.as_ref().ok_or_else(|| { + error_with_bank_level(ItemWriterError::MissingDataField("outcome".to_string())) + })?; + let delta = outcome.delta.as_ref().ok_or_else(|| { + error_with_bank_level(ItemWriterError::MissingDataField("delta".to_string())) + })?; + let growth_padded = format!( + "{:020.2}", + delta + .growth + .parse::() + .map_err(error_with_bank_level)? + ); + let bank_uuid = bank.uuid.clone(); + let sk_format_bank_delta_growth = format!( + "BANK#PERIOD#{}#PERIOD_UNIT#{}#GROWTH#{}#PORTFOLIO_CREATED_AT#{}BANK_UUID#{}", + delta.period, delta.period_unit, growth_padded, created_at, bank_uuid + ); + let bank_name = bank.name.clone(); + let portfolio_uuid = portfolio_uuid.clone(); + let bank_tz = bank.bank_tz.clone(); + let outcome_as_json = to_string(&bank.outcome).map_err(error_with_bank_level)?; + Ok(BankLevelItem { + pk_format_portfolio_uuid: pk_portfolio_uuid.clone(), + sk_format_bank_delta_growth, + bank_uuid, + bank_name, + portfolio_uuid, + bank_tz, + outcome_as_json, + created_at: created_at.clone(), + }) + }) + .collect::, LevelSpecificItemWriterError>>()?; + Ok(Self { items }) + } +} diff --git a/drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_deposit_level_items_wrapper.rs b/drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_deposit_level_items_wrapper.rs new file mode 100644 index 0000000..324cb8d --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_deposit_level_items_wrapper.rs @@ -0,0 +1,107 @@ +use crate::convert::writer::with_level_context::{ + error_with_deposit_level, ItemWriterError, LevelSpecificItemWriterError, +}; +use crate::db_item_types::{ + CalculatePortfolioRestWrapper, DepositLevelItem, DepositLevelItemsWrapper, DepositSortCriteria, +}; +use rust_decimal::Decimal; +use serde_json::to_string; +use tracing::info; + +impl TryFrom<&CalculatePortfolioRestWrapper> for DepositLevelItemsWrapper { + type Error = LevelSpecificItemWriterError; + + fn try_from(rest_wrapper: &CalculatePortfolioRestWrapper) -> Result { + info!("Converting CalculatePortfolioRestWrapper to DepositLevelItemsWrapper"); + info!( + "CalculatePortfolioRestWrapper rest_wrapper: {:#?}", + rest_wrapper + ); + let deposit_sort_criteria = &rest_wrapper.deposit_sort_criteria; + let rest = &rest_wrapper.calculate_portfolio_response; + let portfolio_uuid = rest.uuid.clone(); + let pk_format_portfolio_uuid = format!("PORTFOLIO#UUID#{}", portfolio_uuid); + + let bank_deposit_level_items_per_bank_iter = rest.banks.iter().map(|bank| { + let bank_deposit_level_items_per_bank_iter = bank.deposits.iter().map( + |deposit| -> Result { + let created_at = rest.created_at.clone(); + let outcome = deposit + .outcome + .as_ref() + .ok_or_else(|| error_with_deposit_level(ItemWriterError::MissingDataField("outcome".to_string())))?; + let delta = outcome + .delta + .as_ref() + .ok_or_else(|| error_with_deposit_level(ItemWriterError::MissingDataField("delta".to_string())))?; + let deposit_delta_growth = delta.growth.clone(); + let growth_padded = format!("{:020.2}", deposit_delta_growth.parse::().map_err(error_with_deposit_level)?); + let outcome_with_dates = deposit + .outcome_with_dates + .as_ref() + .ok_or_else(|| error_with_deposit_level(ItemWriterError::MissingDataField("outcome_with_dates".to_string())))?; + + let deposit_maturity_date_in_bank_tz = outcome_with_dates + .maturity_date_in_bank_tz + .as_ref() + .ok_or_else(|| error_with_deposit_level(ItemWriterError::MissingDataField("maturity_date_in_bank_tz".to_string())))? + .clone(); + let deposit_uuid = deposit.uuid.clone(); + let sk_format_deposit_sort_criteria = match deposit_sort_criteria { + DepositSortCriteria::DeltaPeriodGrowth => format!( + "DEPOSIT#PERIOD#{}#PERIOD_UNIT#{}#GROWTH#{}#PORTFOLIO_CREATED_AT#{}#DEPOSIT_UUID#{}", + delta.period, delta.period_unit, growth_padded, created_at, deposit_uuid + ), + DepositSortCriteria::MaturityDate => format!( + "DEPOSIT#MATURITY_DATE#{}#PORTFOLIO_CREATED_AT#{}#DEPOSIT_UUID#{}", + deposit_maturity_date_in_bank_tz, rest.created_at, deposit_uuid + ) + }; + + let bank_uuid = bank.uuid.clone(); + let bank_name = bank.name.clone(); + let portfolio_uuid = portfolio_uuid.clone(); + let account = deposit.account.clone(); + let account_type = deposit.account_type.clone(); + let apy = deposit.apy.clone(); + let years = deposit.years.clone(); + let outcome_as_json = to_string(&deposit.outcome).map_err(error_with_deposit_level)?; + let outcome_with_dates_as_json = to_string(&deposit.outcome_with_dates).map_err(error_with_deposit_level)?; + + Ok(DepositLevelItem { + pk_format_portfolio_uuid: pk_format_portfolio_uuid.clone(), + sk_format_deposit_sort_criteria, + deposit_delta_growth, + deposit_maturity_date_in_bank_tz, + bank_uuid, + bank_name, + portfolio_uuid, + deposit_uuid, + account, + account_type, + apy, + years, + outcome_as_json, + outcome_with_dates_as_json, + created_at, + }) + }, + ); + + // collect for all bank deposit items for a specific bank + let bank_deposit_level_items_per_bank = bank_deposit_level_items_per_bank_iter + .collect::, LevelSpecificItemWriterError>>()?; + Ok(bank_deposit_level_items_per_bank) + }); + // collect all bank deposit items for all banks + let deposit_level_items_all_banks = bank_deposit_level_items_per_bank_iter + .collect::>, LevelSpecificItemWriterError>>()?; + let deposit_level_items_all_banks_flattened = deposit_level_items_all_banks + .into_iter() + .flatten() + .collect::>(); + Ok(DepositLevelItemsWrapper { + deposit_level_items: deposit_level_items_all_banks_flattened, + }) + } +} diff --git a/drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_portfolio_level_item.rs b/drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_portfolio_level_item.rs new file mode 100644 index 0000000..d352aae --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/writer/from_rest_to_portfolio_level_item.rs @@ -0,0 +1,55 @@ +use crate::convert::writer::with_level_context::{ + error_with_portfolio_level, ItemWriterError, LevelSpecificItemWriterError, +}; +use crate::db_item_types::PortfolioLevelItem; +use drive_deposits_rest_types::rest_types::CalculatePortfolioResponse; +use rust_decimal::Decimal; +use serde_json::to_string; + +impl TryFrom<&CalculatePortfolioResponse> for PortfolioLevelItem { + type Error = LevelSpecificItemWriterError; + + fn try_from(rest: &CalculatePortfolioResponse) -> Result { + let outcome_as_json = to_string(&rest.outcome).map_err(error_with_portfolio_level)?; + let outcome = rest.outcome.as_ref().ok_or_else(|| { + error_with_portfolio_level(ItemWriterError::MissingDataField("outcome".to_string())) + })?; + // for different responses + let pk_portfolios = "PORTFOLIOS".to_string(); + + let delta = outcome.delta.as_ref().ok_or_else(|| { + error_with_portfolio_level(ItemWriterError::MissingDataField("delta".to_string())) + })?; + // padding numeric values with leading zeros to ensure correct lexicographical sorting is a common practice in the real world, especially in systems like DynamoDB where sorting is based on string comparison. This approach ensures that numeric values are sorted correctly when stored as strings. + // the format string {:010.2} means: + // 0: Pad with leading zeros. + // 10: The total width of the formatted number, including the decimal point and the digits after it. + // .2: Two digits after the decimal point. + // So, for example, the number 367.76 would be formatted as 0000367.76. + + let growth_padded = format!( + "{:020.2}", + delta + .growth + .parse::() + .map_err(error_with_portfolio_level)? + ); + // used with pk_response to sort response_delta_growth overall for response level comparison for + // different responses overall + // SK RESPONSE_PERIOD#{}#PERIOD_UNIT#{}#GROWTH#{}CREATED_AT#{} used with RESPONSES to sort responses delta period growth + let sk_format_portfolio_delta_growth = format!( + "PORTFOLIO#PERIOD#{}#PERIOD_UNIT#{}#GROWTH#{}#PORTFOLIO_CREATED_AT#{}", + delta.period, delta.period_unit, growth_padded, rest.created_at + ); + let created_at = rest.created_at.clone(); + let portfolio_uuid = rest.uuid.clone(); + + Ok(Self { + pk_portfolios, + sk_format_portfolio_delta_growth, + portfolio_uuid, + outcome_as_json, + created_at, + }) + } +} diff --git a/drive-deposits-lambda-db-types/src/convert/writer/with_level_context.rs b/drive-deposits-lambda-db-types/src/convert/writer/with_level_context.rs new file mode 100644 index 0000000..07023c3 --- /dev/null +++ b/drive-deposits-lambda-db-types/src/convert/writer/with_level_context.rs @@ -0,0 +1,79 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum WriterErrorLevel { + #[error("Bank")] + Bank, + #[error("Deposit")] + Deposit, + #[error("Portfolio")] + Portfolio, +} + +#[derive(Debug, Error)] +pub enum ItemWriterError { + #[error("Missing data field: {0}")] + MissingDataField(String), +} + +#[derive(Debug, Error)] +pub enum LevelSpecificItemWriterError { + #[error("{1} level: {0}")] + Error(#[source] ItemWriterError, WriterErrorLevel), + #[error("{1} level conversion DecimalError: {0}")] + DecimalError(#[source] rust_decimal::Error, WriterErrorLevel), + #[error("{1} level conversion SerdeJsonError: {0}")] + JsonError(#[source] serde_json::Error, WriterErrorLevel), +} + +// pub fn error_with_level(err: E, level: WriterErrorLevel) -> LevelSpecificItemWriterError +// where +// (E, WriterErrorLevel): Into, +// { +// (err, level).into() +// } +pub fn error_with_level(err: E, level: WriterErrorLevel) -> LevelSpecificItemWriterError +where + //means that the type (E, WriterErrorLevel) must implement the From trait for LevelSpecificItemWriterError + LevelSpecificItemWriterError: From<(E, WriterErrorLevel)>, +{ + LevelSpecificItemWriterError::from((err, level)) +} + +pub fn error_with_portfolio_level(err: E) -> LevelSpecificItemWriterError +where + LevelSpecificItemWriterError: From<(E, WriterErrorLevel)>, +{ + error_with_level(err, WriterErrorLevel::Portfolio) +} + +pub fn error_with_bank_level(err: E) -> LevelSpecificItemWriterError +where + LevelSpecificItemWriterError: From<(E, WriterErrorLevel)>, +{ + error_with_level(err, WriterErrorLevel::Bank) +} + +pub fn error_with_deposit_level(err: E) -> LevelSpecificItemWriterError +where + LevelSpecificItemWriterError: From<(E, WriterErrorLevel)>, +{ + error_with_level(err, WriterErrorLevel::Deposit) +} +impl From<(ItemWriterError, WriterErrorLevel)> for LevelSpecificItemWriterError { + fn from((err, level): (ItemWriterError, WriterErrorLevel)) -> Self { + Self::Error(err, level) + } +} + +impl From<(rust_decimal::Error, WriterErrorLevel)> for LevelSpecificItemWriterError { + fn from((err, level): (rust_decimal::Error, WriterErrorLevel)) -> Self { + Self::DecimalError(err, level) + } +} + +impl From<(serde_json::Error, WriterErrorLevel)> for LevelSpecificItemWriterError { + fn from((err, level): (serde_json::Error, WriterErrorLevel)) -> Self { + Self::JsonError(err, level) + } +} diff --git a/drive-deposits-lambda-db-types/src/db_item_types.rs b/drive-deposits-lambda-db-types/src/db_item_types.rs new file mode 100644 index 0000000..cae563a --- /dev/null +++ b/drive-deposits-lambda-db-types/src/db_item_types.rs @@ -0,0 +1,74 @@ +// Item is used in DynamoDB to format data for write items so could be queries using Single Table Design +// Here we are converting to specific format for write items so could be queries using Single Table Design + +// access patterns determine types and the structure of the data + +use drive_deposits_rest_types::rest_types::CalculatePortfolioResponse; +use serde::Serialize; + +// ResponseLevelItem means deals with responses so RESPONSES as pk has many response items +#[derive(Debug, Default, Serialize)] +pub struct PortfolioLevelItem { + pub pk_portfolios: String, + pub sk_format_portfolio_delta_growth: String, + pub portfolio_uuid: String, + pub outcome_as_json: String, + pub created_at: String, +} + +#[derive(Debug, Default)] +pub struct BankLevelItemsWrapper { + pub items: Vec, +} + +// BankLevelItem means deals with banks for a specific response uuid so RESPONSE#UUID#{} as pk has many bank items +#[derive(Debug, Default)] +pub struct BankLevelItem { + pub pk_format_portfolio_uuid: String, + pub sk_format_bank_delta_growth: String, + pub bank_uuid: String, + pub bank_name: String, + pub portfolio_uuid: String, + pub bank_tz: String, + pub outcome_as_json: String, + pub created_at: String, +} + +#[derive(Debug, Default)] +pub struct DepositLevelItemsWrapper { + pub deposit_level_items: Vec, +} + +// BanksDepositLevelItem means deals with bank deposits for a specific response uuid so RESPONSE#UUID#{} as pk has many bank-deposits items + +#[derive(Debug, Default)] +pub enum DepositSortCriteria { + #[default] + DeltaPeriodGrowth, + MaturityDate, +} + +#[derive(Debug, Default)] +pub struct CalculatePortfolioRestWrapper { + pub calculate_portfolio_response: CalculatePortfolioResponse, + pub deposit_sort_criteria: DepositSortCriteria, +} + +#[derive(Debug, Default)] +pub struct DepositLevelItem { + pub pk_format_portfolio_uuid: String, + pub sk_format_deposit_sort_criteria: String, + pub deposit_delta_growth: String, + pub deposit_maturity_date_in_bank_tz: String, + pub created_at: String, + pub bank_uuid: String, + pub bank_name: String, + pub portfolio_uuid: String, + pub deposit_uuid: String, + pub account: String, + pub account_type: String, + pub apy: String, + pub years: String, + pub outcome_as_json: String, + pub outcome_with_dates_as_json: String, +} diff --git a/drive-deposits-lambda-db-types/src/lib.rs b/drive-deposits-lambda-db-types/src/lib.rs new file mode 100644 index 0000000..d97a5cf --- /dev/null +++ b/drive-deposits-lambda-db-types/src/lib.rs @@ -0,0 +1,3 @@ +pub mod convert; +pub mod db_item_types; +pub mod query_response_types; diff --git a/drive-deposits-lambda-db-types/src/query_response_types.rs b/drive-deposits-lambda-db-types/src/query_response_types.rs new file mode 100644 index 0000000..24f4449 --- /dev/null +++ b/drive-deposits-lambda-db-types/src/query_response_types.rs @@ -0,0 +1,112 @@ +use drive_deposits_rest_types::rest_types::{Outcome, OutcomeWithDates}; +use serde::{Deserialize, Serialize}; + +pub const DEFAULT_TOP_K: usize = 2; +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct ItemParamsRequest { + pub order: Option, + pub top_k: Option, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Order { + #[default] + #[serde(rename = "desc")] + Descending, + #[serde(rename = "asc")] + Ascending, +} + +impl From<&Order> for bool { + fn from(order: &Order) -> bool { + match order { + Order::Ascending => true, + Order::Descending => false, + } + } +} + +#[derive(Debug, Default, Serialize)] +pub struct Metadata { + pub order: Order, + pub top_k: usize, + pub sort_criteria_description: String, +} + +// REST API response: Calculation results sorted by delta period growth at the portfolios level +// delta period growth is the default order criteria +#[derive(Debug, Default, Serialize)] +pub struct ByLevelForPortfolios { + pub data: PortfolioData, + pub metadata: Metadata, +} + +#[derive(Debug, Default, Serialize)] +pub struct PortfolioData { + pub responses: Vec, + pub count_responses: usize, +} + +#[derive(Debug, Default, Serialize)] +pub struct PortfolioResponse { + pub portfolio_uuid: String, + pub outcome: Outcome, + pub created_at: String, +} + +// REST API response: Calculation results sorted by delta period growth at the banks level for a given portfolio +// delta period growth is the default order criteria +#[derive(Debug, Default, Serialize)] +pub struct ByLevelForBanks { + pub data: BankData, + pub metadata: Metadata, +} + +#[derive(Debug, Default, Serialize)] +pub struct BankData { + pub responses: Vec, + pub count_responses: usize, +} + +#[derive(Debug, Default, Serialize)] +pub struct BankResponse { + pub bank_uuid: String, + pub bank_name: String, + pub portfolio_uuid: String, + pub bank_tz: String, + pub outcome: Outcome, + pub created_at: String, +} + +// handles 2 rest api responses at the deposits level: +// REST API response: Calculation results sorted by delta period growth at the deposits level for all banks for a given portfolio +// REST API response: Calculation results sorted by maturity date at the deposits level for all banks for a given portfolio +#[derive(Debug, Default, Serialize)] +pub struct ByLevelForDeposits { + pub data: DepositData, + pub metadata: Metadata, +} + +#[derive(Debug, Default, Serialize)] +pub struct DepositData { + pub responses: Vec, + pub count_responses: usize, +} + +#[derive(Debug, Default, Serialize)] +pub struct DepositResponse { + pub deposit_uuid: String, + pub bank_uuid: String, + pub bank_name: String, + pub portfolio_uuid: String, + pub account: String, + pub account_type: String, + pub apy: String, + pub years: String, + pub deposit_delta_growth: String, + pub deposit_maturity_date_in_bank_tz: String, + pub outcome: Outcome, + pub outcome_with_dates: OutcomeWithDates, + pub created_at: String, +} diff --git a/drive-deposits-lambda-dynamodb-reader/.gitignore b/drive-deposits-lambda-dynamodb-reader/.gitignore new file mode 100644 index 0000000..d86e952 --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/.gitignore @@ -0,0 +1,3 @@ +/target +/.aws-sam +out.json \ No newline at end of file diff --git a/drive-deposits-lambda-dynamodb-reader/Cargo.toml b/drive-deposits-lambda-dynamodb-reader/Cargo.toml new file mode 100644 index 0000000..20cb764 --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "drive-deposits-lambda-dynamodb-reader" +version = "0.10.0" +edition = "2021" + +# Starting in Rust 1.62 you can use `cargo add` to add dependencies +# to your project. +# +# If you're using an older Rust version, +# download cargo-edit(https://github.com/killercup/cargo-edit#installation) +# to install the `add` subcommand. +# +# Running `cargo add DEPENDENCY_NAME` will +# add the latest version of a dependency to the list, +# and it will keep the alphabetic ordering for you. + +[dependencies] +aws-config = { version = "1.5.4", features = ["behavior-version-latest"] } +aws-sdk-dynamodb = "1.39.1" +lambda_http = "0.13.0" +thiserror = "1.0.63" +tracing = "0.1.40" +axum = { version = "0.7.5", features = ["macros"] } +serde = { version = "1.0.207", features = ["derive"] } +serde_json = "1.0.124" +tokio = { version = "1", features = ["macros"] } +# workspace member depdenencies +drive-deposits-lambda-db-types = { path = "../drive-deposits-lambda-db-types" } + +# uuid added in different style +[dependencies.uuid] +version = "1.10.0" +features = ["v4"] + +[[bin]] +name = "by_level_lambda_reader" +path = "src/bin/by_level_lambda_reader.rs" diff --git a/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-banks-delta-growth.json b/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-banks-delta-growth.json new file mode 100644 index 0000000..7d4aa9e --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-banks-delta-growth.json @@ -0,0 +1,135 @@ +{ + "resource": "/{proxy+}", + "path": "/portfolios/fe57674d-fe0d-41b5-bf5b-526bd72925bd/by-level-for-banks/delta-growth", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Content-Type": "application/json", + "headerName": "headerValue", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "cache-control": [ + "no-cache" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Content-Type": [ + "application/json" + ], + "headerName": [ + "headerValue" + ], + "Host": [ + "gy415nuibc.execute-api.us-east-1.amazonaws.com" + ], + "Postman-Token": [ + "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f" + ], + "User-Agent": [ + "PostmanRuntime/2.4.5" + ], + "Via": [ + "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==" + ], + "X-Forwarded-For": [ + "54.240.196.186, 54.182.214.83" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "multiValueQueryStringParameters": { + "order": [ + "desc" + ], + "top_k": [ + "3" + ] + }, + "pathParameters": { + "pk_portfolio_uuid": "ab7df69c-5ff0-40e8-b2ce-9bab9e0a5af0" + }, + "stageVariables": { + "stageVariableName": "stageVariableValue" + }, + "requestContext": { + "accountId": "12345678912", + "resourceId": "roq9wj", + "path": "/by-level-for-banks/delta-growth", + "stage": "testStage", + "domainName": "gy415nuibc.execute-api.us-east-2.amazonaws.com", + "domainPrefix": "y0ne18dixk", + "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", + "protocol": "HTTP/1.1", + "identity": { + "cognitoIdentityPoolId": "theCognitoIdentityPoolId", + "accountId": "theAccountId", + "cognitoIdentityId": "theCognitoIdentityId", + "caller": "theCaller", + "apiKey": "theApiKey", + "apiKeyId": "theApiKeyId", + "accessKey": "ANEXAMPLEOFACCESSKEY", + "sourceIp": "192.168.196.186", + "cognitoAuthenticationType": "theCognitoAuthenticationType", + "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", + "userArn": "theUserArn", + "userAgent": "PostmanRuntime/2.4.5", + "user": "theUser" + }, + "authorizer": { + "principalId": "admin", + "clientId": 1, + "clientName": "Exata" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestTime": "15/May/2020:06:01:09 +0000", + "requestTimeEpoch": 1589522469693, + "apiId": "gy415nuibc" + }, + "body": "{\r\n\t\"a\": 1\r\n}" +} \ No newline at end of file diff --git a/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-deposits-delta-growth.json b/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-deposits-delta-growth.json new file mode 100644 index 0000000..4717890 --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-deposits-delta-growth.json @@ -0,0 +1,135 @@ +{ + "resource": "/{proxy+}", + "path": "/portfolios/fe57674d-fe0d-41b5-bf5b-526bd72925bd/by-level-for-deposits/delta-growth", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Content-Type": "application/json", + "headerName": "headerValue", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "cache-control": [ + "no-cache" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Content-Type": [ + "application/json" + ], + "headerName": [ + "headerValue" + ], + "Host": [ + "gy415nuibc.execute-api.us-east-1.amazonaws.com" + ], + "Postman-Token": [ + "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f" + ], + "User-Agent": [ + "PostmanRuntime/2.4.5" + ], + "Via": [ + "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==" + ], + "X-Forwarded-For": [ + "54.240.196.186, 54.182.214.83" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "multiValueQueryStringParameters": { + "order": [ + "desc" + ], + "top_k": [ + "3" + ] + }, + "pathParameters": { + "pk_portfolio_uuid": "95b6d786-640b-4a4b-9751-87dceabf21d7" + }, + "stageVariables": { + "stageVariableName": "stageVariableValue" + }, + "requestContext": { + "accountId": "12345678912", + "resourceId": "roq9wj", + "path": "/by-level-for-banks/delta-growth", + "stage": "testStage", + "domainName": "gy415nuibc.execute-api.us-east-2.amazonaws.com", + "domainPrefix": "y0ne18dixk", + "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", + "protocol": "HTTP/1.1", + "identity": { + "cognitoIdentityPoolId": "theCognitoIdentityPoolId", + "accountId": "theAccountId", + "cognitoIdentityId": "theCognitoIdentityId", + "caller": "theCaller", + "apiKey": "theApiKey", + "apiKeyId": "theApiKeyId", + "accessKey": "ANEXAMPLEOFACCESSKEY", + "sourceIp": "192.168.196.186", + "cognitoAuthenticationType": "theCognitoAuthenticationType", + "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", + "userArn": "theUserArn", + "userAgent": "PostmanRuntime/2.4.5", + "user": "theUser" + }, + "authorizer": { + "principalId": "admin", + "clientId": 1, + "clientName": "Exata" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestTime": "15/May/2020:06:01:09 +0000", + "requestTimeEpoch": 1589522469693, + "apiId": "gy415nuibc" + }, + "body": "{\r\n\t\"a\": 1\r\n}" +} \ No newline at end of file diff --git a/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-deposits-maturity-date.json b/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-deposits-maturity-date.json new file mode 100644 index 0000000..2e10890 --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-deposits-maturity-date.json @@ -0,0 +1,135 @@ +{ + "resource": "/{proxy+}", + "path": "/portfolios/fe57674d-fe0d-41b5-bf5b-526bd72925bd/by-level-for-deposits/maturity-date", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Content-Type": "application/json", + "headerName": "headerValue", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "cache-control": [ + "no-cache" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Content-Type": [ + "application/json" + ], + "headerName": [ + "headerValue" + ], + "Host": [ + "gy415nuibc.execute-api.us-east-1.amazonaws.com" + ], + "Postman-Token": [ + "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f" + ], + "User-Agent": [ + "PostmanRuntime/2.4.5" + ], + "Via": [ + "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==" + ], + "X-Forwarded-For": [ + "54.240.196.186, 54.182.214.83" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "multiValueQueryStringParameters": { + "order": [ + "desc" + ], + "top_k": [ + "3" + ] + }, + "pathParameters": { + "pk_portfolio_uuid": "95b6d786-640b-4a4b-9751-87dceabf21d7" + }, + "stageVariables": { + "stageVariableName": "stageVariableValue" + }, + "requestContext": { + "accountId": "12345678912", + "resourceId": "roq9wj", + "path": "/by-level-for-banks/delta-growth", + "stage": "testStage", + "domainName": "gy415nuibc.execute-api.us-east-2.amazonaws.com", + "domainPrefix": "y0ne18dixk", + "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", + "protocol": "HTTP/1.1", + "identity": { + "cognitoIdentityPoolId": "theCognitoIdentityPoolId", + "accountId": "theAccountId", + "cognitoIdentityId": "theCognitoIdentityId", + "caller": "theCaller", + "apiKey": "theApiKey", + "apiKeyId": "theApiKeyId", + "accessKey": "ANEXAMPLEOFACCESSKEY", + "sourceIp": "192.168.196.186", + "cognitoAuthenticationType": "theCognitoAuthenticationType", + "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", + "userArn": "theUserArn", + "userAgent": "PostmanRuntime/2.4.5", + "user": "theUser" + }, + "authorizer": { + "principalId": "admin", + "clientId": 1, + "clientName": "Exata" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestTime": "15/May/2020:06:01:09 +0000", + "requestTimeEpoch": 1589522469693, + "apiId": "gy415nuibc" + }, + "body": "{\r\n\t\"a\": 1\r\n}" +} \ No newline at end of file diff --git a/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-portfolios-delta-growth.json b/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-portfolios-delta-growth.json new file mode 100644 index 0000000..8884a3a --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/data/apigw-event-request-query-for-portfolios-delta-growth.json @@ -0,0 +1,135 @@ +{ + "resource": "/{proxy+}", + "path": "/by-level-for-portfolios/delta-growth", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Content-Type": "application/json", + "headerName": "headerValue", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "cache-control": [ + "no-cache" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Content-Type": [ + "application/json" + ], + "headerName": [ + "headerValue" + ], + "Host": [ + "gy415nuibc.execute-api.us-east-1.amazonaws.com" + ], + "Postman-Token": [ + "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f" + ], + "User-Agent": [ + "PostmanRuntime/2.4.5" + ], + "Via": [ + "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==" + ], + "X-Forwarded-For": [ + "54.240.196.186, 54.182.214.83" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "multiValueQueryStringParameters": { + "order": [ + "desc" + ], + "top_k": [ + "3" + ] + }, + "pathParameters": { + "proxy": "hello/world" + }, + "stageVariables": { + "stageVariableName": "stageVariableValue" + }, + "requestContext": { + "accountId": "12345678912", + "resourceId": "roq9wj", + "path": "/by-level-for-portfolios/delta-growth", + "stage": "testStage", + "domainName": "gy415nuibc.execute-api.us-east-2.amazonaws.com", + "domainPrefix": "y0ne18dixk", + "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", + "protocol": "HTTP/1.1", + "identity": { + "cognitoIdentityPoolId": "theCognitoIdentityPoolId", + "accountId": "theAccountId", + "cognitoIdentityId": "theCognitoIdentityId", + "caller": "theCaller", + "apiKey": "theApiKey", + "apiKeyId": "theApiKeyId", + "accessKey": "ANEXAMPLEOFACCESSKEY", + "sourceIp": "192.168.196.186", + "cognitoAuthenticationType": "theCognitoAuthenticationType", + "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", + "userArn": "theUserArn", + "userAgent": "PostmanRuntime/2.4.5", + "user": "theUser" + }, + "authorizer": { + "principalId": "admin", + "clientId": 1, + "clientName": "Exata" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestTime": "15/May/2020:06:01:09 +0000", + "requestTimeEpoch": 1589522469693, + "apiId": "gy415nuibc" + }, + "body": "{\r\n\t\"a\": 1\r\n}" +} \ No newline at end of file diff --git a/drive-deposits-lambda-dynamodb-reader/request.http b/drive-deposits-lambda-dynamodb-reader/request.http new file mode 100644 index 0000000..2527851 --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/request.http @@ -0,0 +1,59 @@ +@host = https://rcuxkiyq3l.execute-api.us-west-2.amazonaws.com + +### +GET {{host}}/by-level-for-portfolios/delta-growth +Content-Type: application/json + + +### +GET {{host}}/portfolios/cad34c28-2f14-49be-8f4f-30a72ea9a367/by-level-for-banks/delta-growth +Content-Type: application/json + + +### +GET {{host}}/portfolios/cad34c28-2f14-49be-8f4f-30a72ea9a367/by-level-for-deposits/delta-growth +Content-Type: application/json + + +### +GET {{host}}/portfolios/cad34c28-2f14-49be-8f4f-30a72ea9a367/by-level-for-deposits/delta-growth +Content-Type: application/json + +### +GET {{host}}/portfolios/cad34c28-2f14-49be-8f4f-30a72ea9a367/by-level-for-deposits/delta-growth/maturity-date +Content-Type: application/json + +### with localstack +GET /lambda-url/by_level_lambda_reader/by-level-for-portfolios/delta-growth?order=desc&top_k=1 HTTP/1.1 +Host: [::]:9000 + +# { +# "ordered": [ +# { +# "portfolio_uuid": "eb0dd4af-e212-4191-8135-012499338d32", +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "367.76" +# }, +# "maturity": { +# "amount": "108580.50", +# "interest": "24462.92", +# "total": "133043.42" +# }, +# "errors": [] +# } +# } +# ], +# "order": "desc", +# "top_k": 1 +#} + + +### with localstack +# /portfolios/:pk_portfolio_uuid/by-level-for-banks/delta-growth +GET /lambda-url/by_level_lambda_reader/portfolios/0d22bd83-7897-482f-89a2-e09f4af86965/by-level-for-banks/delta-growth? + order=desc& + top_k=10 HTTP/1.1 +Host: [::]:9000 \ No newline at end of file diff --git a/drive-deposits-lambda-dynamodb-reader/samconfig.toml b/drive-deposits-lambda-dynamodb-reader/samconfig.toml new file mode 100644 index 0000000..b58f475 --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/samconfig.toml @@ -0,0 +1,10 @@ +version = 0.1 +[dev.deploy.parameters] +stack_name = "drive-deposits-dynamodb-queries" +resolve_s3 = true +s3_prefix = "drive-deposits-dynamodb-queries" +region = "us-west-2" +confirm_changeset = true +capabilities = "CAPABILITY_IAM" +parameter_overrides = "Environment=\"dev\" UseLocalstack=\"false\"" +image_repositories = [] diff --git a/drive-deposits-lambda-dynamodb-reader/src/bin/by_level_lambda_reader.rs b/drive-deposits-lambda-dynamodb-reader/src/bin/by_level_lambda_reader.rs new file mode 100644 index 0000000..393ded7 --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/src/bin/by_level_lambda_reader.rs @@ -0,0 +1,230 @@ +use axum::extract::{Path, Query, State}; +use axum::routing::get; +use axum::{debug_handler, Json, Router}; +use drive_deposits_lambda_db_types::db_item_types::DepositSortCriteria; +use drive_deposits_lambda_db_types::query_response_types::{ + ByLevelForBanks, ByLevelForDeposits, ByLevelForPortfolios, ItemParamsRequest, +}; +use drive_deposits_lambda_dynamodb_reader::dynamodb::query::{ + query_banks, query_deposits, query_portfolios, +}; +use drive_deposits_lambda_dynamodb_reader::dynamodb::DriveDepositsDb; +use drive_deposits_lambda_dynamodb_reader::handler_error::Error as HandlerError; +use drive_deposits_lambda_dynamodb_reader::request_error::Error as RequestError; +use lambda_http::{ + http::StatusCode, + run, service_fn, + tracing::{debug, error, info_span, init_default_subscriber}, + Error as LambdaHttpError, Request, Service, +}; +use serde_json::{json, Value}; +use std::env::set_var; +use std::sync::Arc; +use tracing::{info, Instrument, Span}; +use uuid::Uuid; + +async fn root() -> Json { + Json( + json!({ "msg": "Use on of the the routes /by-level-for-portfolios/delta-growth; /by-level-for-banks/delta-growth/:pk_portfolio_uuid; /by-level-for-deposits/delta-growth/:pk_portfolio_uuid; /by-bank-deposits-level/maturity-date/:pk_portfolio_uuid" }), + ) +} + +async fn health_check() -> (StatusCode, String) { + let health = true; + match health { + true => (StatusCode::OK, "Healthy!".to_string()), + false => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Not healthy!".to_string(), + ), + } +} + +// #[debug_handler] +// using just Query instead of Option> would be sufficient and more straightforward. +// Since Axum always provides a Some value with an empty ItemParamsRequest when no query parameters are passed, wrapping it in an Option doesn't add any extra functionality. +async fn by_level_query_portfolios_delta_growth( + Query(query_item_params): Query, + State(db_handler): State>, +) -> Result, HandlerError> { + let span = info_span!("handler by_level_query_portfolios_delta_growth"); + span.in_scope(|| { + debug!( + "inside by_portfolios_level query_item_params is {:?}", + query_item_params + ); + }); + + let portfolios = query_portfolios( + &db_handler.dynamodb_client, + db_handler.table_name.as_str(), + query_item_params, + ) + .instrument(span) + .await?; + + Ok(Json(portfolios)) +} + +// #[debug_handler] +async fn by_level_query_banks_delta_growth( + Path(pk_portfolio_uuid): Path, + Query(query_item_params): Query, + State(db_handler): State>, +) -> Result, HandlerError> { + let span = info_span!("handler by_level_query_banks_delta_growth"); + + span.in_scope(|| { + debug!( + "pk_portfolio_uuid in banks level reader is: {:?}", + pk_portfolio_uuid + ); + debug!( + "inside by_banks_level query_item_params is {:?}", + query_item_params + ) + }); + + validate_uuid(pk_portfolio_uuid.as_str())?; + + let banks = query_banks( + &db_handler.dynamodb_client, + db_handler.table_name.as_str(), + query_item_params, + pk_portfolio_uuid, + ) + .instrument(span) + .await?; + + Ok(Json(banks)) +} + +#[debug_handler] +async fn by_level_query_deposits_delta_growth( + Path(pk_portfolio_uuid): Path, + Query(query_item_params): Query, + State(db_handler): State>, +) -> Result, HandlerError> { + let span = info_span!("handler by_level_query_deposits_delta_growth"); + + by_level_query_deposits( + Path(pk_portfolio_uuid), + Query(query_item_params), + State(db_handler), + DepositSortCriteria::DeltaPeriodGrowth, + ) + .instrument(span) + .await +} + +#[debug_handler] +async fn by_level_query_deposits_maturity_date( + Path(pk_portfolio_uuid): Path, + Query(query_item_params): Query, + State(db_handler): State>, +) -> Result, HandlerError> { + let span = info_span!("handler by_level_query_deposits_maturity_date"); + + by_level_query_deposits( + Path(pk_portfolio_uuid), + Query(query_item_params), + State(db_handler), + DepositSortCriteria::MaturityDate, + ) + .instrument(span) + .await +} + +async fn by_level_query_deposits( + Path(pk_portfolio_uuid): Path, + Query(query_item_params): Query, + State(db_handler): State>, + deposit_sort_criteria: DepositSortCriteria, +) -> Result, HandlerError> { + debug!( + "pk_portfolio_uuid in bank deposits level reader is: {:?}", + pk_portfolio_uuid + ); + debug!( + "inside by_deposits_level query_item_params is {:?}", + query_item_params + ); + debug!( + "deposit_sort_criteria in bank deposits level reader is: {:?}", + deposit_sort_criteria + ); + + validate_uuid(pk_portfolio_uuid.as_str())?; + + let deposits = query_deposits( + &db_handler.dynamodb_client, + db_handler.table_name.as_str(), + query_item_params, + pk_portfolio_uuid, + deposit_sort_criteria, + ) + .instrument(Span::current()) + .await?; + + Ok(Json(deposits)) +} + +fn validate_uuid(uuid: &str) -> Result<(), RequestError> { + if Uuid::try_parse(uuid).is_err() { + return Err(RequestError::InvalidUuid(uuid.to_string())); + } + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), LambdaHttpError> { + println!("before init tracing subscriber"); + init_default_subscriber(); + + let span = info_span!("by_level_lambda_reader_main"); + span.in_scope(|| debug!("after init_default_subscriber")); + + // add AWS_LAMBDA_HTTP_IGNORE_STAGE_IN_PATH: for cargo lambda invoke also + // If you use API Gateway stages, the Rust Runtime will include the stage name + // as part of the path that your application receives. + // Setting the following environment variable, you can remove the stage from the path. + // This variable only applies to API Gateway stages, + // you can remove it if you don't use them. + // i.e with: `GET /test-stage/todo/id/123` without: `GET /todo/id/123` + set_var("AWS_LAMBDA_HTTP_IGNORE_STAGE_IN_PATH", "true"); + + let db_handler = Arc::new( + DriveDepositsDb::handler() + .await + .inspect_err(|err| error!("DriveDepositsDb::handler error is {}", err))?, + ); + + let mut app_router: Router<()> = Router::new() + .route("/", get(root)) + .route("/health", get(health_check)) + .route( + "/by-level-for-portfolios/delta-growth", + get(by_level_query_portfolios_delta_growth), + ) + .route( + "/portfolios/:pk_portfolio_uuid/by-level-for-banks/delta-growth", + get(by_level_query_banks_delta_growth), + ) + .route( + "/portfolios/:pk_portfolio_uuid/by-level-for-deposits/delta-growth", + get(by_level_query_deposits_delta_growth), + ) + .route( + "/portfolios/:pk_portfolio_uuid/by-level-for-deposits/maturity-date", + get(by_level_query_deposits_maturity_date), + ) + .with_state(db_handler); + + run(service_fn(|event: Request| { + info!("api gateway event: {:?}", event); + app_router.call(event) + })) + .await?; + + Ok(()) +} diff --git a/drive-deposits-lambda-dynamodb-reader/src/dynamodb.rs b/drive-deposits-lambda-dynamodb-reader/src/dynamodb.rs new file mode 100644 index 0000000..bf9c7a9 --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/src/dynamodb.rs @@ -0,0 +1,78 @@ +use std::env; +use std::env::var; + +use aws_config::BehaviorVersion; +use aws_sdk_dynamodb::operation::describe_table::DescribeTableError; +use aws_sdk_dynamodb::Client; +use lambda_http::tracing::{debug, error, info, info_span, instrument}; +use thiserror::Error; + +pub mod query; + +const LOCALSTACK_ENDPOINT: &str = "http://localhost.localstack.cloud:4566/"; + +#[derive(Default, Debug, Error)] +pub enum DbError { + #[default] + #[error("DbError DynamoDb client error")] + DynamoDbClientError, + + #[error("DbError Sdk error is: {0}")] + SdkError(#[from] aws_sdk_dynamodb::error::SdkError), + + #[error("DbError Env var error")] + VarError(#[from] env::VarError), +} + +pub struct DriveDepositsDb { + pub dynamodb_client: Client, + pub table_name: String, +} + +impl DriveDepositsDb { + fn new(table_name: String, dynamodb_client: Client) -> Self { + Self { + dynamodb_client, + table_name, + } + } + + pub async fn handler() -> Result { + let span = info_span!("banks_level_db_handler"); + let table_name = var("DRIVE_DEPOSITS_TABLE_NAME").inspect_err(|err| { + span.in_scope(|| { + error!( + "var error for DRIVE_DEPOSITS_TABLE_NAME {:?}, so returning", + err + ) + }) + })?; + span.in_scope(|| debug!("dynamodb drive deposits table_name: {:?}", table_name)); + let mut config_loader = aws_config::defaults(BehaviorVersion::latest()); + if use_localstack() { + config_loader = config_loader.endpoint_url(LOCALSTACK_ENDPOINT); + }; + let config = config_loader.load().await; + let dynamodb_client = Client::new(&config); + let described_table_output = dynamodb_client + .describe_table() + .table_name(&table_name) + .send() + .await?; + info!("described_table_output: {:?}", described_table_output); + span.in_scope(|| debug!("dynamodb client got!")); + Ok(Self::new(table_name, dynamodb_client)) + } +} + +fn env_var_bool(name: &str) -> bool { + let env_val = var(name).unwrap_or_else(|_| "false".to_string()); + env_val.eq_ignore_ascii_case("true") +} + +#[instrument] +fn use_localstack() -> bool { + let use_localstack = env_var_bool("USE_LOCALSTACK"); + debug!("use_localstack env variable is: {}", use_localstack); + use_localstack +} diff --git a/drive-deposits-lambda-dynamodb-reader/src/dynamodb/query.rs b/drive-deposits-lambda-dynamodb-reader/src/dynamodb/query.rs new file mode 100644 index 0000000..a5cba88 --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/src/dynamodb/query.rs @@ -0,0 +1,296 @@ +use aws_sdk_dynamodb::error::SdkError; +use aws_sdk_dynamodb::operation::query::builders::QueryFluentBuilder; +use aws_sdk_dynamodb::operation::query::QueryError; +use aws_sdk_dynamodb::types::AttributeValue; +use aws_sdk_dynamodb::Client; +use drive_deposits_lambda_db_types::convert::reader::with_level_context::LevelSpecificResponseReaderError; +use drive_deposits_lambda_db_types::{ + convert::reader::with_level_context::LevelSpecificItemReaderError, + db_item_types::{BankLevelItem, PortfolioLevelItem}, + db_item_types::{DepositLevelItem, DepositSortCriteria}, + query_response_types::{ + BankData, BankResponse, ByLevelForBanks, ByLevelForPortfolios, ItemParamsRequest, Metadata, + PortfolioData, PortfolioResponse, DEFAULT_TOP_K, + }, + query_response_types::{ByLevelForDeposits, DepositData, DepositResponse}, +}; +use thiserror::Error; +use tracing::{debug, error, info}; + +#[derive(Error, Debug)] +pub enum QueryItemError { + #[error("QueryItemError with DynamoDbSdkError error: {0}")] + DynamoDbSdkError(#[from] SdkError), + + #[error("QueryItemError with QueryNoItemFound error: {0}")] + QueryNoItemFound(String), + + #[error("QueryItemError with item reader error: {0}")] + LevelSpecificItemReaderError(#[from] LevelSpecificItemReaderError), + + #[error("QueryItemError with response reader error: {0}")] + LevelSpecificResponseReaderError(#[from] LevelSpecificResponseReaderError), +} + +pub async fn query_portfolios( + client: &Client, + table: &str, + query_item_params: ItemParamsRequest, +) -> Result { + info!("in query_portfolios"); + info!("query_item_params is {:?}", query_item_params); + let order = query_item_params.order.unwrap_or_default(); + info!("taking order for response: {:?}", order); + let top_k = query_item_params.top_k.unwrap_or(DEFAULT_TOP_K); + info!("taking top_k number k items for response: {:?}", top_k); + + let pk = "PK".to_string(); + let forward = bool::from(&order); + let request = client + .query() + .table_name(table) + .key_condition_expression("#partitionKeyName = :partitionKeyValue".to_string()) + .expression_attribute_names("#partitionKeyName".to_string(), pk) + .expression_attribute_values( + ":partitionKeyValue".to_string(), + AttributeValue::S("PORTFOLIOS".to_string()), + ) + .scan_index_forward(forward); + let query_resp = request.send().await?; + let sort_criteria_description = + "Calculation results sorted by delta period growth for the overall calculation at the portfolios level" + .to_string(); + let metadata = Metadata { + order, + top_k, + sort_criteria_description, + }; + if query_resp.count == 0 { + return Ok(ByLevelForPortfolios { + data: PortfolioData { + responses: vec![], + count_responses: 0, + }, + metadata, + }); + } + + let db_items = query_resp.items.ok_or_else(|| { + QueryItemError::QueryNoItemFound("In query_portfolios_level_item".to_string()) + })?; + debug!("db_items is {:?}", db_items); + let portfolio_level_items = db_items + .into_iter() + .take(top_k) + .map(PortfolioLevelItem::try_from) + .collect::, LevelSpecificItemReaderError>>()?; + let portfolio_responses = portfolio_level_items + .into_iter() + .map(PortfolioResponse::try_from) + .inspect(|responses_level| info!("Ordered responses_level: {:?}", responses_level)) + .collect::, LevelSpecificResponseReaderError>>()?; + let count_responses = portfolio_responses.len(); + let by_level_for_portfolios = ByLevelForPortfolios { + data: PortfolioData { + responses: portfolio_responses, + count_responses, + }, + metadata, + }; + + Ok(by_level_for_portfolios) +} + +pub async fn query_banks( + client: &Client, + table: &str, + query_item_params: ItemParamsRequest, + pk_portfolio_uuid: String, +) -> Result { + info!("in query_banks"); + info!("query_item_params is {:?}", query_item_params); + let order = query_item_params.order.unwrap_or_default(); + info!("taking order for response: {:?}", order); + let top_k = query_item_params.top_k.unwrap_or(DEFAULT_TOP_K); + info!("taking top_k number k items for response: {:?}", top_k); + + let pk = "PK".to_string(); + let sk = "SK".to_string(); + let partition_key_value = format!("PORTFOLIO#UUID#{}", pk_portfolio_uuid); + let sort_key_value = "BANK#PERIOD#".to_string(); + let forward = bool::from(&order); + let query_params = QueryParams { + table: table.to_string(), + pk, + sk, + partition_key_value, + sort_key_value, + forward, + }; + let request = build_begins_with_request_for_query(client, query_params); + let query_resp = request.send().await?; + let sort_criteria_description = format!( + "Calculation results sorted by delta period growth at the banks level for portfolio {}", + pk_portfolio_uuid + ); + let metadata = Metadata { + order, + top_k, + sort_criteria_description, + }; + info!("query_resp.count is {:?}", query_resp.count); + if query_resp.count == 0 { + return Ok(ByLevelForBanks { + data: BankData { + responses: vec![], + count_responses: 0, + }, + metadata, + }); + } + + let db_items = query_resp + .items + .ok_or_else(|| QueryItemError::QueryNoItemFound("In query_banks_level_item".to_string()))?; + debug!("db_items is {:?}", db_items); + let bank_level_item_iter = db_items.into_iter().take(top_k).map(|hashmap_av| { + let bank_level_item = BankLevelItem::try_from(hashmap_av)?; + Ok(bank_level_item) + }); + let bank_level_items = bank_level_item_iter + .collect::, LevelSpecificItemReaderError>>()?; + info!("bank_level_items is {:?}", bank_level_items); + let bank_response_iter = bank_level_items.into_iter().map(|item| { + let bank_level_rest = BankResponse::try_from(item) + .inspect_err(|err| error!("reader bank level rest error is {:?}", err))?; + Ok(bank_level_rest) + }); + let bank_responses = bank_response_iter + .collect::, LevelSpecificResponseReaderError>>()?; + info!("responses is {:?}", bank_responses); + let count_responses = bank_responses.len(); + let by_level_for_banks = ByLevelForBanks { + data: BankData { + responses: bank_responses, + count_responses, + }, + metadata, + }; + + Ok(by_level_for_banks) +} + +pub async fn query_deposits( + client: &Client, + table: &str, + query_item_params: ItemParamsRequest, + pk_portfolio_uuid: String, + sort_criteria: DepositSortCriteria, +) -> Result { + info!("in query_deposits"); + info!("query_item_params is {:?}", query_item_params); + let order = query_item_params.order.unwrap_or_default(); + info!("taking order for response: {:?}", order); + let top_k = query_item_params.top_k.unwrap_or(DEFAULT_TOP_K); + info!("taking top_k number k items for response: {:?}", top_k); + + let pk = "PK".to_string(); + let sk = "SK".to_string(); + let partition_key_value = format!("PORTFOLIO#UUID#{}", pk_portfolio_uuid); + let (sort_key_value, sort_criteria_description) = match sort_criteria { + DepositSortCriteria::DeltaPeriodGrowth => ("DEPOSIT#PERIOD#".to_string(), format!( + "Calculation results sorted by delta period growth at the deposits level for portfolio {}", + pk_portfolio_uuid + )), + DepositSortCriteria::MaturityDate => ("DEPOSIT#MATURITY_DATE#".to_string(), format!( + "Calculation results sorted by maturity date at the deposits level for portfolio {}", + pk_portfolio_uuid + )), + }; + let forward = bool::from(&order); + let query_params = QueryParams { + table: table.to_string(), + pk, + sk, + partition_key_value, + sort_key_value, + forward, + }; + let request = build_begins_with_request_for_query(client, query_params); + let query_resp = request.send().await?; + let metadata = Metadata { + order, + top_k, + sort_criteria_description, + }; + info!("query_resp.count is {:?}", query_resp.count); + if query_resp.count == 0 { + return Ok(ByLevelForDeposits { + data: DepositData { + responses: vec![], + count_responses: 0, + }, + metadata, + }); + } + + let db_items = query_resp.items.ok_or_else(|| { + QueryItemError::QueryNoItemFound("In query_bank_deposits_level_item".to_string()) + })?; + debug!("db_items is {:?}", db_items); + let deposit_level_item_iter = db_items.into_iter().take(top_k).map(|hashmap_av| { + let bank_deposit_level_item = DepositLevelItem::try_from(hashmap_av)?; + Ok(bank_deposit_level_item) + }); + let deposit_level_items = deposit_level_item_iter + .collect::, LevelSpecificItemReaderError>>()?; + // info!("bank_deposit_level_items is {:?}", bank_deposit_level_items); + + let deposit_response_iter = deposit_level_items.into_iter().map(|item| { + let bank_deposit_level_rest = DepositResponse::try_from(item) + .inspect_err(|err| error!("reader bank deposit level rest error is {:?}", err))?; + Ok(bank_deposit_level_rest) + }); + let deposit_responses = deposit_response_iter + .collect::, LevelSpecificResponseReaderError>>()?; + let count_responses = deposit_responses.len(); + let by_bank_deposits_level_rest = ByLevelForDeposits { + data: DepositData { + responses: deposit_responses, + count_responses, + }, + metadata, + }; + + Ok(by_bank_deposits_level_rest) +} + +struct QueryParams { + table: String, + pk: String, + sk: String, + partition_key_value: String, + sort_key_value: String, + forward: bool, +} + +fn build_begins_with_request_for_query(client: &Client, params: QueryParams) -> QueryFluentBuilder { + client + .query() + .table_name(params.table) + .key_condition_expression( + "#partitionKeyName = :partitionKeyValue AND begins_with(#sortKeyName, :sortKeyValue)" + .to_string(), + ) + .expression_attribute_names("#partitionKeyName".to_string(), params.pk) + .expression_attribute_values( + ":partitionKeyValue".to_string(), + AttributeValue::S(params.partition_key_value), + ) + .expression_attribute_names("#sortKeyName".to_string(), params.sk) + .expression_attribute_values( + ":sortKeyValue".to_string(), + AttributeValue::S(params.sort_key_value), + ) + .scan_index_forward(params.forward) +} diff --git a/drive-deposits-lambda-dynamodb-reader/src/handler_error.rs b/drive-deposits-lambda-dynamodb-reader/src/handler_error.rs new file mode 100644 index 0000000..16bb7df --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/src/handler_error.rs @@ -0,0 +1,56 @@ +use crate::dynamodb::query::QueryItemError; +use crate::request_error::Error as RequestError; +use axum::{ + http::StatusCode, + response::{IntoResponse, Json, Response}, +}; +use serde::Serialize; +use thiserror::Error; +use tracing::error; + +#[derive(Default, Debug, Error)] +pub enum Error { + #[default] + #[error("Internal error")] + InternalServer, + + #[error("RequestError got: {0}")] + RequestError(#[from] RequestError), + + #[error("QueryItemError querying DynamoDB: {0}")] + QueryItemError(#[from] QueryItemError), +} + +#[derive(Serialize)] +pub struct SerializableError { + error: String, +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + error!("IntoResponse for Error is {:?}", self); + + let (status, error_message) = match self { + Error::RequestError(err) => ( + StatusCode::BAD_REQUEST, + format!("Bad request error: {}", err), + ), + Error::QueryItemError(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Query item error: {}", err), + ), + Error::InternalServer => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal Server Error".to_string(), + ), + }; + + error!("Reader app error message is {:?}", error_message); + + let error = SerializableError { + error: error_message, + }; + + (status, Json(error)).into_response() + } +} diff --git a/drive-deposits-lambda-dynamodb-reader/src/lib.rs b/drive-deposits-lambda-dynamodb-reader/src/lib.rs new file mode 100644 index 0000000..331dd1f --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/src/lib.rs @@ -0,0 +1,4 @@ +pub mod dynamodb; + +pub mod handler_error; +pub mod request_error; diff --git a/drive-deposits-lambda-dynamodb-reader/src/request_error.rs b/drive-deposits-lambda-dynamodb-reader/src/request_error.rs new file mode 100644 index 0000000..36a1e24 --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/src/request_error.rs @@ -0,0 +1,7 @@ +use thiserror::Error as thisError; + +#[derive(Debug, thisError)] +pub enum Error { + #[error("Invalid Uuid: {0}")] + InvalidUuid(String), +} diff --git a/drive-deposits-lambda-dynamodb-reader/template.yaml b/drive-deposits-lambda-dynamodb-reader/template.yaml new file mode 100644 index 0000000..db69cbf --- /dev/null +++ b/drive-deposits-lambda-dynamodb-reader/template.yaml @@ -0,0 +1,68 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Description: stack pattern drive-deposits-dynamodb-queries; CloudFormation template for Lambda DynamoDB Reader for queries based on data persisted at the write level in drive-deposits-logs-lambda-target + +Parameters: + Environment: + Type: String + Description: "The environment name (e.g., dev, staging, production)" + + UseLocalstack: + Type: String + Default: "false" + Description: "Flag to determine if Localstack should be used" + + DriveDepositsTableName: + Type: String + Description: "The name of the DynamoDB table for Drive Deposits" + +Resources: + DriveDepositsHttpApi: + Type: AWS::Serverless::HttpApi + Properties: + DefaultRouteSettings: + ThrottlingBurstLimit: 100 # Maximum number of concurrent requests for all routes + ThrottlingRateLimit: 100 # Maximum number of requests per second for all routes + + DriveDepositsByLevelLambdaReaderFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: !Sub "drive_deposits_by_level_lambda_reader_${Environment}" + CodeUri: . + Handler: bootstrap + Runtime: provided.al2023 + Architectures: + - arm64 + Timeout: 30 + Environment: + Variables: + RUST_LOG: debug + AWS_LAMBDA_HTTP_IGNORE_STAGE_IN_PATH: 'true' + USE_LOCALSTACK: !Ref UseLocalstack + DRIVE_DEPOSITS_TABLE_NAME: + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html + Fn::ImportValue: + !Sub "${DriveDepositsTableName}" + Policies: + - DynamoDBReadPolicy: + TableName: + Fn::ImportValue: + !Sub "${DriveDepositsTableName}" + Events: + ApiEvent: + Type: HttpApi + Properties: + Path: /{proxy+} + Method: ANY + ApiId: !Ref DriveDepositsHttpApi + Metadata: + BuildMethod: rust-cargolambda + BuildProperties: + Binary: by_level_lambda_reader + BuildArgs: --release + Cache: true + +Outputs: + DriveDepositsByLevelLambdaReaderApiGatewayUrl: + Value: !Sub 'https://${DriveDepositsHttpApi}.execute-api.${AWS::Region}.amazonaws.com/' + Description: URL for the Axum Banks Level Lambda Reader API function endpoint diff --git a/drive-deposits-logs-lambda-target/.gitignore b/drive-deposits-logs-lambda-target/.gitignore new file mode 100644 index 0000000..0f6fddf --- /dev/null +++ b/drive-deposits-logs-lambda-target/.gitignore @@ -0,0 +1,3 @@ +/target +/.aws-sam +out.json diff --git a/drive-deposits-logs-lambda-target/Cargo.toml b/drive-deposits-logs-lambda-target/Cargo.toml new file mode 100644 index 0000000..80384ed --- /dev/null +++ b/drive-deposits-logs-lambda-target/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "drive-deposits-logs-lambda-target" +version = "0.10.0" +edition = "2021" + +[dependencies] +aws-config = { version = "1.5.4", features = ["behavior-version-latest"] } +aws-sdk-dynamodb = "1.39.1" +aws_lambda_events = "0.15.1" +lambda_runtime = "0.13.0" +tokio = { version = "1", features = ["macros"] } +serde = { version = "1.0.204", features = ["derive"] } +serde_json = "1.0.121" +thiserror = "1.0.63" +tracing = "0.1.40" +# workspace member depdenencies +drive-deposits-rest-types = { path = "../drive-deposits-rest-types" } +drive-deposits-lambda-db-types = { path = "../drive-deposits-lambda-db-types" } + + +[[bin]] +name = "by_level_lambda_writer" +path = "src/bin/by_level_lambda_writer.rs" \ No newline at end of file diff --git a/drive-deposits-logs-lambda-target/data/drive-deposits-event-banks-level.json b/drive-deposits-logs-lambda-target/data/drive-deposits-event-banks-level.json new file mode 100644 index 0000000..db4ae21 --- /dev/null +++ b/drive-deposits-logs-lambda-target/data/drive-deposits-event-banks-level.json @@ -0,0 +1,264 @@ +{ + "version": "0", + "id": "624d6b05-95e3-47c6-8734-b4c2fafe6b89", + "detail-type": "portfolio-level", + "source": "drive-deposits", + "account": "000000000000", + "time": "2024-08-09T20:07:18Z", + "region": "us-west-2", + "resources": [], + "detail": { + "uuid": "d187c423-9b81-48f4-8501-54096b73451b", + "banks": [ + { + "uuid": "e1263c7f-2c19-4458-a713-0071e98c715e", + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "deposits": [ + { + "uuid": "5ed5e8ec-b076-402a-aae1-41358e77b681", + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "5", + "years": "7", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "45.16" + }, + "maturity": { + "amount": "10990", + "interest": "3846.50", + "total": "14836.50" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2023-02-16", + "maturity_date_in_bank_tz": "2030-02-14", + "errors": [] + } + }, + { + "uuid": "252284ae-195d-42d4-b178-997f1c21b19f", + "account": "9898", + "account_type": "CertificateOfDeposit", + "apy": "2.22", + "years": "1", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "10.04" + }, + "maturity": { + "amount": "5500", + "interest": "122.10", + "total": "5622.10" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2020-02-16", + "maturity_date_in_bank_tz": "2021-02-15", + "errors": [] + } + }, + { + "uuid": "ee89fcf1-175f-4c17-a94e-ea73c577fe3b", + "account": "3833", + "account_type": "Savings", + "apy": "3.75", + "years": "20", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "44.72" + }, + "maturity": { + "amount": "10000.50", + "interest": "10882.06", + "total": "20882.56" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2024-02-16", + "maturity_date_in_bank_tz": "2044-02-11", + "errors": [] + } + } + ], + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "99.92" + }, + "maturity": { + "amount": "26490.50", + "interest": "14850.66", + "total": "41341.16" + }, + "errors": [] + } + }, + { + "uuid": "4b24ca37-eb0d-4fc6-a710-78e6b64f806a", + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "deposits": [ + { + "uuid": "013c2d15-07f6-4fb8-8a27-a4305e6d318d", + "account": "1234", + "account_type": "Checking", + "apy": "0.01", + "years": "1", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "0.00" + }, + "maturity": { + "amount": "100", + "interest": "0.01", + "total": "100.01" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2019-01-01", + "maturity_date_in_bank_tz": "2020-01-01", + "errors": [] + } + }, + { + "uuid": "e1c5f562-ba9c-46a4-a96a-d60226344e9a", + "account": "1256", + "account_type": "CertificateOfDeposit", + "apy": "5.40", + "years": "2", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "227.91" + }, + "maturity": { + "amount": "50000", + "interest": "5545.80", + "total": "55545.80" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2018-04-07", + "maturity_date_in_bank_tz": "2020-04-06", + "errors": [] + } + }, + { + "uuid": "5af191bf-05a9-4cca-9e5a-60564d426d75", + "account": "1111", + "account_type": "CertificateOfDeposit", + "apy": "1.01", + "years": "10", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "18.25" + }, + "maturity": { + "amount": "21000", + "interest": "2220.04", + "total": "23220.04" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2018-08-14", + "maturity_date_in_bank_tz": "2028-08-11", + "errors": [] + } + } + ], + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "246.16" + }, + "maturity": { + "amount": "71100", + "interest": "7765.85", + "total": "78865.85" + }, + "errors": [] + } + }, + { + "uuid": "33b2b081-2e55-4701-9af0-ce7a038fd896", + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "deposits": [ + { + "uuid": "b39a3042-5778-491e-9b97-db9c94e598ed", + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "2.4", + "years": "7", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "21.68" + }, + "maturity": { + "amount": "10990", + "interest": "1846.32", + "total": "12836.32" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2024-02-16", + "maturity_date_in_bank_tz": "2031-02-14", + "errors": [] + } + } + ], + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "21.68" + }, + "maturity": { + "amount": "10990", + "interest": "1846.32", + "total": "12836.32" + }, + "errors": [] + } + } + ], + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "367.76" + }, + "maturity": { + "amount": "108580.50", + "interest": "24462.83", + "total": "133043.33" + }, + "errors": [] + }, + "created_at": "2024-08-09 20:07:18.124446 UTC" + } +} \ No newline at end of file diff --git a/drive-deposits-logs-lambda-target/samconfig.toml b/drive-deposits-logs-lambda-target/samconfig.toml new file mode 100644 index 0000000..7b2cfd8 --- /dev/null +++ b/drive-deposits-logs-lambda-target/samconfig.toml @@ -0,0 +1,9 @@ +version = 0.1 +[dev.deploy.parameters] +stack_name = "drive-deposits-event-rules-dev" +resolve_s3 = true +s3_prefix = "drive-deposits-event-rules-dev" +region = "us-west-2" +confirm_changeset = true +capabilities = "CAPABILITY_IAM" +image_repositories = [] \ No newline at end of file diff --git a/drive-deposits-logs-lambda-target/src/bin/by_level_lambda_writer.rs b/drive-deposits-logs-lambda-target/src/bin/by_level_lambda_writer.rs new file mode 100644 index 0000000..6de3f2b --- /dev/null +++ b/drive-deposits-logs-lambda-target/src/bin/by_level_lambda_writer.rs @@ -0,0 +1,55 @@ +use aws_lambda_events::event::eventbridge::EventBridgeEvent; +use drive_deposits_logs_lambda_target::dynamodb::add::add_item; +use drive_deposits_logs_lambda_target::dynamodb::DriveDepositsDb; +use drive_deposits_rest_types::rest_types::CalculatePortfolioResponse; +use lambda_runtime::{ + run, service_fn, + tracing::{debug, error, info_span, init_default_subscriber}, + Error, LambdaEvent, +}; +use serde_json::from_value; + +async fn banks_level_handler( + db_handler: &DriveDepositsDb, + event: LambdaEvent, +) -> Result<(), Error> { + // Extract some useful information from the request + let span = info_span!("banks-level-handler-event-bridge-event"); + span.in_scope(|| debug!("inside banks_level_handler")); + // debug!("EventBridgeEvent event bridge event: {}", event); + let payload = event.payload; + debug!("payload.detail_type: {:?}", payload.detail_type); + debug!("payload.source: {:?}", payload.source); + debug!("db_handler table name is: {:?}", db_handler.table_name); + let payload_detail = payload.detail; + // debug!("payload.detail is {:#?}", payload_detail); + debug!("event_target_response is ------------ "); + let event_target_response: CalculatePortfolioResponse = from_value(payload_detail) + .inspect_err(|err| error!("Failed to deserialize payload_detail: {:?}", err))?; + debug!("event_target_response is {:#?}", event_target_response); + + add_item( + &db_handler.dynamodb_client, + db_handler.table_name.as_str(), + event_target_response, + ) + .await?; + + Ok(()) +} +#[tokio::main] +async fn main() -> Result<(), Error> { + println!("before init tracing subscriber"); + init_default_subscriber(); + + let span = info_span!("by_level_lambda_writer_main"); + span.in_scope(|| debug!("after init_default_subscriber")); + + let db_handler = DriveDepositsDb::handler().await?; + + // service_fn returns ServiceFn that implements FnMut so cannot pass ownership of db_handler + // closure is `FnOnce` if it moves the variable `db_handler` out of its environment + run(service_fn(|event| banks_level_handler(&db_handler, event))).await?; + + Ok(()) +} diff --git a/drive-deposits-logs-lambda-target/src/dynamodb.rs b/drive-deposits-logs-lambda-target/src/dynamodb.rs new file mode 100644 index 0000000..783a022 --- /dev/null +++ b/drive-deposits-logs-lambda-target/src/dynamodb.rs @@ -0,0 +1,68 @@ +pub mod add; + +use std::env; +use std::env::var; + +use aws_config::BehaviorVersion; +use aws_sdk_dynamodb::Client; +use lambda_runtime::tracing::{debug, error, info_span, instrument}; +use thiserror::Error; + +const LOCALSTACK_ENDPOINT: &str = "http://localhost.localstack.cloud:4566/"; + +#[derive(Default, Debug, Error)] +pub enum DbError { + #[default] + #[error("DynamoDb client error")] + DynamoDbClientError, + + #[error("Env var error")] + VarError(#[from] env::VarError), +} + +pub struct DriveDepositsDb { + pub dynamodb_client: Client, + pub table_name: String, +} + +impl DriveDepositsDb { + fn new(table_name: String, dynamodb_client: Client) -> Self { + Self { + dynamodb_client, + table_name, + } + } + + pub async fn handler() -> Result { + let span = info_span!("banks_level_db_handler"); + let table_name = var("DRIVE_DEPOSITS_TABLE_NAME").inspect_err(|err| { + span.in_scope(|| { + error!( + "var error for DRIVE_DEPOSITS_TABLE_NAME {:?}, so returning", + err + ) + }) + })?; + span.in_scope(|| debug!("dynamodb drive deposits table_name: {:?}", table_name)); + let mut config_loader = aws_config::defaults(BehaviorVersion::latest()); + if use_localstack() { + config_loader = config_loader.endpoint_url(LOCALSTACK_ENDPOINT); + }; + let config = config_loader.load().await; + let dynamodb_client = Client::new(&config); + span.in_scope(|| debug!("dynamodb client got!")); + Ok(Self::new(table_name, dynamodb_client)) + } +} + +fn env_var_bool(name: &str) -> bool { + let env_val = var(name).unwrap_or_else(|_| "false".to_string()); + env_val.eq_ignore_ascii_case("true") +} + +#[instrument] +fn use_localstack() -> bool { + let use_localstack = env_var_bool("USE_LOCALSTACK"); + debug!("use_localstack env variable is: {}", use_localstack); + use_localstack +} diff --git a/drive-deposits-logs-lambda-target/src/dynamodb/add.rs b/drive-deposits-logs-lambda-target/src/dynamodb/add.rs new file mode 100644 index 0000000..d16bb8b --- /dev/null +++ b/drive-deposits-logs-lambda-target/src/dynamodb/add.rs @@ -0,0 +1,118 @@ +use aws_sdk_dynamodb::{error::SdkError, operation::put_item::PutItemError, Client}; +use drive_deposits_lambda_db_types::{ + convert::writer::with_level_context::LevelSpecificItemWriterError, + db_item_types::{ + BankLevelItemsWrapper, CalculatePortfolioRestWrapper, DepositLevelItemsWrapper, + DepositSortCriteria, PortfolioLevelItem, + }, +}; +use drive_deposits_rest_types::rest_types::CalculatePortfolioResponse; +use std::collections::HashMap; +use thiserror::Error; +use tracing::debug; + +#[derive(Error, Debug)] +pub enum AddItemError { + #[error("AddItemError with LevelSpecificItemWriterError: {0}")] + LevelSpecificItemWriterError(#[from] LevelSpecificItemWriterError), + + #[error("AddItemError with DynamoDbSdkPutItemError error: {0}")] + DynamoDbSdkPutItemError(#[from] SdkError), +} + +pub async fn add_item( + client: &Client, + table: &str, + rest: CalculatePortfolioResponse, +) -> Result<(), AddItemError> { + let portfolio_level_item = PortfolioLevelItem::try_from(&rest)?; + add_portfolio_level_item(client, table, portfolio_level_item).await?; + + let banks_level_items_wrapper = BankLevelItemsWrapper::try_from(&rest)?; + add_bank_level_items(client, table, banks_level_items_wrapper).await?; + + let rest_wrapper_growth_criteria = CalculatePortfolioRestWrapper { + calculate_portfolio_response: rest.clone(), + deposit_sort_criteria: DepositSortCriteria::DeltaPeriodGrowth, + }; + let deposit_level_items_growth_criteria = + DepositLevelItemsWrapper::try_from(&rest_wrapper_growth_criteria)?; + add_deposit_level_items(client, table, deposit_level_items_growth_criteria).await?; + + let rest_wrapper_date_criteria = CalculatePortfolioRestWrapper { + calculate_portfolio_response: rest, + deposit_sort_criteria: DepositSortCriteria::MaturityDate, + }; + let deposit_level_items_date_criteria = + DepositLevelItemsWrapper::try_from(&rest_wrapper_date_criteria)?; + add_deposit_level_items(client, table, deposit_level_items_date_criteria).await?; + + Ok(()) +} + +pub async fn add_portfolio_level_item( + client: &Client, + table: &str, + item: PortfolioLevelItem, +) -> Result<(), AddItemError> { + debug!("portfolio level item: {:#?}", item); + let input_attributes = HashMap::from(item); + + let request = client + .put_item() + .table_name(table) + .set_item(Some(input_attributes)); + + let resp = request.send().await?; + debug!( + "dynamodb add_portfolio_level_item using HashMap from putItem with set_item response is response: {:#?}", + resp + ); + + Ok(()) +} + +pub async fn add_bank_level_items( + client: &Client, + table: &str, + wrapper: BankLevelItemsWrapper, +) -> Result<(), AddItemError> { + for item in wrapper.items { + let input_attributes = HashMap::from(item); + let request = client + .put_item() + .table_name(table) + .set_item(Some(input_attributes)); + + let resp = request.send().await?; + debug!( + "add_banks_level_item using HashMap from PutItemOutput is: {:#?}", + resp + ); + } + + Ok(()) +} + +pub async fn add_deposit_level_items( + client: &Client, + table: &str, + wrapper: DepositLevelItemsWrapper, +) -> Result<(), AddItemError> { + debug!("banks deposits level wrapper: {:#?}", wrapper); + for item in wrapper.deposit_level_items { + let input_attributes = HashMap::from(item); + + let request = client + .put_item() + .table_name(table) + .set_item(Some(input_attributes)); + + let resp = request.send().await?; + debug!( + "add_bank_deposits_level_item using HashMap from PutItemOutput is: {:#?}", + resp + ); + } + Ok(()) +} diff --git a/drive-deposits-logs-lambda-target/src/lib.rs b/drive-deposits-logs-lambda-target/src/lib.rs new file mode 100644 index 0000000..3745955 --- /dev/null +++ b/drive-deposits-logs-lambda-target/src/lib.rs @@ -0,0 +1 @@ +pub mod dynamodb; diff --git a/drive-deposits-logs-lambda-target/template.yaml b/drive-deposits-logs-lambda-target/template.yaml new file mode 100644 index 0000000..d07c7e6 --- /dev/null +++ b/drive-deposits-logs-lambda-target/template.yaml @@ -0,0 +1,154 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Description: stack pattern drive-deposits-event-rules-dev; CloudFormation template for EventBridge Rules and Cloudwatch Log Group with Lambda targets + +Parameters: + Environment: + Type: String + Description: "The environment name (e.g., dev, staging, production)" + + UseLocalstack: + Type: String + Default: "false" + Description: "Flag to determine if Localstack should be used" + +Resources: + DriveDepositsLogGroupBankLevel: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub "/aws/events/drive-deposits-log-group-bank-level-${Environment}" + RetentionInDays: 1 + + DriveDepositsBankLevelRule: + Type: AWS::Events::Rule + Properties: + Name: drive-deposits-bank-level + EventPattern: + source: + - "drive-deposits" + detail-type: + - "bank-level" + State: ENABLED + EventBusName: DriveDepositsEventBus + Targets: + - Id: "DriveDepositsLogGroupBankLevelTarget" + Arn: !GetAtt DriveDepositsLogGroupBankLevel.Arn + + DriveDepositsLogGroupPortfolioLevel: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub "/aws/events/drive-deposits-log-group-banks-level-${Environment}" + RetentionInDays: 1 + + DriveDepositsPortfolioLevelRule: + Type: AWS::Events::Rule + Properties: + Name: drive-deposits-banks-level + EventPattern: + source: + - "drive-deposits" + detail-type: + - "portfolio-level" + State: ENABLED + EventBusName: DriveDepositsEventBus + Targets: + - Id: "DriveDepositsLogGroupPortfolioLevelTarget" + Arn: !GetAtt DriveDepositsLogGroupPortfolioLevel.Arn + + + DriveDepositsTable: + Type: AWS::DynamoDB::Table + Properties: + # Reduce name collisions let AWS CloudFormation generate the name + # TableName: drive-deposits + AttributeDefinitions: + - AttributeName: PK + AttributeType: S + - AttributeName: SK + AttributeType: S + KeySchema: + - AttributeName: PK + KeyType: HASH + - AttributeName: SK + KeyType: RANGE + BillingMode: PAY_PER_REQUEST + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + + DriveDepositsByLevelLambdaWriterFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: !Sub "drive_deposits_by_level_lambda_writer_${Environment}" + CodeUri: . + Handler: bootstrap + Runtime: provided.al2023 + Architectures: + - arm64 + Timeout: 30 + Environment: + Variables: + RUST_LOG: debug + USE_LOCALSTACK: !Ref UseLocalstack + DRIVE_DEPOSITS_TABLE_NAME: !Ref DriveDepositsTable + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref DriveDepositsTable + Metadata: + BuildMethod: rust-cargolambda + BuildProperties: + Binary: by_level_lambda_writer + BuildArgs: --release + Cache: true + + # New duplicated rule functionality from drive-deposits-banks-level + # following best practice for eb rules https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rules-best-practices.html + DriveDepositsPortfolioLevelRuleForLambda: + Type: AWS::Events::Rule + Properties: + Name: drive-deposits-banks-level-for-lambda + EventPattern: + source: + - "drive-deposits" + detail-type: + - "portfolio-level" + State: ENABLED + EventBusName: DriveDepositsEventBus + Targets: + - Id: "DriveDepositsByLevelLambdaTarget" + Arn: !GetAtt DriveDepositsByLevelLambdaWriterFunction.Arn + + # { + # "Version": "2012-10-17", + # "Id": "default", + # "Statement": [ + # { + # "Sid": "drive-deposits-event-rules-dev-DriveDepositsByLevelLambdaPermission-3acnXMVxvasI", + # "Effect": "Allow", + # "Principal": { + # "Service": "events.amazonaws.com" + # }, + # "Action": "lambda:InvokeFunction", + # "Resource": "arn:aws:lambda:us-west-2:164190501998:function:by_level_lambda_writer_using_sam", + # "Condition": { + # "ArnLike": { + # "AWS:SourceArn": "arn:aws:events:us-west-2:164190501998:rule/DriveDepositsEventBus/drive-deposits-banks-level-for-lambda" + # } + # } + # } + # ] + #} + DriveDepositsByLevelLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !Ref DriveDepositsByLevelLambdaWriterFunction + Action: lambda:InvokeFunction + Principal: events.amazonaws.com + SourceArn: !GetAtt DriveDepositsPortfolioLevelRuleForLambda.Arn + + +Outputs: + DriveDepositsTableName: + Description: "DriveDepositTable name" + Value: !Ref DriveDepositsTable + Export: + Name: !Sub '${AWS::StackName}-DRIVE-DEPOSITS-TABLE-NAME' diff --git a/drive-deposits-proto-grpc-types/Cargo.toml b/drive-deposits-proto-grpc-types/Cargo.toml new file mode 100644 index 0000000..61b8675 --- /dev/null +++ b/drive-deposits-proto-grpc-types/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "drive-deposits-proto-grpc-types" +version = "0.10.0" +edition = "2021" + +[dependencies] +prost = "0.13.0" +tonic = "0.12.0" +tracing = "0.1.40" +heck = "0.5.0" + +# workspace member depdenencies +drive-deposits-rest-types = { path = "../drive-deposits-rest-types" } + +[build-dependencies] +tonic-build = "0.12.0" \ No newline at end of file diff --git a/drive-deposits-proto-grpc-types/build.rs b/drive-deposits-proto-grpc-types/build.rs new file mode 100644 index 0000000..44098d2 --- /dev/null +++ b/drive-deposits-proto-grpc-types/build.rs @@ -0,0 +1,13 @@ +use std::env; +use std::path::PathBuf; + +pub fn main() -> Result<(), Box> { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + println!("OUT_DIR: {:?}", out_dir); + tonic_build::configure() + .file_descriptor_set_path(out_dir.join("drivedeposits_descriptor.bin")) + .compile(&["v1/drivedeposits.proto"], &["proto"]) + .inspect_err(|err| println!("build.rs error : {:?}", err))?; + + Ok(()) +} diff --git a/drive-deposits-proto-grpc-types/src/convert.rs b/drive-deposits-proto-grpc-types/src/convert.rs new file mode 100644 index 0000000..4cf7526 --- /dev/null +++ b/drive-deposits-proto-grpc-types/src/convert.rs @@ -0,0 +1,2 @@ +pub mod from_grpc_rest_response; +pub mod from_rest_grpc_request; diff --git a/drive-deposits-proto-grpc-types/src/convert/from_grpc_rest_response.rs b/drive-deposits-proto-grpc-types/src/convert/from_grpc_rest_response.rs new file mode 100644 index 0000000..f2892ad --- /dev/null +++ b/drive-deposits-proto-grpc-types/src/convert/from_grpc_rest_response.rs @@ -0,0 +1,112 @@ +use heck::ToUpperCamelCase; +use tracing::{info, info_span}; + +use drive_deposits_rest_types::rest_types::{ + Bank as RestBank, CalculatePortfolioResponse as RestCalculatePortfolioResponse, + Delta as RestDelta, Deposit as RestDeposit, Maturity as RestMaturity, Outcome as RestOutcome, + OutcomeWithDates as RestOutcomeWithDates, ProcessingError as RestProcessingError, +}; + +use crate::generated::{ + AccountType as GrpcAccountType, Bank as GrpcBank, + CalculatePortfolioResponse as GrpcCalculatePortfolioResponse, Delta as GrpcDelta, + Deposit as GrpcDeposit, Maturity as GrpcMaturity, Outcome as GrpcOutcome, + OutcomeWithDates as GrpcOutcomeWithDates, PeriodUnit as GrpcPeriodUnit, + ProcessingError as GrpcProcessingError, +}; + +impl From for RestProcessingError { + fn from(grpc: GrpcProcessingError) -> Self { + Self { + uuid: grpc.uuid, + message: grpc.message, + } + } +} + +impl From for RestDelta { + fn from(grpc: GrpcDelta) -> Self { + Self { + period: grpc.period, + period_unit: GrpcPeriodUnit::try_from(grpc.period_unit) + .unwrap_or_default() + .as_str_name() + .to_upper_camel_case(), + growth: grpc.growth, + } + } +} + +impl From for RestMaturity { + fn from(grpc: GrpcMaturity) -> Self { + Self { + amount: grpc.amount, + interest: grpc.interest, + total: grpc.total, + } + } +} +impl From for RestOutcome { + fn from(grpc: GrpcOutcome) -> Self { + Self { + delta: grpc.delta.map(|x| x.into()), + maturity: grpc.maturity.map(|x| x.into()), + errors: grpc.errors.into_iter().map(|x| x.into()).collect(), + } + } +} + +impl From for RestOutcomeWithDates { + fn from(grpc: GrpcOutcomeWithDates) -> Self { + Self { + start_date_in_bank_tz: grpc.start_date_in_bank_tz, + maturity_date_in_bank_tz: grpc.maturity_date_in_bank_tz, + errors: grpc.errors.into_iter().map(|x| x.into()).collect(), + } + } +} +impl From for RestDeposit { + fn from(grpc: GrpcDeposit) -> Self { + Self { + uuid: grpc.uuid, + account: grpc.account, + account_type: GrpcAccountType::try_from(grpc.account_type) + .unwrap_or_default() + .as_str_name() + .to_upper_camel_case(), + apy: grpc.apy, + years: grpc.years, + outcome: grpc.outcome.map(|x| x.into()), + outcome_with_dates: grpc.outcome_with_dates.map(|x| x.into()), + } + } +} +impl From for RestBank { + fn from(grpc: GrpcBank) -> Self { + Self { + uuid: grpc.uuid, + name: grpc.name, + bank_tz: grpc.bank_tz.to_string(), + deposits: grpc.deposits.into_iter().map(|x| x.into()).collect(), + outcome: grpc.outcome.map(|x| x.into()), + } + } +} + +impl From for RestCalculatePortfolioResponse { + fn from(grpc: GrpcCalculatePortfolioResponse) -> Self { + let rest = Self { + uuid: grpc.uuid, + banks: grpc.banks.into_iter().map(|x| x.into()).collect(), + created_at: grpc.created_at, + outcome: grpc.outcome.map(|x| x.into()), + }; + info_span!("grpc_rest_response::From::grpc").in_scope(|| { + info!( + "In From: grpc response converted to rest response: {:?}", + rest + ) + }); + rest + } +} diff --git a/drive-deposits-proto-grpc-types/src/convert/from_rest_grpc_request.rs b/drive-deposits-proto-grpc-types/src/convert/from_rest_grpc_request.rs new file mode 100644 index 0000000..e184570 --- /dev/null +++ b/drive-deposits-proto-grpc-types/src/convert/from_rest_grpc_request.rs @@ -0,0 +1,64 @@ +use heck::ToShoutySnakeCase; +use tracing::{debug, info, info_span}; + +use drive_deposits_rest_types::rest_types::{ + CalculatePortfolioRequest as RestCalculatePortfolioRequest, NewBank as RestNewBank, + NewDelta as RestNewDelta, NewDeposit as RestNewDeposit, +}; + +use crate::generated::{ + AccountType as GrpcAccountType, CalculatePortfolioRequest as GrpcCalculatePortfolioRequest, + NewBank as GrpcNewBank, NewDelta as GrpcNewDelta, NewDeposit as GrpcNewDeposit, + PeriodUnit as GrpcPeriodUnit, +}; + +impl From for GrpcNewDeposit { + fn from(rest: RestNewDeposit) -> Self { + debug!( + "rest.account_type.to_shouty_snake_case() is {}", + rest.account_type.to_shouty_snake_case() + ); + GrpcNewDeposit { + account: rest.account, + account_type: GrpcAccountType::from_str_name(&rest.account_type.to_shouty_snake_case()) + .unwrap_or_default() as i32, + apy: rest.apy, + years: rest.years, + amount: rest.amount, + start_date_in_bank_tz: rest.start_date_in_bank_tz.to_string(), + } + } +} + +impl From for GrpcNewBank { + fn from(rest: RestNewBank) -> Self { + Self { + name: rest.name, + bank_tz: rest.bank_tz.to_string(), + new_deposits: rest.new_deposits.into_iter().map(|x| x.into()).collect(), + } + } +} + +impl From for GrpcNewDelta { + fn from(rest: RestNewDelta) -> Self { + Self { + period: rest.period, + period_unit: GrpcPeriodUnit::from_str_name(&rest.period_unit.to_shouty_snake_case()) + .unwrap_or_default() as i32, + } + } +} +impl From for GrpcCalculatePortfolioRequest { + fn from(rest: RestCalculatePortfolioRequest) -> Self { + // 44 | let grpc_new_delta = rest.new_delta.into(); + // | ^^^^ the trait `From` is not implemented for `generated::NewDelta`, which is required by `drive_deposits_io_types::io_types::NewDelta: Into<_>` + let grpc = Self { + new_banks: rest.new_banks.into_iter().map(|x| x.into()).collect(), + new_delta: Some(rest.new_delta.into()), + }; + info_span!("rest_grpc_request::From::rest") + .in_scope(|| info!("rest request converted to grpc request: {:?}", grpc)); + grpc + } +} diff --git a/drive-deposits-proto-grpc-types/src/lib.rs b/drive-deposits-proto-grpc-types/src/lib.rs new file mode 100644 index 0000000..dbff74c --- /dev/null +++ b/drive-deposits-proto-grpc-types/src/lib.rs @@ -0,0 +1,9 @@ +pub mod generated { + tonic::include_proto!("drivedepositsproto.v1"); + + // used for reflection by grpc service builder; not grpc client + pub const FILE_DESCRIPTOR_SET: &[u8] = + tonic::include_file_descriptor_set!("drivedeposits_descriptor"); +} + +pub mod convert; diff --git a/drive-deposits-proto-grpc-types/v1/drivedeposits.proto b/drive-deposits-proto-grpc-types/v1/drivedeposits.proto new file mode 100644 index 0000000..40eccd2 --- /dev/null +++ b/drive-deposits-proto-grpc-types/v1/drivedeposits.proto @@ -0,0 +1,114 @@ +syntax = "proto3"; + +// ~/drinnovations/mywork_jmd/rust_1_parvatimata_shivji_learnings_jmd/rust_courses_jmd/rust_tokio_ecosystem_video_text__/tonic_video_text/tonic-deposits-grpc-only-jmd git:[v0.3] +//buf lint +// after doing brew install bufbuild/buf/buf +// buf:lint:ignore PACKAGE_DIRECTORY_MATCH +package drivedepositsproto.v1; + +import "google/protobuf/wrappers.proto"; + + +// The drive deposits service definition. +service DriveDepositsService { + // calculated delta interest for each bank and for all banks as per delta period defined in BankRequest + rpc CalculatePortfolio(CalculatePortfolioRequest) returns (CalculatePortfolioResponse) {} +} + +// Request sections +message CalculatePortfolioRequest { + repeated NewBank new_banks = 1; + NewDelta new_delta = 2; +} + +message NewDelta { + string period = 1; + PeriodUnit period_unit = 2; +} + +enum PeriodUnit { + PERIOD_UNIT_UNSPECIFIED = 0; + DAY = 1; + WEEK = 2; + MONTH = 3; + YEAR = 4; +} + +message NewBank { + string name = 1; + string bank_tz = 2; + repeated NewDeposit new_deposits = 3; +} + +message NewDeposit { + string account = 1; + AccountType account_type = 2; + string apy = 3; + string years = 4; + string amount = 5; + string start_date_in_bank_tz = 6; +} + +enum AccountType { + ACCOUNT_TYPE_UNSPECIFIED = 0; + CHECKING = 1; + SAVINGS = 2; + CERTIFICATE_OF_DEPOSIT = 3; + BROKERAGE_CERTIFICATE_OF_DEPOSIT = 4; +} + +// Response sections + +message CalculatePortfolioResponse { + string uuid = 1; + repeated Bank banks = 2; + Outcome outcome = 3; + string created_at = 4; +} + +message Delta { + string period = 1; + PeriodUnit period_unit = 2; + string growth = 3; +} + +message Maturity { + string amount = 1; + string interest = 2; + string total = 3; +} + +message Bank { + string uuid = 1; + string name = 2; + string bank_tz = 3; + repeated Deposit deposits = 4; + Outcome outcome = 5; +} + +message Deposit { + string uuid = 1; + string account = 2; + AccountType account_type = 3; + string apy = 4; + string years = 5; + Outcome outcome = 6; + OutcomeWithDates outcome_with_dates = 7; +} + +message Outcome { + Delta delta = 1; + Maturity maturity = 2; + repeated ProcessingError errors = 3; +} + +message OutcomeWithDates { + string start_date_in_bank_tz = 1; + google.protobuf.StringValue maturity_date_in_bank_tz = 2; + repeated ProcessingError errors = 3; +} + +message ProcessingError { + string uuid = 1; + string message = 2; +} diff --git a/drive-deposits-rest-gateway-server/Cargo.toml b/drive-deposits-rest-gateway-server/Cargo.toml new file mode 100644 index 0000000..12505a8 --- /dev/null +++ b/drive-deposits-rest-gateway-server/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "drive-deposits-rest-gateway-server" +version = "0.10.0" +edition = "2021" + +[dependencies] +axum = { version = "0.7.5", features = ["macros"] } +tokio = { version = "1.38.0", features = ["full"] } +serde = { version = "1.0.203", features = ["derive"] } +serde_json = "1.0.117" +thiserror = "1.0.61" +validator = { version = "0.18.1", features = ["derive"] } +tonic = "0.12.0" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +# tower and tower-http +tower = { version = "0.4.13", features = ["full"] } +tower-http = { version = "0.5.2", features = ["full"] } + +# workspace member depdenencies +drive-deposits-rest-types = { path = "../drive-deposits-rest-types" } +# proto generated dependency here the drive-deposits-proto-grpc-types is still package +# name so with dashes +drive-deposits-proto-grpc-types = { path = "../drive-deposits-proto-grpc-types" } diff --git a/drive-deposits-rest-gateway-server/data/rest_request_invalid_decimal.json b/drive-deposits-rest-gateway-server/data/rest_request_invalid_decimal.json new file mode 100644 index 0000000..d598690 --- /dev/null +++ b/drive-deposits-rest-gateway-server/data/rest_request_invalid_decimal.json @@ -0,0 +1,82 @@ +{ + "new_delta": { + "period": "", + "period_unit": "Month" + }, + "new_banks": [ + { + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "new_deposits": [ + { + "account": "1234", + "account_type": "Checking", + "apy": "0", + "years": "1", + "amount": "100", + "start_date_in_bank_tz": "2019-01-01" + }, + { + "account": "1256", + "account_type": "CertificateOfDeposit", + "apy": "Hello", + "years": "2", + "amount": "50000", + "start_date_in_bank_tz": "2018-04-07" + }, + { + "account": "1111", + "account_type": "CertificateOfDeposit", + "apy": "1.01", + "years": "10", + "amount": "21000", + "start_date_in_bank_tz": "2018-08-14" + } + ] + }, + { + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "2.4", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2024-02-16" + } + ] + }, + { + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "5", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2023-02-16" + }, + { + "account": "9898", + "account_type": "CertificateOfDeposit", + "apy": "2.22", + "years": "1", + "amount": "5500", + "start_date_in_bank_tz": "2020-02-16" + }, + { + "account": "3833", + "account_type": "Savings", + "apy": "3.75", + "years": "20", + "amount": "10000.50", + "start_date_in_bank_tz": "2024-02-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/drive-deposits-rest-gateway-server/data/rest_request_invalid_json_structure.json b/drive-deposits-rest-gateway-server/data/rest_request_invalid_json_structure.json new file mode 100644 index 0000000..4af4292 --- /dev/null +++ b/drive-deposits-rest-gateway-server/data/rest_request_invalid_json_structure.json @@ -0,0 +1,82 @@ +{ + new_delta": { + "period": "1", + "period_unit": "Month" + }, + "new_banks: [ + { + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "new_deposits": [ + { + "account": "1234", + "account_type": "Checking", + "apy": "0.01", + "years": "1", + "amount": "100", + "start_date_in_bank_tz": "2019-01-01" + }, + { + "account": "1256", + "account_type": "CertificateOfDeposit", + "apy": "5.40", + "years": "2", + "amount": "50000", + "start_date_in_bank_tz": "2018-04-07" + }, + { + "account": "1111", + "account_type": "CertificateOfDeposit", + "apy": "1.01", + "years": "10", + "amount": "21000", + "start_date_in_bank_tz": "2018-08-14" + } + ] + }, + { + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "2.4", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2024-02-16" + } + ] + }, + { + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "5", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2023-02-16" + }, + { + "account": "9898", + "account_type": "CertificateOfDeposit", + "apy": "2.22", + "years": "1", + "amount": "5500", + "start_date_in_bank_tz": "2020-02-16" + }, + { + "account": "3833", + "account_type": "Savings", + "apy": "3.75", + "years": "20", + "amount": "10000.50", + "start_date_in_bank_tz": "2024-02-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/drive-deposits-rest-gateway-server/data/rest_request_invalid_period_unit_account_type_decimal_bank_tz_start_date.json b/drive-deposits-rest-gateway-server/data/rest_request_invalid_period_unit_account_type_decimal_bank_tz_start_date.json new file mode 100644 index 0000000..c31f53e --- /dev/null +++ b/drive-deposits-rest-gateway-server/data/rest_request_invalid_period_unit_account_type_decimal_bank_tz_start_date.json @@ -0,0 +1,82 @@ +{ + "new_delta": { + "period": "1", + "period_unit": "Century!" + }, + "new_banks": [ + { + "name": "MOUNTAIN", + "bank_tz": "America/Chicag", + "new_deposits": [ + { + "account": "1234", + "account_type": "Checking", + "apy": "0", + "years": "1", + "amount": "100", + "start_date_in_bank_tz": "201901-01" + }, + { + "account": "1256", + "account_type": "CertificateOfDeposit", + "apy": "5.40", + "years": "2", + "amount": "50000", + "start_date_in_bank_tz": "2018-04-07" + }, + { + "account": "1111", + "account_type": "CertificateOfDeposit", + "apy": "Incorrect!", + "years": "10", + "amount": "21000", + "start_date_in_bank_tz": "2018-08-14" + } + ] + }, + { + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "2.4", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2024-02-16" + } + ] + }, + { + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "5", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2023-02-16" + }, + { + "account": "9898", + "account_type": "CertificateOfDeposit", + "apy": "2.22", + "years": "1", + "amount": "5500", + "start_date_in_bank_tz": "2020-02-16" + }, + { + "account": "3833", + "account_type": "Svings", + "apy": "3.75", + "years": "20", + "amount": "10000.50", + "start_date_in_bank_tz": "2024-02-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/drive-deposits-rest-gateway-server/data/rest_request_valid.json b/drive-deposits-rest-gateway-server/data/rest_request_valid.json new file mode 100644 index 0000000..00c776c --- /dev/null +++ b/drive-deposits-rest-gateway-server/data/rest_request_valid.json @@ -0,0 +1,82 @@ +{ + "new_delta": { + "period": "1", + "period_unit": "Month" + }, + "new_banks": [ + { + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "new_deposits": [ + { + "account": "1234N", + "account_type": "Checking", + "apy": "0.01", + "years": "10", + "amount": "100", + "start_date_in_bank_tz": "2019-01-01" + }, + { + "account": "1256N", + "account_type": "CertificateOfDeposit", + "apy": "5.40", + "years": "2", + "amount": "50000", + "start_date_in_bank_tz": "2018-04-07" + }, + { + "account": "1111N", + "account_type": "CertificateOfDeposit", + "apy": "1.01", + "years": "10", + "amount": "21000", + "start_date_in_bank_tz": "2018-08-14" + } + ] + }, + { + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "new_deposits": [ + { + "account": "1235N", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "2.4", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2024-02-16" + } + ] + }, + { + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "new_deposits": [ + { + "account": "1235N", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "5", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2023-02-16" + }, + { + "account": "9898N", + "account_type": "CertificateOfDeposit", + "apy": "2.22", + "years": "1", + "amount": "5500", + "start_date_in_bank_tz": "2020-02-16" + }, + { + "account": "3833N", + "account_type": "Savings", + "apy": "3.75", + "years": "20", + "amount": "10000.50", + "start_date_in_bank_tz": "2024-02-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/drive-deposits-rest-gateway-server/data/rest_request_valid_6_months_delta_period.json b/drive-deposits-rest-gateway-server/data/rest_request_valid_6_months_delta_period.json new file mode 100644 index 0000000..373d9be --- /dev/null +++ b/drive-deposits-rest-gateway-server/data/rest_request_valid_6_months_delta_period.json @@ -0,0 +1,82 @@ +{ + "new_delta": { + "period": "6", + "period_unit": "Month" + }, + "new_banks": [ + { + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "new_deposits": [ + { + "account": "1234", + "account_type": "Checking", + "apy": "0.01", + "years": "1", + "amount": "100", + "start_date_in_bank_tz": "2019-01-01" + }, + { + "account": "1256", + "account_type": "CertificateOfDeposit", + "apy": "5.40", + "years": "2", + "amount": "50000", + "start_date_in_bank_tz": "2018-04-07" + }, + { + "account": "1111", + "account_type": "CertificateOfDeposit", + "apy": "1.01", + "years": "10", + "amount": "21000", + "start_date_in_bank_tz": "2018-08-14" + } + ] + }, + { + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "2.4", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2024-02-16" + } + ] + }, + { + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "new_deposits": [ + { + "account": "1234", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "5", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2023-02-16" + }, + { + "account": "9898", + "account_type": "CertificateOfDeposit", + "apy": "2.22", + "years": "1", + "amount": "5500", + "start_date_in_bank_tz": "2020-02-16" + }, + { + "account": "3833", + "account_type": "Savings", + "apy": "3.75", + "years": "20", + "amount": "10000.50", + "start_date_in_bank_tz": "2024-02-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/drive-deposits-rest-gateway-server/data/rest_request_valid_90_days_delta_period.json b/drive-deposits-rest-gateway-server/data/rest_request_valid_90_days_delta_period.json new file mode 100644 index 0000000..b02d992 --- /dev/null +++ b/drive-deposits-rest-gateway-server/data/rest_request_valid_90_days_delta_period.json @@ -0,0 +1,82 @@ +{ + "new_delta": { + "period": "90", + "period_unit": "Day" + }, + "new_banks": [ + { + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "new_deposits": [ + { + "account": "1234N", + "account_type": "Checking", + "apy": "0.01", + "years": "1", + "amount": "100", + "start_date_in_bank_tz": "2019-01-01" + }, + { + "account": "1256N", + "account_type": "CertificateOfDeposit", + "apy": "5.40", + "years": "2", + "amount": "50000", + "start_date_in_bank_tz": "2018-04-07" + }, + { + "account": "1111N", + "account_type": "CertificateOfDeposit", + "apy": "1.01", + "years": "10", + "amount": "21000", + "start_date_in_bank_tz": "2018-08-14" + } + ] + }, + { + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "new_deposits": [ + { + "account": "1235N", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "2.4", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2024-02-16" + } + ] + }, + { + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "new_deposits": [ + { + "account": "1235N", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "5", + "years": "7", + "amount": "10990", + "start_date_in_bank_tz": "2023-02-16" + }, + { + "account": "9898N", + "account_type": "CertificateOfDeposit", + "apy": "2.22", + "years": "1", + "amount": "5500", + "start_date_in_bank_tz": "2020-02-16" + }, + { + "account": "3833N", + "account_type": "Savings", + "apy": "3.75", + "years": "20", + "amount": "10000.50", + "start_date_in_bank_tz": "2024-02-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/drive-deposits-rest-gateway-server/data/rest_request_valid_greater_amount_investments.json b/drive-deposits-rest-gateway-server/data/rest_request_valid_greater_amount_investments.json new file mode 100644 index 0000000..a41eb28 --- /dev/null +++ b/drive-deposits-rest-gateway-server/data/rest_request_valid_greater_amount_investments.json @@ -0,0 +1,80 @@ +{ + "new_delta": { + "period": "1", + "period_unit": "Month" + }, + "new_banks": [ + { + "name": "MOUNTAIN-GREATER", + "bank_tz": "America/Chicago", + "new_deposits": [ + { + "account": "9111G", + "account_type": "CertificateOfDeposit", + "apy": "3.50", + "years": "10", + "amount": "21000", + "start_date_in_bank_tz": "2019-08-14" + } + ] + }, + { + "name": "CASHBACKJMD-GREATER", + "bank_tz": "America/Los_Angeles", + "new_deposits": [ + { + "account": "1111G", + "account_type": "CertificateOfDeposit", + "apy": "3.50", + "years": "5", + "amount": "410000", + "start_date_in_bank_tz": "2019-08-14" + }, + { + "account": "1112G", + "account_type": "CertificateOfDeposit", + "apy": "2.50", + "years": "5", + "amount": "230000", + "start_date_in_bank_tz": "2019-08-14" + } + ] + }, + { + "name": "PEACEMAKER-GREATER", + "bank_tz": "America/New_York", + "new_deposits": [ + { + "account": "1234G", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "4.25", + "years": "10", + "amount": "79990", + "start_date_in_bank_tz": "2022-02-16" + } + ] + }, + { + "name": "VISION-BANK-GREATER", + "bank_tz": "America/Los_Angeles", + "new_deposits": [ + { + "account": "1235G", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "3.34", + "years": "7", + "amount": "74990", + "start_date_in_bank_tz": "2023-02-16" + }, + { + "account": "3833G", + "account_type": "Savings", + "apy": "2.75", + "years": "20", + "amount": "49000.50", + "start_date_in_bank_tz": "2024-02-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/drive-deposits-rest-gateway-server/data/rest_request_valid_lesser_amount_investments.json b/drive-deposits-rest-gateway-server/data/rest_request_valid_lesser_amount_investments.json new file mode 100644 index 0000000..bbf8e92 --- /dev/null +++ b/drive-deposits-rest-gateway-server/data/rest_request_valid_lesser_amount_investments.json @@ -0,0 +1,58 @@ +{ + "new_delta": { + "period": "1", + "period_unit": "Month" + }, + "new_banks": [ + { + "name": "MOUNTAIN-LESSER", + "bank_tz": "America/Chicago", + "new_deposits": [ + { + "account": "1111L", + "account_type": "CertificateOfDeposit", + "apy": "1.01", + "years": "10", + "amount": "21000", + "start_date_in_bank_tz": "2018-08-14" + } + ] + }, + { + "name": "PEACEMAKER-LESSER", + "bank_tz": "America/New_York", + "new_deposits": [ + { + "account": "1234L", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "2.4", + "years": "2", + "amount": "5990", + "start_date_in_bank_tz": "2024-02-16" + } + ] + }, + { + "name": "VISION-BANK-LESSER", + "bank_tz": "America/Los_Angeles", + "new_deposits": [ + { + "account": "1235L", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "3.34", + "years": "7", + "amount": "4990", + "start_date_in_bank_tz": "2023-02-16" + }, + { + "account": "3833L", + "account_type": "Savings", + "apy": "2.75", + "years": "20", + "amount": "7000.50", + "start_date_in_bank_tz": "2024-02-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/drive-deposits-rest-gateway-server/data/rest_response_for_valid.json b/drive-deposits-rest-gateway-server/data/rest_response_for_valid.json new file mode 100644 index 0000000..618d923 --- /dev/null +++ b/drive-deposits-rest-gateway-server/data/rest_response_for_valid.json @@ -0,0 +1,254 @@ +{ + "uuid": "0c64fb0f-5ab9-4f2c-9317-4fa403297b7d", + "banks": [ + { + "uuid": "809c688e-d420-41ab-a3e6-2d8e4698a700", + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "deposits": [ + { + "uuid": "4768e2e2-da98-4037-992f-709128a9b1c2", + "account": "1235N", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "5", + "years": "7", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "45.16" + }, + "maturity": { + "amount": "10990", + "interest": "3846.50", + "total": "14836.50" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2023-02-16", + "maturity_date_in_bank_tz": "2030-02-14", + "errors": [] + } + }, + { + "uuid": "2bb893a4-8831-451f-ba22-14cf81d1e40b", + "account": "9898N", + "account_type": "CertificateOfDeposit", + "apy": "2.22", + "years": "1", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "10.04" + }, + "maturity": { + "amount": "5500", + "interest": "122.10", + "total": "5622.10" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2020-02-16", + "maturity_date_in_bank_tz": "2021-02-15", + "errors": [] + } + }, + { + "uuid": "b2f49c84-db51-43b7-bc37-11735f5c23b0", + "account": "3833N", + "account_type": "Savings", + "apy": "3.75", + "years": "20", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "44.72" + }, + "maturity": { + "amount": "10000.50", + "interest": "10882.06", + "total": "20882.56" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2024-02-16", + "maturity_date_in_bank_tz": "2044-02-11", + "errors": [] + } + } + ], + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "99.92" + }, + "maturity": { + "amount": "26490.50", + "interest": "14850.66", + "total": "41341.16" + }, + "errors": [] + } + }, + { + "uuid": "3c3a9c21-de54-45bc-8712-817c4b52759f", + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "deposits": [ + { + "uuid": "28da837b-3eaa-4745-be70-dc031a682ad6", + "account": "1234N", + "account_type": "Checking", + "apy": "0.01", + "years": "10", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "0.00" + }, + "maturity": { + "amount": "100", + "interest": "0.10", + "total": "100.10" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2019-01-01", + "maturity_date_in_bank_tz": "2028-12-29", + "errors": [] + } + }, + { + "uuid": "2e29fb9d-b84a-4821-92bd-4fcd4fd71a72", + "account": "1256N", + "account_type": "CertificateOfDeposit", + "apy": "5.40", + "years": "2", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "227.91" + }, + "maturity": { + "amount": "50000", + "interest": "5545.80", + "total": "55545.80" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2018-04-07", + "maturity_date_in_bank_tz": "2020-04-06", + "errors": [] + } + }, + { + "uuid": "54cfe116-8f2a-4aab-8e8b-0a71a6cad272", + "account": "1111N", + "account_type": "CertificateOfDeposit", + "apy": "1.01", + "years": "10", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "18.25" + }, + "maturity": { + "amount": "21000", + "interest": "2220.04", + "total": "23220.04" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2018-08-14", + "maturity_date_in_bank_tz": "2028-08-11", + "errors": [] + } + } + ], + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "246.16" + }, + "maturity": { + "amount": "71100", + "interest": "7765.94", + "total": "78865.94" + }, + "errors": [] + } + }, + { + "uuid": "a474e924-ff79-4fe9-a2e8-b2b1880e6fe6", + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "deposits": [ + { + "uuid": "f28efc5c-28b4-4334-819d-5967cfa00e18", + "account": "1234N", + "account_type": "BrokerageCertificateOfDeposit", + "apy": "2.4", + "years": "7", + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "21.68" + }, + "maturity": { + "amount": "10990", + "interest": "1846.32", + "total": "12836.32" + }, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2024-02-16", + "maturity_date_in_bank_tz": "2031-02-14", + "errors": [] + } + } + ], + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "21.68" + }, + "maturity": { + "amount": "10990", + "interest": "1846.32", + "total": "12836.32" + }, + "errors": [] + } + } + ], + "outcome": { + "delta": { + "period": "1", + "period_unit": "Month", + "growth": "367.76" + }, + "maturity": { + "amount": "108580.50", + "interest": "24462.92", + "total": "133043.42" + }, + "errors": [] + }, + "created_at": "2024-08-28T21:05:27.283882Z" +} \ No newline at end of file diff --git a/drive-deposits-rest-gateway-server/data/rest_response_with_processing_errors_no_panic_outcome_maturity_calculation_not_present.json b/drive-deposits-rest-gateway-server/data/rest_response_with_processing_errors_no_panic_outcome_maturity_calculation_not_present.json new file mode 100644 index 0000000..d5d9264 --- /dev/null +++ b/drive-deposits-rest-gateway-server/data/rest_response_with_processing_errors_no_panic_outcome_maturity_calculation_not_present.json @@ -0,0 +1,177 @@ +{ + "uuid": "3ea3c292-3d91-4b85-b9b2-c95d241204d5", + "banks": [ + { + "uuid": "cbf46f68-b82c-4b67-b870-ab378012b9f4", + "name": "PEACEMAKER", + "bank_tz": "America/New_York", + "deposits": [ + { + "uuid": "dce6639f-a96c-47dd-b496-6e1f404aadb9", + "account": "1234", + "account_type": "4", + "apy": "2.4", + "years": "7", + "outcome": { + "delta": null, + "maturity": null, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2024-02-16", + "maturity_date_in_bank_tz": "2031-02-14", + "errors": [] + } + } + ], + "outcome": { + "delta": null, + "maturity": null, + "errors": [ + { + "uuid": "e80f60a4-72e9-4593-8eea-46fadf4b9b3e", + "message": "Error accumulating deposits: MissingMaturity" + } + ] + } + }, + { + "uuid": "c0d19534-b8fb-419a-9d9f-ccb3ed2b42fc", + "name": "MOUNTAIN", + "bank_tz": "America/Chicago", + "deposits": [ + { + "uuid": "02fc04d9-ec53-4c9f-b5cc-d675d8a6c35a", + "account": "1234", + "account_type": "1", + "apy": "0", + "years": "1", + "outcome": { + "delta": null, + "maturity": null, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2019-01-01", + "maturity_date_in_bank_tz": "2020-01-01", + "errors": [] + } + }, + { + "uuid": "6d6b699b-334d-4df1-8af8-55803a0603eb", + "account": "1256", + "account_type": "3", + "apy": "5.40", + "years": "2", + "outcome": { + "delta": null, + "maturity": null, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2018-04-07", + "maturity_date_in_bank_tz": "2020-04-06", + "errors": [] + } + }, + { + "uuid": "c306e9d9-988a-47b7-a40a-5228d6fda00f", + "account": "1111", + "account_type": "3", + "apy": "1.01", + "years": "10", + "outcome": { + "delta": null, + "maturity": null, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2018-08-14", + "maturity_date_in_bank_tz": "2028-08-11", + "errors": [] + } + } + ], + "outcome": { + "delta": null, + "maturity": null, + "errors": [ + { + "uuid": "671e0b11-edec-4033-bc7d-fce304a807b4", + "message": "Error accumulating deposits: MissingMaturity" + } + ] + } + }, + { + "uuid": "d23edc7c-a4a6-427f-a8f4-b6366e43c08f", + "name": "VISION-BANK", + "bank_tz": "America/Los_Angeles", + "deposits": [ + { + "uuid": "1d54e80f-8773-4c8e-85d2-1750bc14dc3f", + "account": "1234", + "account_type": "4", + "apy": "5", + "years": "7", + "outcome": { + "delta": null, + "maturity": null, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2023-02-16", + "maturity_date_in_bank_tz": "2030-02-14", + "errors": [] + } + }, + { + "uuid": "0f665c10-352e-4b0e-a55c-2fccf2b07f5e", + "account": "9898", + "account_type": "3", + "apy": "2.22", + "years": "1", + "outcome": { + "delta": null, + "maturity": null, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2020-02-16", + "maturity_date_in_bank_tz": "2021-02-15", + "errors": [] + } + }, + { + "uuid": "aa06c0e1-2f55-4c0a-a861-c727a5477cc6", + "account": "3833", + "account_type": "2", + "apy": "3.75", + "years": "20", + "outcome": { + "delta": null, + "maturity": null, + "errors": [] + }, + "outcome_with_dates": { + "start_date_in_bank_tz": "2024-02-16", + "maturity_date_in_bank_tz": "2044-02-11", + "errors": [] + } + } + ], + "outcome": { + "delta": null, + "maturity": null, + "errors": [ + { + "uuid": "6dc2212b-5199-46be-a916-56f519730f97", + "message": "Error accumulating deposits: MissingMaturity" + } + ] + } + } + ], + "outcome": null, + "created_at": "2024-07-12 23:24:17.253955 UTC" +} \ No newline at end of file diff --git a/drive-deposits-rest-gateway-server/request.http b/drive-deposits-rest-gateway-server/request.http new file mode 100644 index 0000000..7c4ebd9 --- /dev/null +++ b/drive-deposits-rest-gateway-server/request.http @@ -0,0 +1,846 @@ +@host = http://localhost:3000 + +### +# with with root URL +POST {{host}}/ +Content-Type: application/json +Authorization: Bearer token +Accept-Encoding: br, gzip, deflate + +< ./data/rest_request_valid.json + +# Expected Output: +# For Calculate Drive Deposits API, use POST with Path /api/drive-deposits/calculate-portfolio + +### +# with correct API path +POST {{host}}/api/drive-deposits/calculate-portfolio +Content-Type: application/json +Authorization: Bearer token +Accept-Encoding: br, gzip, deflate + +< ./data/rest_request_valid.json + +# Expected Output: +# { +# "uuid": "76326bad-a359-4d3d-850d-c380970156d1", +# "banks": [ +# { +# "uuid": "309a9682-05b9-4afa-961d-9249a76020de", +# "name": "PEACEMAKER", +# "bank_tz": "America/New_York", +# "deposits": [ +# { +# "uuid": "5707c463-9227-4077-bb2f-421d2816bb76", +# "account": "1234", +# "account_type": "BrokerageCertificateOfDeposit", +# "apy": "2.4", +# "years": "7", +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "21.68" +# }, +# "maturity": { +# "amount": "10990", +# "interest": "1846.32", +# "total": "12836.32" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2024-02-16", +# "maturity_date_in_bank_tz": "2031-02-14", +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "21.68" +# }, +# "maturity": { +# "amount": "10990", +# "interest": "1846.32", +# "total": "12836.32" +# }, +# "errors": [] +# } +# }, +# { +# "uuid": "4df271b1-57a2-4c90-9485-266dd246f716", +# "name": "VISION-BANK", +# "bank_tz": "America/Los_Angeles", +# "deposits": [ +# { +# "uuid": "8b616e50-1f8a-4fdd-82b7-7809cb3dd6d3", +# "account": "1234", +# "account_type": "BrokerageCertificateOfDeposit", +# "apy": "5", +# "years": "7", +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "45.16" +# }, +# "maturity": { +# "amount": "10990", +# "interest": "3846.50", +# "total": "14836.50" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2023-02-16", +# "maturity_date_in_bank_tz": "2030-02-14", +# "errors": [] +# } +# }, +# { +# "uuid": "551d5a0b-ff34-4ff4-9301-61e5394327c1", +# "account": "9898", +# "account_type": "CertificateOfDeposit", +# "apy": "2.22", +# "years": "1", +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "10.04" +# }, +# "maturity": { +# "amount": "5500", +# "interest": "122.10", +# "total": "5622.10" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2020-02-16", +# "maturity_date_in_bank_tz": "2021-02-15", +# "errors": [] +# } +# }, +# { +# "uuid": "8041acb5-99b1-42ea-af70-b87d98824bda", +# "account": "3833", +# "account_type": "Savings", +# "apy": "3.75", +# "years": "20", +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "44.72" +# }, +# "maturity": { +# "amount": "10000.50", +# "interest": "10882.06", +# "total": "20882.56" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2024-02-16", +# "maturity_date_in_bank_tz": "2044-02-11", +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "99.92" +# }, +# "maturity": { +# "amount": "26490.50", +# "interest": "14850.66", +# "total": "41341.16" +# }, +# "errors": [] +# } +# }, +# { +# "uuid": "e2cbcf9a-263c-4221-a4c8-05793595bc8a", +# "name": "MOUNTAIN", +# "bank_tz": "America/Chicago", +# "deposits": [ +# { +# "uuid": "2c8f4d1f-3d3f-4113-a834-efaaff30df36", +# "account": "1234", +# "account_type": "Checking", +# "apy": "0.01", +# "years": "1", +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "0.00" +# }, +# "maturity": { +# "amount": "100", +# "interest": "0.01", +# "total": "100.01" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2019-01-01", +# "maturity_date_in_bank_tz": "2020-01-01", +# "errors": [] +# } +# }, +# { +# "uuid": "bf0e7eaa-1037-4126-aa53-9657bed2c787", +# "account": "1256", +# "account_type": "CertificateOfDeposit", +# "apy": "5.40", +# "years": "2", +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "227.91" +# }, +# "maturity": { +# "amount": "50000", +# "interest": "5545.80", +# "total": "55545.80" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2018-04-07", +# "maturity_date_in_bank_tz": "2020-04-06", +# "errors": [] +# } +# }, +# { +# "uuid": "1119868a-d39c-4125-a65e-944f91a06f82", +# "account": "1111", +# "account_type": "CertificateOfDeposit", +# "apy": "1.01", +# "years": "10", +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "18.25" +# }, +# "maturity": { +# "amount": "21000", +# "interest": "2220.04", +# "total": "23220.04" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2018-08-14", +# "maturity_date_in_bank_tz": "2028-08-11", +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "246.16" +# }, +# "maturity": { +# "amount": "71100", +# "interest": "7765.85", +# "total": "78865.85" +# }, +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "1", +# "period_unit": "Month", +# "growth": "367.76" +# }, +# "maturity": { +# "amount": "108580.50", +# "interest": "24462.83", +# "total": "133043.33" +# }, +# "errors": [] +# }, +# "created_at": "2024-07-18 03:37:40.958417 UTC" +#} + + + +### +POST {{host}}/api/drive-deposits/calculate-portfolio +Content-Type: application/json +Authorization: Bearer token +Accept-Encoding: br, gzip, deflate + +< ./data/rest_request_invalid_decimal.json + +# Expected Output: +# Input validation error: [new_delta.period: Validation error: length [{"value": String(""), "min": Number(1)}], Incorrect value: . Must be a valid decimal number., , new_banks[0].new_deposits[1].apy: Incorrect value: Hello. Must be a valid decimal number., ] + + + +### +POST {{host}}/api/drive-deposits/calculate-portfolio +Content-Type: application/json +Authorization: Bearer token +Accept-Encoding: br, gzip, deflate + +< ./data/rest_request_invalid_period_unit_account_type_decimal_bank_tz_start_date.json + +# Expected Output: +# Input validation error: [new_banks[0].bank_tz: Error: failed to parse timezone. Incorrect timezone: America/Chicag. Must be a valid timezone., new_banks[0].new_deposits[0].start_date_in_bank_tz: Error: input contains invalid characters. Incorrect date format: 201901-01. Must be in ISO 8601 format YYYY-MM-DD., new_banks[0].new_deposits[2].apy: Incorrect value: Incorrect!. Must be a valid decimal number., new_banks[2].new_deposits[2].account_type: Error: Matching variant not found. Incorrect account_type: Svings. Must be Checking, Savings, CertificateOfDeposit, or BrokerageCertificateOfDeposit., , new_delta.period_unit: Incorrect period_unit: Century!. Must be Day, Week, Month, or Year., ] + + +### +POST {{host}}/api/drive-deposits/calculate-portfolio +Content-Type: application/json +Authorization: Bearer token +Accept-Encoding: br, gzip, deflate + +< ./data/rest_request_invalid_json_structure.json + +# Expected Output: +# Axum Json Rejection error: JsonSyntaxError(JsonSyntaxError(Error { inner: Error { path: Path { segments: [Unknown] }, original: Error("key must be a string", line: 2, column: 3) } })) + +### +POST {{host}}/api/drive-deposits/calculate-portfolio +Content-Type: application/json +Authorization: Bearer token +Accept-Encoding: br, gzip, deflate + +< ./data/rest_request_valid_90_days_delta_period.json + + +# Expected output +# { +# "uuid": "db7b799c-5db9-4e8d-bd5b-98984decd594", +# "banks": [ +# { +# "uuid": "5b9076db-0241-4737-a189-89e1306d0124", +# "name": "PEACEMAKER", +# "bank_tz": "America/New_York", +# "deposits": [ +# { +# "uuid": "32ec0855-b788-417f-b5e3-710cf7b049c9", +# "account": "1234", +# "account_type": "BrokerageCertificateOfDeposit", +# "apy": "2.4", +# "years": "7", +# "outcome": { +# "delta": { +# "period": "90", +# "period_unit": "Day", +# "growth": "65.04" +# }, +# "maturity": { +# "amount": "10990", +# "interest": "1846.32", +# "total": "12836.32" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2024-02-16", +# "maturity_date_in_bank_tz": "2031-02-14", +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "90", +# "period_unit": "Day", +# "growth": "65.04" +# }, +# "maturity": { +# "amount": "10990", +# "interest": "1846.32", +# "total": "12836.32" +# }, +# "errors": [] +# } +# }, +# { +# "uuid": "4d98a7b2-3a24-4270-8737-8fcd75b207ce", +# "name": "MOUNTAIN", +# "bank_tz": "America/Chicago", +# "deposits": [ +# { +# "uuid": "b5ca91a3-1cb9-4c69-9fab-bf6d1f5b04e2", +# "account": "1234", +# "account_type": "Checking", +# "apy": "0.01", +# "years": "1", +# "outcome": { +# "delta": { +# "period": "90", +# "period_unit": "Day", +# "growth": "0.00" +# }, +# "maturity": { +# "amount": "100", +# "interest": "0.01", +# "total": "100.01" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2019-01-01", +# "maturity_date_in_bank_tz": "2020-01-01", +# "errors": [] +# } +# }, +# { +# "uuid": "4492ef0d-c99d-4e09-9564-07683d760633", +# "account": "1256", +# "account_type": "CertificateOfDeposit", +# "apy": "5.40", +# "years": "2", +# "outcome": { +# "delta": { +# "period": "90", +# "period_unit": "Day", +# "growth": "683.73" +# }, +# "maturity": { +# "amount": "50000", +# "interest": "5545.80", +# "total": "55545.80" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2018-04-07", +# "maturity_date_in_bank_tz": "2020-04-06", +# "errors": [] +# } +# }, +# { +# "uuid": "9e57e522-115b-4d8c-a182-04fba6e54124", +# "account": "1111", +# "account_type": "CertificateOfDeposit", +# "apy": "1.01", +# "years": "10", +# "outcome": { +# "delta": { +# "period": "90", +# "period_unit": "Day", +# "growth": "54.74" +# }, +# "maturity": { +# "amount": "21000", +# "interest": "2220.04", +# "total": "23220.04" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2018-08-14", +# "maturity_date_in_bank_tz": "2028-08-11", +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "90", +# "period_unit": "Day", +# "growth": "738.47" +# }, +# "maturity": { +# "amount": "71100", +# "interest": "7765.85", +# "total": "78865.85" +# }, +# "errors": [] +# } +# }, +# { +# "uuid": "49cf775a-5c32-4995-abf4-65c230c29b15", +# "name": "VISION-BANK", +# "bank_tz": "America/Los_Angeles", +# "deposits": [ +# { +# "uuid": "8df47207-1992-4438-b4ca-8f7339e4cdd9", +# "account": "1234", +# "account_type": "BrokerageCertificateOfDeposit", +# "apy": "5", +# "years": "7", +# "outcome": { +# "delta": { +# "period": "90", +# "period_unit": "Day", +# "growth": "135.49" +# }, +# "maturity": { +# "amount": "10990", +# "interest": "3846.50", +# "total": "14836.50" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2023-02-16", +# "maturity_date_in_bank_tz": "2030-02-14", +# "errors": [] +# } +# }, +# { +# "uuid": "ace50ced-7a56-4eac-9eee-dec01cf25d23", +# "account": "9898", +# "account_type": "CertificateOfDeposit", +# "apy": "2.22", +# "years": "1", +# "outcome": { +# "delta": { +# "period": "90", +# "period_unit": "Day", +# "growth": "30.11" +# }, +# "maturity": { +# "amount": "5500", +# "interest": "122.10", +# "total": "5622.10" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2020-02-16", +# "maturity_date_in_bank_tz": "2021-02-15", +# "errors": [] +# } +# }, +# { +# "uuid": "29b1a525-2636-4548-a00c-60f7d34b102c", +# "account": "3833", +# "account_type": "Savings", +# "apy": "3.75", +# "years": "20", +# "outcome": { +# "delta": { +# "period": "90", +# "period_unit": "Day", +# "growth": "134.16" +# }, +# "maturity": { +# "amount": "10000.50", +# "interest": "10882.06", +# "total": "20882.56" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2024-02-16", +# "maturity_date_in_bank_tz": "2044-02-11", +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "90", +# "period_unit": "Day", +# "growth": "299.76" +# }, +# "maturity": { +# "amount": "26490.50", +# "interest": "14850.66", +# "total": "41341.16" +# }, +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "90", +# "period_unit": "Day", +# "growth": "1103.27" +# }, +# "maturity": { +# "amount": "108580.50", +# "interest": "24462.83", +# "total": "133043.33" +# }, +# "errors": [] +# }, +# "created_at": "2024-07-18 03:58:16.546197 UTC" +#} + + +### +POST {{host}}/api/drive-deposits/calculate-portfolio +Content-Type: application/json +Authorization: Bearer token +Accept-Encoding: br, gzip, deflate + +< ./data/rest_request_valid_6_months_delta_period.json + + +# { +# "uuid": "45fc9d46-ee54-4f7f-918d-9f4f599995f4", +# "banks": [ +# { +# "uuid": "7b0740cc-7acc-4000-b38b-e6889dde1fcd", +# "name": "VISION-BANK", +# "bank_tz": "America/Los_Angeles", +# "deposits": [ +# { +# "uuid": "92ba5b93-f864-4b4e-8374-79873c5e0502", +# "account": "1234", +# "account_type": "BrokerageCertificateOfDeposit", +# "apy": "5", +# "years": "7", +# "outcome": { +# "delta": { +# "period": "6", +# "period_unit": "Month", +# "growth": "270.99" +# }, +# "maturity": { +# "amount": "10990", +# "interest": "3846.50", +# "total": "14836.50" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2023-02-16", +# "maturity_date_in_bank_tz": "2030-02-14", +# "errors": [] +# } +# }, +# { +# "uuid": "5d1b10d2-f2c5-4356-934f-0e8308c34897", +# "account": "9898", +# "account_type": "CertificateOfDeposit", +# "apy": "2.22", +# "years": "1", +# "outcome": { +# "delta": { +# "period": "6", +# "period_unit": "Month", +# "growth": "60.21" +# }, +# "maturity": { +# "amount": "5500", +# "interest": "122.10", +# "total": "5622.10" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2020-02-16", +# "maturity_date_in_bank_tz": "2021-02-15", +# "errors": [] +# } +# }, +# { +# "uuid": "c46ed3cc-13ba-4b32-b1ae-3e1ac15b8f0d", +# "account": "3833", +# "account_type": "Savings", +# "apy": "3.75", +# "years": "20", +# "outcome": { +# "delta": { +# "period": "6", +# "period_unit": "Month", +# "growth": "268.32" +# }, +# "maturity": { +# "amount": "10000.50", +# "interest": "10882.06", +# "total": "20882.56" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2024-02-16", +# "maturity_date_in_bank_tz": "2044-02-11", +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "6", +# "period_unit": "Month", +# "growth": "599.52" +# }, +# "maturity": { +# "amount": "26490.50", +# "interest": "14850.66", +# "total": "41341.16" +# }, +# "errors": [] +# } +# }, +# { +# "uuid": "d9628443-6e8d-462e-8ce8-4401d02a41f8", +# "name": "PEACEMAKER", +# "bank_tz": "America/New_York", +# "deposits": [ +# { +# "uuid": "c3e19c10-fd3a-4db1-b221-da8bcb03a683", +# "account": "1234", +# "account_type": "BrokerageCertificateOfDeposit", +# "apy": "2.4", +# "years": "7", +# "outcome": { +# "delta": { +# "period": "6", +# "period_unit": "Month", +# "growth": "130.07" +# }, +# "maturity": { +# "amount": "10990", +# "interest": "1846.32", +# "total": "12836.32" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2024-02-16", +# "maturity_date_in_bank_tz": "2031-02-14", +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "6", +# "period_unit": "Month", +# "growth": "130.07" +# }, +# "maturity": { +# "amount": "10990", +# "interest": "1846.32", +# "total": "12836.32" +# }, +# "errors": [] +# } +# }, +# { +# "uuid": "a95f65ca-ef41-436b-8705-4071e2e183a1", +# "name": "MOUNTAIN", +# "bank_tz": "America/Chicago", +# "deposits": [ +# { +# "uuid": "44c83662-56ce-4516-9661-e1ff46c4d4d1", +# "account": "1234", +# "account_type": "Checking", +# "apy": "0.01", +# "years": "1", +# "outcome": { +# "delta": { +# "period": "6", +# "period_unit": "Month", +# "growth": "0.00" +# }, +# "maturity": { +# "amount": "100", +# "interest": "0.01", +# "total": "100.01" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2019-01-01", +# "maturity_date_in_bank_tz": "2020-01-01", +# "errors": [] +# } +# }, +# { +# "uuid": "08638d63-97e0-404d-91a5-bc1cd40b53a0", +# "account": "1256", +# "account_type": "CertificateOfDeposit", +# "apy": "5.40", +# "years": "2", +# "outcome": { +# "delta": { +# "period": "6", +# "period_unit": "Month", +# "growth": "1367.46" +# }, +# "maturity": { +# "amount": "50000", +# "interest": "5545.80", +# "total": "55545.80" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2018-04-07", +# "maturity_date_in_bank_tz": "2020-04-06", +# "errors": [] +# } +# }, +# { +# "uuid": "bb3cdd45-2917-4d8b-ba75-2ffded5e4f52", +# "account": "1111", +# "account_type": "CertificateOfDeposit", +# "apy": "1.01", +# "years": "10", +# "outcome": { +# "delta": { +# "period": "6", +# "period_unit": "Month", +# "growth": "109.48" +# }, +# "maturity": { +# "amount": "21000", +# "interest": "2220.04", +# "total": "23220.04" +# }, +# "errors": [] +# }, +# "outcome_with_dates": { +# "start_date_in_bank_tz": "2018-08-14", +# "maturity_date_in_bank_tz": "2028-08-11", +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "6", +# "period_unit": "Month", +# "growth": "1476.94" +# }, +# "maturity": { +# "amount": "71100", +# "interest": "7765.85", +# "total": "78865.85" +# }, +# "errors": [] +# } +# } +# ], +# "outcome": { +# "delta": { +# "period": "6", +# "period_unit": "Month", +# "growth": "2206.53" +# }, +# "maturity": { +# "amount": "108580.50", +# "interest": "24462.83", +# "total": "133043.33" +# }, +# "errors": [] +# }, +# "created_at": "2024-07-31 18:00:49.388889 UTC" +#} \ No newline at end of file diff --git a/drive-deposits-rest-gateway-server/src/drive_deposits_client.rs b/drive-deposits-rest-gateway-server/src/drive_deposits_client.rs new file mode 100644 index 0000000..126f7b1 --- /dev/null +++ b/drive-deposits-rest-gateway-server/src/drive_deposits_client.rs @@ -0,0 +1,48 @@ +use axum::Json; +use tracing::{debug, debug_span, error, info}; + +use app_error::Error as AppError; +use drive_deposits_proto_grpc_types::generated::drive_deposits_service_client::DriveDepositsServiceClient; +use drive_deposits_rest_types::rest_types::CalculatePortfolioResponse; +use request_error::ValidateCalculateRequest; + +mod app_error; + +mod request_error; + +// #[debug_handler] +pub async fn calculate_portfolio( + ValidateCalculateRequest(rest_delta_request): ValidateCalculateRequest, +) -> Result, AppError> { + let span = debug_span!("rest_calculate_portfolio"); + span.in_scope(|| { + debug!( + "calculate_portfolio request incoming is : {:#?}", + rest_delta_request + ) + }); + + let mut client = DriveDepositsServiceClient::connect("http://[::1]:50052") + .await + .inspect_err(|err| { + span.in_scope(|| error!("grpc client connection error: {:?}", err)); + })?; + let grpc_delta_request = span.in_scope(|| rest_delta_request.into()); + + let grpc_request = tonic::Request::new(grpc_delta_request); + let grpc_response = client.calculate_portfolio(grpc_request).await?; + + span.in_scope(|| { + info!( + "grpc response in client for rest server acting as gateway is : {:#?}", + grpc_response + ); + let grpc_delta_response = grpc_response.into_inner(); + let rest_response = grpc_delta_response.into(); + info!( + "rest response in client for rest server acting as gateway is : {:#?}", + rest_response + ); + Ok(Json(rest_response)) + }) +} diff --git a/drive-deposits-rest-gateway-server/src/drive_deposits_client/app_error.rs b/drive-deposits-rest-gateway-server/src/drive_deposits_client/app_error.rs new file mode 100644 index 0000000..180f88e --- /dev/null +++ b/drive-deposits-rest-gateway-server/src/drive_deposits_client/app_error.rs @@ -0,0 +1,52 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Json, Response}, +}; +use serde::Serialize; +use thiserror::Error; +use tracing::error; + +#[derive(Default, Debug, Error)] +pub enum Error { + #[default] + #[error("Internal error")] + InternalServer, + #[error("Tonic transport error: {0}")] + Transport(#[from] tonic::transport::Error), + #[error("Tonic status error: {0}")] + Status(#[from] tonic::Status), +} + +#[derive(Serialize)] +pub struct SerializableError { + error: String, +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + error!("IntoResponse for Error is {:?}", self); + + let (status, error_message) = match self { + Error::InternalServer => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal Server Error".to_string(), + ), + Error::Transport(err) => ( + StatusCode::SERVICE_UNAVAILABLE, + format!("Connect Server Error {:?}", err), + ), + Error::Status(err) => ( + StatusCode::BAD_GATEWAY, + format!("gRPC Server Error {:?}", err), + ), + }; + + error!("{}", error_message); + + let error = SerializableError { + error: error_message, + }; + + (status, Json(error)).into_response() + } +} diff --git a/drive-deposits-rest-gateway-server/src/drive_deposits_client/request_error.rs b/drive-deposits-rest-gateway-server/src/drive_deposits_client/request_error.rs new file mode 100644 index 0000000..5426043 --- /dev/null +++ b/drive-deposits-rest-gateway-server/src/drive_deposits_client/request_error.rs @@ -0,0 +1,55 @@ +use axum::extract::rejection::JsonRejection; +use axum::extract::{FromRequest, Request}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::{async_trait, Json}; +use thiserror::Error as thisError; +use tracing::info; +use validator::Validate; + +use drive_deposits_rest_types::rest_types::CalculatePortfolioRequest; + +#[derive(Debug, thisError)] +pub enum Error { + #[error(transparent)] + Validation(#[from] validator::ValidationErrors), + + #[error(transparent)] + RequestJsonRejection(#[from] JsonRejection), +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + info!("into response self is {:?}", self); + match self { + Error::Validation(_) => { + let message = format!("Input validation error: [{self}]").replace('\n', ", "); + (StatusCode::BAD_REQUEST, message) + } + + Error::RequestJsonRejection(err) => { + let message = format!("Axum Json Rejection error: {:?}", err); + (StatusCode::BAD_REQUEST, message) + } + } + .into_response() + } +} + +#[derive(Debug)] +pub struct ValidateCalculateRequest(pub CalculatePortfolioRequest); + +#[async_trait] +impl FromRequest for ValidateCalculateRequest +where + S: Send + Sync, +{ + type Rejection = Error; + + async fn from_request(req: Request, state: &S) -> Result { + let Json(value) = Json::::from_request(req, state).await?; + + value.validate()?; + Ok(ValidateCalculateRequest(value)) + } +} diff --git a/drive-deposits-rest-gateway-server/src/lib.rs b/drive-deposits-rest-gateway-server/src/lib.rs new file mode 100644 index 0000000..e33f354 --- /dev/null +++ b/drive-deposits-rest-gateway-server/src/lib.rs @@ -0,0 +1,2 @@ +pub mod drive_deposits_client; +pub mod service_router; diff --git a/drive-deposits-rest-gateway-server/src/main.rs b/drive-deposits-rest-gateway-server/src/main.rs new file mode 100644 index 0000000..b0f01e2 --- /dev/null +++ b/drive-deposits-rest-gateway-server/src/main.rs @@ -0,0 +1,43 @@ +use std::error::Error; + +use tracing::{debug, debug_span, info, Instrument}; +use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; + +use drive_deposits_rest_gateway_server::service_router::router; + +#[tokio::main] +async fn main() -> Result<(), Box> { + registry() + .with( + EnvFilter::try_from_default_env()? + // added in .cargo/config.toml .add_directive("drive_deposits_rest_grpc_gateway=debug".parse()?) + .add_directive("axum::rejection=trace".parse()?) + .add_directive("tower_http=debug".parse()?), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let rust_log = std::env::var("RUST_LOG")?; + println!("RUST_LOG is {}", rust_log); + + let span = debug_span!("main"); + tracing::dispatcher::get_default(|dispatch| -> Option<()> { + let metadata = span.metadata()?; + if dispatch.enabled(metadata) { + println!("Span event is emitted"); + } else { + println!("Span event is not emitted"); + } + None + }); + span.in_scope(|| debug!("router is being set up first up")); + let app_router = router().instrument(span.clone()).await; + + let span = tracing::span!(tracing::Level::INFO, "server"); + // run it + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?; + let listening_on = listener.local_addr()?; + span.in_scope(|| info!("listening on {}", listening_on)); + axum::serve(listener, app_router).await?; + Ok(()) +} diff --git a/drive-deposits-rest-gateway-server/src/service_router.rs b/drive-deposits-rest-gateway-server/src/service_router.rs new file mode 100644 index 0000000..840b254 --- /dev/null +++ b/drive-deposits-rest-gateway-server/src/service_router.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; +use std::time::Duration; + +use axum::{ + body::Bytes, + http::header, + routing::{get, post}, + Router, +}; +use tower::ServiceBuilder; +use tower_http::{ + timeout::TimeoutLayer, + trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}, + LatencyUnit, ServiceBuilderExt, +}; +use tracing::{info, instrument}; + +use crate::drive_deposits_client::calculate_portfolio; + +const CALCULATE_PORTFOLIO: &str = "/api/drive-deposits/calculate-portfolio"; + +pub async fn root() -> String { + format!( + "For Calculate Drive Deposits API, use POST with Path {}", + CALCULATE_PORTFOLIO + ) +} + +#[instrument] +pub async fn router() -> Router { + let sensitive_headers: Arc<[_]> = vec![header::AUTHORIZATION].into(); + + let middleware = ServiceBuilder::new() + .sensitive_request_headers(sensitive_headers.clone()) + .layer( + TraceLayer::new_for_http() + .on_body_chunk(|chunk: &Bytes, latency: Duration, _: &tracing::Span| { + tracing::trace!(size_bytes = chunk.len(), latency = ?latency, "sending body chunk") + }) + .make_span_with(DefaultMakeSpan::new().include_headers(true)) + .on_response(DefaultOnResponse::new().include_headers(true).latency_unit(LatencyUnit::Micros)), + ) + .sensitive_response_headers(sensitive_headers) + .layer(TimeoutLayer::new(Duration::from_secs(10))) + .compression(); + info!("Creating router"); + Router::new() + .route("/", get(root).post(root)) + .route(CALCULATE_PORTFOLIO, post(calculate_portfolio)) + .layer(middleware) +} diff --git a/drive-deposits-rest-types/Cargo.toml b/drive-deposits-rest-types/Cargo.toml new file mode 100644 index 0000000..ce72ac4 --- /dev/null +++ b/drive-deposits-rest-types/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "drive-deposits-rest-types" +version = "0.10.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0.203", features = ["derive"] } +serde_json = "1.0.117" +validator = { version = "0.18.1", features = ["derive"] } +rust_decimal = { version = "1.35.0" } +strum = "0.26" +strum_macros = "0.26" +chrono = "0.4.38" +chrono-tz = "0.9.0" \ No newline at end of file diff --git a/drive-deposits-rest-types/src/lib.rs b/drive-deposits-rest-types/src/lib.rs new file mode 100644 index 0000000..34f9279 --- /dev/null +++ b/drive-deposits-rest-types/src/lib.rs @@ -0,0 +1 @@ +pub mod rest_types; diff --git a/drive-deposits-rest-types/src/rest_types.rs b/drive-deposits-rest-types/src/rest_types.rs new file mode 100644 index 0000000..b986281 --- /dev/null +++ b/drive-deposits-rest-types/src/rest_types.rs @@ -0,0 +1,262 @@ +use std::str::FromStr; + +use chrono::NaiveDate; +use chrono_tz::Tz; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +use strum_macros::EnumString; +use validator::Validate; +use validator::ValidationError; + +// Request sections +#[derive(Default, Debug, Deserialize, Validate)] +pub struct CalculatePortfolioRequest { + #[validate(length(min = 1), nested)] + pub new_banks: Vec, + #[validate(nested)] + pub new_delta: NewDelta, +} + +#[derive(Default, Debug, Deserialize, Validate)] +pub struct NewDelta { + #[validate(length(min = 1), custom(function = "validate_positive_decimal"))] + pub period: String, + #[validate(custom(function = "validate_period_unit"))] + pub period_unit: String, +} + +// declarative programming -- functional programming inspired +// #[derive(Default)]: Automatically implements the Default trait for the enum. The Default trait allows you to create a default value for the enum. For enums, the first variant is considered the default unless specified otherwise with the #[default] attribute on one of the variants. +// #[derive(Debug)]: Implements the Debug trait to enable formatting using the {:?} formatter. +// #[derive(Deserialize)]: Derives the Deserialize trait from the serde crate, enabling your enum to be deserialized from formats like JSON. +// #[derive(EnumString)]:Converts strings to enum variants based on their name.auto-derives std::str::FromStr on the enum and std::convert::TryFrom<&str> will be derived as well per docs +// #[derive(Serialize) : since used in response as well +#[derive(Default, Debug, Deserialize, EnumString, Serialize)] +pub enum DeltaPeriodUnit { + #[default] + Unspecified = 0, + Day = 1, + Week = 2, + Month = 3, + Year = 4, +} + +fn validate_period_unit(period_unit: &str) -> Result<(), ValidationError> { + // DeltaPeriodUnit::from_str(period_unit).map_err(|_| { + // let mut error = ValidationError::new("invalid_period_unit"); + // error.message = Some( + // format!( + // "Incorrect period_unit: {}. Invalid period unit. Must be Day, Week, Month, or Year.\n", + // period_unit + // ) + // .into(), + // ); + // error + // })?; + + // uses FromStr implementation to parse the string into the enum variant: fn from_str(s: &str) -> Result; + // converting from strum parse error to Validation error without thisError crate since no custom error defined here + period_unit.parse::().map_err(|_| { + let mut error = ValidationError::new("invalid_period_unit"); + error.message = Some( + format!( + "Incorrect period_unit: {}. Must be Day, Week, Month, or Year.\n", + period_unit + ) + .into(), + ); + error + })?; + Ok(()) +} + +#[derive(Default, Debug, Deserialize, Validate, Serialize)] +pub struct NewBank { + #[validate(length(min = 1))] + pub name: String, + #[validate(custom(function = "validate_bank_tz"))] + pub bank_tz: String, + // requires Serialize trait because it needs to be able to convert the NewDeposit instances into a serializable format for error reporting. + #[validate(length(min = 1), nested)] + pub new_deposits: Vec, +} + +fn validate_bank_tz(bank_tz: &str) -> Result<(), ValidationError> { + bank_tz.parse::().map_err(|e| { + let mut error = ValidationError::new("invalid_tz"); + error.message = Some( + format!( + "Error: {}. Incorrect timezone: {}. Must be a valid timezone.\n", + e, bank_tz + ) + .into(), + ); + error + })?; + Ok(()) +} +#[derive(Default, Debug, Deserialize, Validate, Serialize)] +pub struct NewDeposit { + #[validate(length(min = 4))] + pub account: String, + #[validate(custom(function = "validate_account_type"))] + pub account_type: String, + #[validate(custom(function = "validate_decimal"))] + pub apy: String, + #[validate(custom(function = "validate_positive_decimal"))] + pub years: String, + #[validate(custom(function = "validate_decimal"))] + pub amount: String, + #[validate(custom(function = "validate_iso8601_date"))] + pub start_date_in_bank_tz: String, +} + +fn validate_iso8601_date(date_str: &str) -> Result<(), ValidationError> { + NaiveDate::parse_from_str(date_str, "%Y-%m-%d").map_err(|e| { + let mut error = ValidationError::new("invalid_iso8601_datetime"); + error.message = Some( + format!( + "Error: {}. Incorrect date format: {}. Must be in ISO 8601 format YYYY-MM-DD.\n", + e, date_str + ) + .into(), + ); + error + })?; + Ok(()) +} + +#[derive(Default, Deserialize, Debug, EnumString)] +pub enum AccountType { + #[default] + Unspecified = 0, + Checking = 1, + Savings = 2, + CertificateOfDeposit = 3, + BrokerageCertificateOfDeposit = 4, +} + +fn validate_account_type(account_type: &str) -> Result<(), ValidationError> { + AccountType::from_str(account_type).map_err(|e| { + let mut error = ValidationError::new("invalid_account_type"); + error.message = Some( + format!( + "Error: {}. Incorrect account_type: {}. Must be Checking, Savings, CertificateOfDeposit, or BrokerageCertificateOfDeposit.\n", + e, account_type + ) + .into(), + ); + error + })?; + Ok(()) +} + +fn validate_decimal(value: &str) -> Result<(), ValidationError> { + let v = match value.parse::() { + Ok(val) => val, + Err(_) => { + let mut error = ValidationError::new("invalid_decimal"); + error.message = Some( + format!( + "Incorrect value: {}. Must be a valid decimal number.\n", + value + ) + .into(), + ); + return Err(error); + } + }; + if v.is_sign_negative() { + let mut error = ValidationError::new("invalid_decimal"); + error.message = Some( + format!( + "Incorrect value: {}. Must not be a negative number.\n", + value + ) + .into(), + ); + Err(error) + } else { + Ok(()) + } +} + +fn validate_positive_decimal(value: &str) -> Result<(), ValidationError> { + validate_decimal(value)?; + if value.parse::().unwrap().is_zero() { + let mut error = ValidationError::new("invalid_positive_decimal"); + error.message = Some( + format!( + "Incorrect value: {}. Must be a positive decimal number.\n", + value + ) + .into(), + ); + return Err(error); + } + Ok(()) +} + +// Response sections + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct CalculatePortfolioResponse { + pub uuid: String, + pub banks: Vec, + pub outcome: Option, + pub created_at: String, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct Delta { + pub period: String, + pub period_unit: String, + pub growth: String, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct Maturity { + pub amount: String, + pub interest: String, + pub total: String, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct Bank { + pub uuid: String, + pub name: String, + pub bank_tz: String, + pub deposits: Vec, + pub outcome: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct Deposit { + pub uuid: String, + pub account: String, + pub account_type: String, + pub apy: String, + pub years: String, + pub outcome: Option, + pub outcome_with_dates: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct Outcome { + pub delta: Option, + pub maturity: Option, + pub errors: Vec, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct OutcomeWithDates { + pub start_date_in_bank_tz: String, + pub maturity_date_in_bank_tz: Option, + pub errors: Vec, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct ProcessingError { + pub uuid: String, + pub message: String, +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..170a14e --- /dev/null +++ b/justfile @@ -0,0 +1,611 @@ +# clean +clean: + cargo clean + +clean-with-lambdas:clean-build-drive-deposits-dynamodb-queries clean-build-drive-deposit-event-rules clean-build-drive-deposit-event-bus + cargo clean + +# only for checking; could use cargo build --workspace --tests instead also +# for further confirmation -- keeps binary separate from tests still +check-with-tests: + cargo check --workspace --tests + +# build must have default-members or pass --workspace +build-with-tests: + cargo build --workspace --tests + +build: + cargo build --workspace + + +build-with-lambdas:build-drive-deposits-event-bus build-drive-deposits-event-rules build-drive-deposits-dynamodb-queries + cargo build --workspace --tests + +udeps: + cargo +nightly udeps --workspace + +build-drive-deposits-cal-types: + cd drive-deposits-cal-types + cargo build --package drive-deposits-cal-types + +build-drive-deposits-proto-grpc-types: + cd drive-deposits-proto-grpc-types + cargo build --package drive-deposits-proto-grpc-types + +build-drive-deposits-rest-types: + cd drive-deposits-rest-types + cargo build --package drive-deposits-rest-types + +# See README.md for more details. +build-drive-deposits-check-cmd: + cd drive-deposits-check-cmd + cargo build --package drive-deposits-check-cmd + +build-drive-deposits-grpc-server: + cd drive-deposits-grpc-server + cargo build --package drive-deposits-grpc-server + +build-drive-deposits-rest-gateway-server: + cd drive-deposits-rest-gateway-server + cargo build --package drive-deposits-rest-gateway-server + +# watching builds +# .cargo/cargo.toml has SEND_CAL_EVENTS = "true" so we can overrride it here as needed +watch-build: + cargo watch -x "build --workspace" + +watch-build-with-tests: + cargo watch -x "build --workspace --tests" + +watch-build-drive-deposits-cal-types: + cd drive-deposits-cal-types + cargo watch -x "build --package drive-deposits-cal-types" + +watch-build-drive-deposits-event-source: + cd drive-deposits-event-source + cargo watch -x "build --package drive-deposits-event-source" + +watch-build-drive-deposits-grpc-server: + cd drive-deposits-grpc-server + cargo watch -x "build --package drive-deposits-grpc-server" + +# .cargo/cargo.toml has SEND_CAL_EVENTS = "true" so we can overrride it here as needed +# run only +run-drive-deposits-check-cmd-valid: + SEND_CAL_EVENTS="false" cargo ddcheck -- drive-deposits-rest-gateway-server/data/rest_request_valid.json + +run-drive-deposits-check-cmd-valid-send-events: + SEND_CAL_EVENTS="true" cargo ddcheck -- drive-deposits-rest-gateway-server/data/rest_request_valid.json + +run-drive-deposits-check-cmd-valid-send-events-lesser-amount-investments: + SEND_CAL_EVENTS="true" cargo ddcheck -- drive-deposits-rest-gateway-server/data/rest_request_valid_lesser_amount_investments.json + +run-drive-deposits-check-cmd-valid-send-events-greater-amount-investments: + SEND_CAL_EVENTS="true" cargo ddcheck -- drive-deposits-rest-gateway-server/data/rest_request_valid_greater_amount_investments.json + +run-drive-deposits-check-cmd-invalid-decimal: + SEND_CAL_EVENTS="false" cargo ddcheck -- drive-deposits-rest-gateway-server/data/rest_request_invalid_decimal.json + +run-drive-deposits-check-cmd-invalid: + SEND_CAL_EVENTS="false" cargo ddcheck -- drive-deposits-rest-gateway-server/data/rest_request_invalid_period_unit_account_type_decimal_bank_tz_start_date.json + +run-drive-deposits-grpc-server: + SEND_CAL_EVENTS="true" cargo run --package drive-deposits-grpc-server --bin drive-deposits-grpc-server + +run-drive-deposits-rest-grpc-gateway-server: + cargo run --package drive-deposits-rest-gateway-server --bin drive-deposits-rest-gateway-server + +# .cargo/cargo.toml has SEND_CAL_EVENTS = "true" so we can overrride it here as needed +# watch running +watch-run-drive-deposits-check-cmd-valid: + SEND_CAL_EVENTS="false" cargo watch -x "ddcheck -- examples/data/rest_server_as_gateway_request_valid.json" + +watch-run-drive-deposits-check-cmd-valid-send-events: + SEND_CAL_EVENTS="true" cargo watch -x "ddcheck -- examples/data/rest_server_as_gateway_request_valid.json" + +watch-run-drive-deposits-check-cmd-invalid-decimal: + SEND_CAL_EVENTS="false" cargo watch -x "ddcheck -- examples/data/rest_server_as_gateway_request_invalid_decimal.json" + +watch-run-drive-deposits-check-cmd-invalid: + SEND_CAL_EVENTS="false" cargo watch -x "ddcheck -- examples/data/rest_server_as_gateway_request_invalid_period_unit_account_type_decimal_bank_tz_start_date.json" + +watch-run-drive-deposits-grpc-server: + cargo watch --why --poll -x "run --package drive-deposits-grpc-server --bin drive-deposits-grpc-server" + +watch-run-drive-deposits-rest-gateway-server: + cargo watch --why --poll -x "run --package drive-deposits-rest-gateway-server --bin drive-deposits-rest-gateway-server" + +# test +test: + cargo test --workspace + +# watch test +watch-test: + cargo watch -x "test --workspace" + + +# deploy additions + +# actual aws deploy (localstack counterpart below) + +# event source with bus +validate-drive-deposits-event-bus: + echo "validating before building DriveDepositsEventBus" && \ + cd drive-deposits-event-source && \ + sam validate --lint --template-file template.yaml + +build-drive-deposits-event-bus:validate-drive-deposits-event-bus + echo "building before deploy DriveDepositsEventBus" && \ + cd drive-deposits-event-source && \ + sam build --template-file template.yaml + +deploy-drive-deposits-event-bus:build-drive-deposits-event-bus + echo "deploy DriveDepositsEventBus" && \ + cd drive-deposits-event-source && \ + sam deploy --no-confirm-changeset --config-env dev || true + +# if needed guided; usually needed to create samconfig.yml initially that can be modifed later +deploy-guided-drive-deposits-event-bus:build-drive-deposits-event-bus + echo "deploy guided DriveDepositsEventBus"&& \ + cd drive-deposits-event-source && \ + sam deploy --guided + +# event target with rule +validate-drive-deposits-event-rules: + echo "validating before building event rules" && \ + cd drive-deposits-logs-lambda-target && \ + sam validate --lint --beta-features --template-file template.yaml + +build-drive-deposits-event-rules:validate-drive-deposits-event-rules + echo "building before deploy event rules" && \ + cd drive-deposits-logs-lambda-target && \ + sam build --beta-features --template-file template.yaml + +deploy-drive-deposits-event-rules:deploy-drive-deposits-event-bus build-drive-deposits-event-rules + echo "deploy event rules" + cd drive-deposits-logs-lambda-target && \ + sam deploy --no-confirm-changeset --config-env dev --parameter-overrides UseLocalstack="false" Environment="dev" || true + +# if needed guided; usually needed to create samconfig.yml initially that can be modifed later +deploy-guided-drive-deposits-event-rules:build-drive-deposits-event-rules + echo "deploy guided event rules" && \ + cd drive-deposits-logs-lambda-target && \ + sam deploy --guided + +aws-invoke-drive-deposits-event-rules-lambda: + echo "invoking lambda function" && \ + cd drive-deposits-logs-lambda-target && \ + aws lambda invoke --invocation-type=Event --function-name drive_deposits_by_level_lambda_writer_dev --payload file://data/drive-deposits-event-banks-level.json --cli-binary-format raw-in-base64-out out.json + +logs-drive-deposits-event-rules-lambda: + echo "localstack logs" && \ + cd drive-deposits-logs-lambda-target && \ + sam logs --stack-name drive-deposits-event-rules-dev --name DriveDepositsByLevelLambdaFunction -t + +# cargo lambda for pure local development start withiout even localstack conatiner +# if needed for cargo lambda without going through sam for just lambda function +# cargo lambda build --release +cargo-lambda-build-drive-deposits-event-rules-lambda: + echo "building lambda function" && \ + cd drive-deposits-logs-lambda-target && \ + cargo lambda build + +cargo-lambda-build-watching-drive-deposits-event-rules-lambda: + echo "building lambda function" && \ + cd drive-deposits-logs-lambda-target && \ + cargo watch -x "lambda build" + +# for reader and writer to work together in localstack +localstack_drive_deposit_table_name := "drive-deposits-event-rules-dev-DriveDepositsTable-8bfc05e7" + +# RUST_LOG=by_level_lambda_writer=debug not needed as added in .cargo/config.toml +# cargo lambda build --release +cargo-lambda-watch-drive-deposits-event-rules-lambda: + echo "watching lambda function" && \ + cd drive-deposits-logs-lambda-target && \ + export DRIVE_DEPOSITS_TABLE_NAME={{localstack_drive_deposit_table_name}} && \ + export USE_LOCALSTACK="true" && \ + echo $DRIVE_DEPOSITS_TABLE_NAME && \ + echo $USE_LOCALSTACK && \ + cargo lambda watch + +cargo-lambda-invoke-drive-deposits-event-rules-lambda: + echo "invoking lambda function" && \ + cd drive-deposits-logs-lambda-target && \ + cargo lambda invoke by_level_lambda_writer --data-file data/drive-deposits-event-banks-level.json + +# deployed delete + +# event target with rule +deployed-delete-drive-deposits-event-rules: + echo "deployed delete drive-deposits-event-rules-dev" && \ + cd drive-deposits-logs-lambda-target && \ + sam delete --stack-name drive-deposits-event-rules-dev --region us-west-2 + +# event source with bus +deployed-delete-drive-deposits-event-bus:deployed-delete-drive-deposits-dynamodb-queries deployed-delete-drive-deposits-event-rules + echo "deployed delete DriveDepositsEventBus" && \ + cd drive-deposits-event-source && \ + sam delete --stack-name drive-deposits-event-bus-dev --region us-west-2 + +# clean +clean-build-drive-deposit-event-bus: + echo "cleaning build drive-deposits-event-bus" && \ + rm -rf ./drive-deposits-event-source/.aws-sam || true + +clean-build-drive-deposit-event-rules: clean-build-drive-deposit-event-bus + echo "cleaning build drive-deposit-event-rules" && \ + rm -rf ./drive-deposits-logs-lambda-target/.aws-sam || true + rm -rf ./drive-deposits-logs-lambda-target/target || true + + +# localstack; can be started in Docker desktop terminal ( actual aws deploy counterpart above) +localstack-build: + # Build the Docker image using the custom Dockerfile name + LOCALSTACK_DEBUG=1 docker build -f Dockerfile.localstack -t custom-localstack . + +localstack-start:localstack-build + # Run a container from the built image, mapping the ports + docker run -p 4566:4566 -p 4510-4559:4510-4559 \ + -v ${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack \ + -v /var/run/docker.sock:/var/run/docker.sock \ + custom-localstack + +# after localstack started +# in localstack event source with bus +localstack-build-drive-deposits-event-bus: + echo "localstack building before deploy DriveDepositsEventBus" && \ + cd drive-deposits-event-source && \ + samlocal build --template-file template.yaml + +# to not error out if samlocal deploy --config-env dev already exists +localstack-deploy-drive-deposits-event-bus:localstack-build-drive-deposits-event-bus + echo "localstack deploy DriveDepositsEventBus" && \ + cd drive-deposits-event-source && \ + samlocal deploy --no-confirm-changeset --config-env dev || true + +# in localstack event target with rule +localstack-build-drive-deposits-event-rules: + echo "localstack building before deploy rules" && \ + cd drive-deposits-logs-lambda-target && \ + samlocal build --beta-features --template-file template.yaml + +localstack-deploy-drive-deposits-event-rules:localstack-deploy-drive-deposits-event-bus localstack-build-drive-deposits-event-rules + echo "localstack deploy rules" + cd drive-deposits-logs-lambda-target && \ + samlocal deploy --no-confirm-changeset --config-env dev --parameter-overrides UseLocalstack="true" Environment="dev" + +# only this receipe for localstack +localstack-deploy-drive-deposits-event-rules-only: + echo "localstack deploy rules" + cd drive-deposits-logs-lambda-target && \ + samlocal deploy --no-confirm-changeset --config-env dev --parameter-overrides UseLocalstack="true" Environment="dev" + +awslocal-invoke-drive-deposits-event-rules-lambda: + echo "invoking lambda function" && \ + cd drive-deposits-logs-lambda-target && \ + awslocal lambda invoke --invocation-type=Event --function-name drive_deposits_by_level_lambda_writer_dev --payload file://data/drive-deposits-event-banks-level.json --cli-binary-format raw-in-base64-out out.json + +awslocal-list-dynamodb-tables: + # --endpoint-url https://localhost.localstack.cloud:4566 + # dynamodb-proxy has dynamodb_endpoint_url = https://localhost.localstack.cloud:4566 + awslocal dynamodb list-tables --profile dynamodb-proxy + + +# localstack run +localstack-run-drive-deposits-check-cmd-valid-send-events: + USE_LOCALSTACK="true" SEND_CAL_EVENTS="true" cargo ddcheck -- drive-deposits-rest-gateway-server/data/rest_request_valid.json + +localstack-run-drive-deposits-check-cmd-valid-send-events-lesser-amount-investments: + USE_LOCALSTACK="true" SEND_CAL_EVENTS="true" cargo ddcheck -- drive-deposits-rest-gateway-server/data/rest_request_valid_lesser_amount_investments.json + +localstack-run-drive-deposits-check-cmd-valid-send-events-greater-amount-investments: + USE_LOCALSTACK="true" SEND_CAL_EVENTS="true" cargo ddcheck -- drive-deposits-rest-gateway-server/data/rest_request_valid_greater_amount_investments.json + +# localstack watch +# .cargo/config.toml has USE_LOCALSTACK = "false" so we can overrride it here as needed +localstack-watch-run-drive-deposits-check-cmd-valid-send-events: + USE_LOCALSTACK="true" SEND_CAL_EVENTS="true" cargo watch -x "ddcheck -- drive-deposits-rest-gateway-server/data/rest_request_valid.json" + +# localstack logs from samlocal +localstack-logs-drive-deposits-event-rules-lambda: + echo "localstack logs" && \ + cd drive-deposits-logs-lambda-target && \ + samlocal logs --stack-name drive-deposits-event-rules-dev --name DriveDepositsByLevelLambdaFunction -t + +# localstack clean automatically happens on shutodwn since localstack by default is ephemeral +localstack-clean-build-drive-deposit-event-rules:clean-build-drive-deposit-event-rules + echo "localstack cleaned build drive-deposit-event-rules" + + + +# rest gateway server curl POST commands to send calculation events to target through RESRT and gRPC servers -- same as in request.http at drive-deposits-rest-gateway-server/request.http +# however had to exclude br since in curl not directly supported in curl verison 8.7.1 +# the request.http supports it so alternatively that can be used for br brotli compression +# Define variables +rest_gateway_server_host := "http://localhost:3000" +token := "Bearer token" + +# Recipe for the POST request with root path +post-root: + cd drive-deposits-rest-gateway-server && \ + curl -X POST {{rest_gateway_server_host}}/ \ + -H "Content-Type: application/json" \ + -H "Authorization: {{token}}" \ + -H "Accept-Encoding: gzip, deflate" \ + --data @./data/rest_request_valid.json \ + --compressed + +# Recipe for the POST request with correct API path +post-calculate-portfolio-valid: + cd drive-deposits-rest-gateway-server && \ + curl -X POST {{rest_gateway_server_host}}/api/drive-deposits/calculate-portfolio \ + -H "Content-Type: application/json" \ + -H "Authorization: {{token}}" \ + -H "Accept-Encoding: gzip, deflate" \ + --data @./data/rest_request_valid.json \ + --compressed \ + | jq + +post-calculate-portfolio-valid-lesser-amount: + cd drive-deposits-rest-gateway-server && \ + curl -X POST {{rest_gateway_server_host}}/api/drive-deposits/calculate-portfolio \ + -H "Content-Type: application/json" \ + -H "Authorization: {{token}}" \ + -H "Accept-Encoding: gzip, deflate" \ + --data @./data/rest_request_valid_lesser_amount_investments.json \ + --compressed \ + | jq + +post-calculate-portfolio-valid-greater-amount: + cd drive-deposits-rest-gateway-server && \ + curl -X POST {{rest_gateway_server_host}}/api/drive-deposits/calculate-portfolio \ + -H "Content-Type: application/json" \ + -H "Authorization: {{token}}" \ + -H "Accept-Encoding: gzip, deflate" \ + --data @./data/rest_request_valid_greater_amount_investments.json \ + --compressed \ + | jq + + +# Recipe for the invalid decimal request +post-calculate-portfolio-invalid-decimal: + cd drive-deposits-rest-gateway-server && \ + curl -X POST {{rest_gateway_server_host}}/api/drive-deposits/calculate-portfolio \ + -H "Content-Type: application/json" \ + -H "Authorization: {{token}}" \ + -H "Accept-Encoding: gzip, deflate" \ + --data @./data/rest_request_invalid_decimal.json \ + --compressed + +# Recipe for the invalid period unit, account type, decimal, and bank tz start date request +post-calculate-portfolio-invalid-period-unit-account-type-decimal-bank-tz-start-date: + cd drive-deposits-rest-gateway-server && \ + curl --include -X POST {{rest_gateway_server_host}}/api/drive-deposits/calculate-portfolio \ + -H "Content-Type: application/json" \ + -H "Authorization: {{token}}" \ + -H "Accept-Encoding: gzip, deflate" \ + --data @./data/rest_request_invalid_period_unit_account_type_decimal_bank_tz_start_date.json \ + --compressed + +# Recipe for the invalid JSON structure request +post-calculate-portfolio-invalid-json-structure: + cd drive-deposits-rest-gateway-server && \ + curl -X POST {{rest_gateway_server_host}}/api/drive-deposits/calculate-portfolio \ + -H "Content-Type: application/json" \ + -H "Authorization: {{token}}" \ + -H "Accept-Encoding: gzip, deflate" \ + --data @./data/rest_request_invalid_json_structure.json \ + --compressed + +# Recipe for the valid 90 days delta period request +post-calculate-portfolio-valid-90-days-delta-period: + cd drive-deposits-rest-gateway-server && \ + curl -X POST {{rest_gateway_server_host}}/api/drive-deposits/calculate-portfolio \ + -H "Content-Type: application/json" \ + -H "Authorization: {{token}}" \ + -H "Accept-Encoding: gzip, deflate" \ + --data @./data/rest_request_valid_90_days_delta_period.json \ + --compressed \ + | jq + +# Recipe for the valid 6 months delta period request +post-calculate-portfolio-valid-6-months-delta-period: + cd drive-deposits-rest-gateway-server && \ + curl -X POST {{rest_gateway_server_host}}/api/drive-deposits/calculate-portfolio \ + -H "Content-Type: application/json" \ + -H "Authorization: {{token}}" \ + -H "Accept-Encoding: gzip, deflate" \ + --data @./data/rest_request_valid_6_months_delta_period.json \ + --compressed \ + | jq + + +######################## +# lambda reader for dynamodb queries per writes by lambda writer to dynamodb in lambda target +######################## + +# cargo lambda for pure local development start withiout even localstack conatiner +# if needed for cargo lambda without going through sam for just lambda function +# cargo lambda build --release +cargo-lambda-build-drive-deposits-lambda-dynamodb-reader: + echo "building lambda reader function" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + cargo lambda build + +cargo-lambda-build-watching-drive-deposits-lambda-dynamodb-reader: + USE_LOCALSTACK="true" echo "building lambda reader function" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + cargo watch -x "lambda build" + + +# RUST_LOG=by_level_lambda_writer=debug not needed as added in .cargo/config.toml +# manually since localstack community version does not support apigwv2 +# export DRIVE_DEPOSITS_TABLE_NAME=drive-deposits-event-rules-dev-DriveDepositsTable-62d90558 +# export USE_LOCALSTACK="true" +# echo $DRIVE_DEPOSITS_TABLE_NAME +# echo $USE_LOCALSTACK +cargo-lambda-watch-drive-deposits-lambda-dynamodb-reader-localstack-for-dynamodb: + echo "watching lambda reader function" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + export DRIVE_DEPOSITS_TABLE_NAME={{localstack_drive_deposit_table_name}} && \ + export USE_LOCALSTACK="true" && \ + echo $DRIVE_DEPOSITS_TABLE_NAME && \ + echo $USE_LOCALSTACK && \ + cargo lambda watch + +# cargo lambda invoke by_level_lambda_reader --data-file data/apigw-event-request-query-for-portfolios.json +# based on cargo lambda invoke by_level_lambda_reader --data-example apigw-request --skip-cache +# alternatelively directly invoking lambda in postman quivalent to cargo lambda invoke:required special urls -- {{cargo_lambda_url}}/lambda-url/by_level_lambda_reader/by-level-for-portfolios/delta-growth where +# {{cargo_lambda_url}} is the url http://[::]:9000 +cargo-lambda-invoke-drive-deposits-lambda-dynamodb-reader-apigw-event-query-for-portfolios-delta-growth: + echo "invoking lambda reader function" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + cargo lambda invoke by_level_lambda_reader --data-file data/apigw-event-request-query-for-portfolios-delta-growth.json + +cargo-lambda-invoke-drive-deposits-lambda-dynamodb-reader-apigw-event-query-for-banks-delta-growth: + echo "invoking lambda reader function" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + cargo lambda invoke by_level_lambda_reader --data-file data/apigw-event-request-query-for-banks-delta-growth.json + +cargo-lambda-invoke-drive-deposits-lambda-dynamodb-reader-apigw-event-query-for-deposits-delta-growth: + echo "invoking lambda reader function" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + cargo lambda invoke by_level_lambda_reader --data-file data/apigw-event-request-query-for-deposits-delta-growth.json + +cargo-lambda-invoke-drive-deposits-lambda-dynamodb-reader-apigw-event-query-for-deposits-maturity-date: + echo "invoking lambda reader function" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + cargo lambda invoke by_level_lambda_reader --data-file data/apigw-event-request-query-for-deposits-maturity-date.json + + +# after full deploy and ingestion writes are completed +# dynamodb queries based on designed persistence in drive-deposits-logs-lambda-target +# in sequence of usage the recipes below +# dynamodb reader for queries +validate-drive-deposits-dynamodb-queries: + echo "validating before building DriveDepositsByLevelLambdaReaderFunction" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + sam validate --lint --template-file template.yaml + +# aws guided +# if needed guided; usually needed to create samconfig.yml initially that can be modifed later +deploy-guided-drive-deposits-dynamodb-queries: + echo "deploy guided event rules" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + sam deploy --guided + +# API Gateway v2 is not supported by localstack community edition...have to be on pro version +# here for sanity check for any other issues +# onto localstack guided +# initially manual confirmation +localstack-deploy-guided-drive-deposits-dynamodb-queries: + echo "deploy guided event rules" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + samlocal deploy --guided + + +localstack-build-drive-deposits-dynamodb-queries: + echo "localstack building before deploy queries" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + samlocal build --beta-features --template-file template.yaml + +# note localstack community version does not support apigwv2 +# use only with pro version of localstack +# error - To find out if AWS::ApiGatewayV2::Api is supported in LocalStack Pro, please check out our docs at https://docs.localstack.cloud/user-guide/aws/cloudformation/#resources-pro--enterprise-edition +localstack-deploy-drive-deposits-dynamodb-queries:localstack-deploy-drive-deposits-event-rules localstack-build-drive-deposits-dynamodb-queries + echo "localstack deploy DriveDepositsByLevelLambdaReaderFunction" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + samlocal deploy --no-confirm-changeset --config-env dev --parameter-overrides UseLocalstack="true" Environment="dev" DriveDepositsTableName="drive-deposits-event-rules-dev-DRIVE-DEPOSITS-TABLE-NAME" + +# only this receipe for localstack +# note localstack community version does not support apigwv2 +localstack-deploy-drive-deposits-dynamodb-queries-only: + echo "localstack deploy DriveDepositsByLevelLambdaReaderFunction" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + samlocal deploy --no-confirm-changeset --config-env dev --parameter-overrides UseLocalstack="true" Environment="dev" DriveDepositsTableName="drive-deposits-event-rules-dev-DRIVE-DEPOSITS-TABLE-NAME" + +# back to aws +build-drive-deposits-dynamodb-queries:validate-drive-deposits-dynamodb-queries + echo "building before deploy DriveDepositsByLevelLambdaReaderFunction" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + sam build --beta-features --template-file template.yaml + +deploy-drive-deposits-dynamodb-queries:deploy-drive-deposits-event-rules build-drive-deposits-dynamodb-queries + echo "deploy event DriveDepositsByLevelLambdaReaderFunction" + cd drive-deposits-lambda-dynamodb-reader && \ + sam deploy --no-confirm-changeset --config-env dev --parameter-overrides UseLocalstack="false" Environment="dev" DriveDepositsTableName="drive-deposits-event-rules-dev-DRIVE-DEPOSITS-TABLE-NAME" + +deploy-drive-deposits-dynamodb-queries-only:build-drive-deposits-dynamodb-queries + echo "deploy event DriveDepositsByLevelLambdaReaderFunction" + cd drive-deposits-lambda-dynamodb-reader && \ + sam deploy --no-confirm-changeset --config-env dev --parameter-overrides UseLocalstack="false" Environment="dev" DriveDepositsTableName="drive-deposits-event-rules-dev-DRIVE-DEPOSITS-TABLE-NAME" + + +logs-drive-deposits-dynamodb-queries-lambda: + echo "localstack logs" && \ + cd drive-deposits-logs-lambda-target && \ + sam logs --stack-name drive-deposits-dynamodb-queries --name DriveDepositsByLevelLambdaReaderFunction -t + +deployed-delete-drive-deposits-dynamodb-queries: + echo "deployed delete DriveDepositsByLevelLambdaReaderFunction" && \ + cd drive-deposits-lambda-dynamodb-reader && \ + sam delete --stack-name drive-deposits-dynamodb-queries --region us-west-2 + + +# clean +clean-build-drive-deposits-dynamodb-queries: + echo "drive-deposits-dynamodb-queries" && \ + rm -rf ./drive-deposits-lambda-dynamodb-reader/.aws-sam || true + rm -rf ./drive-deposits-lambda-dynamodb-reader/target || true + + +# rest lambda query curl GET commands +# Recipe for query by responses level for different responses submitted and sorted by delta period per correctly saved in order in dynamodb + +api_gateway_host := "https://979g34of3m.execute-api.us-west-2.amazonaws.com/" +get-query-by-level-portfolios-delta-growth: + cd drive-deposits-lambda-dynamodb-reader && \ + curl '{{api_gateway_host}}/by-level-for-portfolios/delta-growth?order=asc&top_k=10' \ + | jq + +get-query-by-level-for-banks-delta-growth: + cd drive-deposits-lambda-dynamodb-reader && \ + curl --location '{{api_gateway_host}}/portfolios/9ee49480-359c-4c87-a31d-a92aff218601/by-level-for-banks/delta-growth/?order=asc&top_k=10' \ + | jq + +get-query-by-level-for-deposits-delta-growth: + cd drive-deposits-lambda-dynamodb-reader && \ + curl --location '{{api_gateway_host}}/portfolios/9ee49480-359c-4c87-a31d-a92aff218601/by-level-for-deposits/delta-growth?order=asc&top_k=10' \ + | jq + +get-query-by-level-for-deposits-maturity-date: + cd drive-deposits-lambda-dynamodb-reader && \ + curl --location '{{api_gateway_host}}/portfolios/9ee49480-359c-4c87-a31d-a92aff218601/by-level-for-deposits/delta-growth?order=asc&top_k=10' \ + | jq + +# for git release tags +git-tag-add-local TAG: + git tag -a {{TAG}} -m "Release version {{TAG}}" + +git-tag-add-force-local TAG: + git tag -a {{TAG}} -f -m "Release version {{TAG}}" + +git-tag-add-remote TAG: + git push origin {{TAG}} + +git-tag-add-force-remote TAG: + git push origin {{TAG}} --force + +git-tag-delete-local TAG: + git tag -d {{TAG}} + +git-tag-delete-remote TAG: + git push origin :refs/tags/{{TAG}} + +git-tag-list-verbose: + git tag -n + +git-tag-show-commit TAG: + @echo "Commit for tag {{TAG}}:" + @git show-ref --tags {{TAG}} | cut -d ' ' -f 1 | xargs git show --no-patch --pretty='format:Commit SHA: %H%nCommit Message: %s' \ No newline at end of file