From 1732d26216483638a471da73db4d00880cbf227e Mon Sep 17 00:00:00 2001 From: Brooks Townsend Date: Thu, 14 Dec 2023 11:38:00 -0500 Subject: [PATCH] feat(*)!: create bank aggregate and projector Signed-off-by: Brooks Townsend ensure providers run on cosmonic Signed-off-by: Brooks Townsend --- Justfile | 4 +- aggregate/.cargo/config.toml | 5 + aggregate/.gitignore | 16 ++++ .../.keys/bankaccount_aggregate_module.nk | 1 + aggregate/Cargo.toml | 27 ++++++ aggregate/README.md | 33 +++++++ aggregate/src/commands.rs | 14 +++ aggregate/src/events.rs | 17 ++++ aggregate/src/lib.rs | 38 ++++++++ aggregate/src/state.rs | 69 ++++++++++++++ aggregate/wasmcloud.toml | 7 ++ eventcatalog/README.md | 4 + eventcatalog/actor/Cargo.lock | 2 +- eventcatalog/actor/Cargo.toml | 2 +- eventcatalog/all_events.png | Bin 0 -> 29306 bytes eventcatalog/events/AccountCreated/index.md | 20 ++++ .../events/AccountCreated/schema.json | 26 +++++ eventcatalog/events/CreateAccount/index.md | 17 ++++ eventcatalog/events/CreateAccount/schema.json | 25 +++++ eventcatalog/package-lock.json | 4 +- eventcatalog/package.json | 2 +- .../services/Bank Account Aggregate/index.md | 11 +++ .../services/Bank Account Projector/index.md | 11 +++ projector/.cargo/config.toml | 5 + projector/.gitignore | 16 ++++ .../.keys/bankaccount_projector_module.nk | 1 + projector/Cargo.toml | 28 ++++++ projector/README.md | 22 +++++ projector/src/lib.rs | 17 ++++ projector/src/store.rs | 90 ++++++++++++++++++ projector/wasmcloud.toml | 7 ++ wadm.yaml | 79 ++++++++++++++- 32 files changed, 611 insertions(+), 9 deletions(-) create mode 100644 aggregate/.cargo/config.toml create mode 100644 aggregate/.gitignore create mode 100644 aggregate/.keys/bankaccount_aggregate_module.nk create mode 100644 aggregate/Cargo.toml create mode 100644 aggregate/README.md create mode 100644 aggregate/src/commands.rs create mode 100644 aggregate/src/events.rs create mode 100644 aggregate/src/lib.rs create mode 100644 aggregate/src/state.rs create mode 100644 aggregate/wasmcloud.toml create mode 100644 eventcatalog/all_events.png create mode 100644 eventcatalog/events/AccountCreated/index.md create mode 100644 eventcatalog/events/AccountCreated/schema.json create mode 100644 eventcatalog/events/CreateAccount/index.md create mode 100644 eventcatalog/events/CreateAccount/schema.json create mode 100644 eventcatalog/services/Bank Account Aggregate/index.md create mode 100644 eventcatalog/services/Bank Account Projector/index.md create mode 100644 projector/.cargo/config.toml create mode 100644 projector/.gitignore create mode 100644 projector/.keys/bankaccount_projector_module.nk create mode 100644 projector/Cargo.toml create mode 100644 projector/README.md create mode 100644 projector/src/lib.rs create mode 100644 projector/src/store.rs create mode 100644 projector/wasmcloud.toml diff --git a/Justfile b/Justfile index e6023c6..56adbc6 100644 --- a/Justfile +++ b/Justfile @@ -10,7 +10,9 @@ build: (cd $dir && wash build); \ done -version := "0.0.0" +version := "0.1.0" push: # Push to GHCR + wash push ghcr.io/cosmonic/cosmonic-gitops/bankaccount_projector:{{version}} projector/build/bankaccount_projector_s.wasm + wash push ghcr.io/cosmonic/cosmonic-gitops/bankaccount_aggregate:{{version}} aggregate/build/bankaccount_aggregate_s.wasm wash push ghcr.io/cosmonic/cosmonic-gitops/bankaccount_catalog:{{version}} eventcatalog/actor/build/bankaccountcatalog_s.wasm \ No newline at end of file diff --git a/aggregate/.cargo/config.toml b/aggregate/.cargo/config.toml new file mode 100644 index 0000000..4905f77 --- /dev/null +++ b/aggregate/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "wasm32-unknown-unknown" + +[net] +git-fetch-with-cli = true \ No newline at end of file diff --git a/aggregate/.gitignore b/aggregate/.gitignore new file mode 100644 index 0000000..262ca9a --- /dev/null +++ b/aggregate/.gitignore @@ -0,0 +1,16 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +build/ diff --git a/aggregate/.keys/bankaccount_aggregate_module.nk b/aggregate/.keys/bankaccount_aggregate_module.nk new file mode 100644 index 0000000..54994b7 --- /dev/null +++ b/aggregate/.keys/bankaccount_aggregate_module.nk @@ -0,0 +1 @@ +SMANGOYSDZ4P3SG4GDUXECMO2J2GFOIV6NQJWGFOVA3GZBSXMGZT5GWFJ4 \ No newline at end of file diff --git a/aggregate/Cargo.toml b/aggregate/Cargo.toml new file mode 100644 index 0000000..2239cf3 --- /dev/null +++ b/aggregate/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "bankaccount-aggregate" +version = "0.2.0" +authors = ["Cosmonic Team"] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +name = "bankaccount_aggregate" + +[dependencies] +anyhow = "1.0.40" +async-trait = "0.1" +futures = { version = "0.3", features = ["executor"] } +serde_bytes = "0.11" +serde_json = "1.0.94" +serde = { version = "1.0", features = ["derive"] } +wasmbus-rpc = "0.14.0" +concordance-gen = { git = "https://github.com/cosmonic/concordance"} +wasmcloud-interface-logging = {version = "0.10.0", features = ["sync_macro"]} +regress = "0.7.1" + +[profile.release] +# Optimize for small code size +lto = true +opt-level = "s" +strip = true \ No newline at end of file diff --git a/aggregate/README.md b/aggregate/README.md new file mode 100644 index 0000000..4c02bb9 --- /dev/null +++ b/aggregate/README.md @@ -0,0 +1,33 @@ +# Bank Account Aggregate +This aggregate represents the sum of events on the `bankaccount` stream, which is keyed by the account number on the commands and events in this logical stream. + +# Configuration +The following configuration values should be set for this aggregate to work properly. +* `ROLE` - `aggregate` +* `INTEREST` - `bankaccount` +* `NAME` - `bankaccount` +* `KEY` - `account_number` + +# Manual Testing +You can send the following commands manually to watch the aggregate perform its tasks: + +## Creating an Account +You can use the following `nats req` command (edit the data as you see fit) to create a new account by submitting a new `create_account` command: +``` +nats req cc.commands.bankaccount '{"command_type": "create_account", "key": "ABC123", "data": {"account_number": "ABC123", "initial_balance": 4000, "min_balance": 100, "customer_id": "CUSTBOB"}}' +``` +You should receive a reply that looks something like this: +``` +11:25:05 Sending request on "cc.commands.bankaccount" +11:25:05 Received with rtt 281.083µs +{"stream":"CC_COMMANDS", "seq":2} +``` + +And now you can verify that you have indeed created the `ABC123` account (note the key is account number and not customer ID). +``` +nats kv get CC_STATE agg.bankaccount.ABC123 +CC_STATE > agg.bankaccount.ABC123 created @ 20 Mar 23 15:25 UTC + +{"balance":4000,"min_balance":100,"account_number":"ABC123"} +``` + diff --git a/aggregate/src/commands.rs b/aggregate/src/commands.rs new file mode 100644 index 0000000..e79e664 --- /dev/null +++ b/aggregate/src/commands.rs @@ -0,0 +1,14 @@ +use crate::*; + +pub(crate) fn handle_create_account(input: CreateAccount) -> Result { + Ok(vec![Event::new( + AccountCreated::TYPE, + STREAM, + &AccountCreated { + initial_balance: input.initial_balance, + account_number: input.account_number.to_string(), + min_balance: input.min_balance, + customer_id: input.customer_id, + }, + )]) +} diff --git a/aggregate/src/events.rs b/aggregate/src/events.rs new file mode 100644 index 0000000..8964974 --- /dev/null +++ b/aggregate/src/events.rs @@ -0,0 +1,17 @@ +use crate::*; + +impl From for BankAccountAggregateState { + fn from(input: AccountCreated) -> BankAccountAggregateState { + BankAccountAggregateState { + balance: input.initial_balance.unwrap_or(0) as _, + min_balance: input.min_balance.unwrap_or(0) as _, + account_number: input.account_number, + customer_id: input.customer_id, + reserved_funds: HashMap::new(), + } + } +} + +pub(crate) fn apply_account_created(input: AccountCreated) -> Result { + Ok(StateAck::ok(Some(BankAccountAggregateState::from(input)))) +} diff --git a/aggregate/src/lib.rs b/aggregate/src/lib.rs new file mode 100644 index 0000000..baa5b2b --- /dev/null +++ b/aggregate/src/lib.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +mod commands; +mod events; +mod state; + +use state::BankAccountAggregateState; + +concordance_gen::generate!({ + path: "../eventcatalog", + role: "aggregate", + entity: "bank account" +}); + +impl BankAccountAggregate for BankAccountAggregateImpl { + // -- Commands -- + fn handle_create_account( + &self, + input: CreateAccount, + _state: Option, + ) -> anyhow::Result { + commands::handle_create_account(input) + } + + // -- Events -- + fn apply_account_created( + &self, + input: AccountCreated, + _state: Option, + ) -> anyhow::Result { + events::apply_account_created(input) + } +} + +const STREAM: &str = "bankaccount"; diff --git a/aggregate/src/state.rs b/aggregate/src/state.rs new file mode 100644 index 0000000..b3aaa9c --- /dev/null +++ b/aggregate/src/state.rs @@ -0,0 +1,69 @@ +use crate::*; + +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +pub struct BankAccountAggregateState { + pub balance: u32, // CENTS + pub min_balance: u32, + pub account_number: String, + pub customer_id: String, + pub reserved_funds: HashMap, // wire_transfer_id -> amount +} + +impl BankAccountAggregateState { + /// Returns the regular balance minus the sum of transfer holds + pub fn available_balance(&self) -> u32 { + self.balance + .checked_sub(self.reserved_funds.values().sum::()) + .unwrap_or(0) + } + + /// Returns the total amount of funds on hold + pub fn total_reserved(&self) -> u32 { + self.reserved_funds.values().sum::() + } + + /// Releases the funds associated with a wire transfer hold. Has no affect on actual balance, only available + pub fn release_funds(self, reservation_id: &str) -> Self { + let mut new_state = self.clone(); + new_state.reserved_funds.remove(reservation_id); + + new_state + } + + /// Adds a reservation hold for a given wire transfer. Has no affect on actual balance, only available + pub fn reserve_funds(self, reservation_id: &str, amount: u32) -> Self { + let mut new_state = self.clone(); + new_state + .reserved_funds + .insert(reservation_id.to_string(), amount); + new_state + } + + /// Commits held funds. Subtracts held funds from balance. Note: A more realistic banking + /// app might emit an overdrawn/overdraft event if the new balance is less than 0. Here we + /// just floor the balance at 0. Also note that overcommits shouldn't happen because we reject + /// attempts to hold beyond available funds + pub fn commit_funds(self, reservation_id: &str) -> Self { + let mut new_state = self.clone(); + let amount = new_state.reserved_funds.remove(reservation_id).unwrap_or(0); + new_state.balance = new_state.balance.checked_sub(amount).unwrap_or(0); + new_state + } + + /// Withdraws a given amount of funds + pub fn withdraw(self, amount: u32) -> Self { + let mut new_state = self.clone(); + new_state.balance = new_state.balance.checked_sub(amount).unwrap_or(0); + new_state + } + + /// Deposits a given amount of funds. Ceilings at u32::MAX + pub fn deposit(self, amount: u32) -> Self { + let mut new_state = self.clone(); + new_state.balance = new_state + .balance + .checked_add(amount) + .unwrap_or(new_state.balance); + new_state + } +} diff --git a/aggregate/wasmcloud.toml b/aggregate/wasmcloud.toml new file mode 100644 index 0000000..ecd3909 --- /dev/null +++ b/aggregate/wasmcloud.toml @@ -0,0 +1,7 @@ +name = "BankAccountAggregate" +language = "rust" +type = "actor" + +[actor] +key_directory = "./.keys" +claims = ["cosmonic:eventsourcing", "wasmcloud:builtin:logging"] diff --git a/eventcatalog/README.md b/eventcatalog/README.md index 086c479..c0caa4f 100644 --- a/eventcatalog/README.md +++ b/eventcatalog/README.md @@ -17,3 +17,7 @@ npm run build cd actor wash build ``` + +## All Events + +![All Bank Account Events](./all_events.png) diff --git a/eventcatalog/actor/Cargo.lock b/eventcatalog/actor/Cargo.lock index 59b59d6..5962c99 100644 --- a/eventcatalog/actor/Cargo.lock +++ b/eventcatalog/actor/Cargo.lock @@ -250,7 +250,7 @@ dependencies = [ [[package]] name = "bankaccountcatalog" -version = "0.3.0" +version = "0.2.0" dependencies = [ "async-trait", "futures", diff --git a/eventcatalog/actor/Cargo.toml b/eventcatalog/actor/Cargo.toml index 9a307db..1d04bb0 100644 --- a/eventcatalog/actor/Cargo.toml +++ b/eventcatalog/actor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bankaccountcatalog" -version = "0.3.0" +version = "0.2.0" authors = ["Cosmonic Team"] edition = "2021" diff --git a/eventcatalog/all_events.png b/eventcatalog/all_events.png new file mode 100644 index 0000000000000000000000000000000000000000..69baebd1306635bf933896ca236db7c9cf27af87 GIT binary patch literal 29306 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 + +## Schema + \ No newline at end of file diff --git a/eventcatalog/events/AccountCreated/schema.json b/eventcatalog/events/AccountCreated/schema.json new file mode 100644 index 0000000..32d1bd2 --- /dev/null +++ b/eventcatalog/events/AccountCreated/schema.json @@ -0,0 +1,26 @@ +{ + "$id": "https://cosmonic.com/concordance/bankaccount/AccountCreated.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "AccountCreated", + "type": "object", + "properties": { + "accountNumber": { + "type": "string", + "description": "The account number of the new account" + }, + "minBalance": { + "type": "integer", + "description": "The minimum required maintenance balance for the account" + }, + "initialBalance": { + "type": "integer", + "description": "Initial deposit amount for the account" + }, + "customerId": { + "type": "string", + "description": "The ID of the customer" + } + }, + "required": ["accountNumber", "customerId"] + } + \ No newline at end of file diff --git a/eventcatalog/events/CreateAccount/index.md b/eventcatalog/events/CreateAccount/index.md new file mode 100644 index 0000000..ba54d68 --- /dev/null +++ b/eventcatalog/events/CreateAccount/index.md @@ -0,0 +1,17 @@ +--- +name: CreateAccount +summary: "Requests the creation of a new bank account" +version: 0.0.1 +consumers: + - 'Bank Account Aggregate' +tags: + - label: 'command' +externalLinks: [] +badges: [] +--- +Requests the creation of a new bank account. This command can fail to process if the parameters are invalid or if the account already exists. + + + +## Schema + \ No newline at end of file diff --git a/eventcatalog/events/CreateAccount/schema.json b/eventcatalog/events/CreateAccount/schema.json new file mode 100644 index 0000000..8dabe27 --- /dev/null +++ b/eventcatalog/events/CreateAccount/schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://cosmonic.com/concordance/bankaccount/CreateAccount.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "CreateAccount", + "type": "object", + "properties": { + "accountNumber": { + "type": "string", + "description": "The account number to be created" + }, + "minBalance": { + "type": "integer", + "description": "The minimum required maintenance balance for the account" + }, + "initialBalance": { + "type": "integer", + "description": "Initial deposit amount for the account" + }, + "customerId": { + "type": "string", + "description": "The ID of the customer" + } + }, + "required": ["accountNumber", "customerId"] +} diff --git a/eventcatalog/package-lock.json b/eventcatalog/package-lock.json index 12eeef8..1529e99 100644 --- a/eventcatalog/package-lock.json +++ b/eventcatalog/package-lock.json @@ -1,12 +1,12 @@ { "name": "eventcatalog", - "version": "0.0.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "eventcatalog", - "version": "0.0.0", + "version": "0.0.1", "dependencies": { "@eventcatalog/core": "1.0.1" }, diff --git a/eventcatalog/package.json b/eventcatalog/package.json index bc46ce7..f767f0b 100644 --- a/eventcatalog/package.json +++ b/eventcatalog/package.json @@ -1,6 +1,6 @@ { "name": "eventcatalog", - "version": "0.0.0", + "version": "0.1.0", "private": true, "scripts": { "start": "eventcatalog start", diff --git a/eventcatalog/services/Bank Account Aggregate/index.md b/eventcatalog/services/Bank Account Aggregate/index.md new file mode 100644 index 0000000..d032b60 --- /dev/null +++ b/eventcatalog/services/Bank Account Aggregate/index.md @@ -0,0 +1,11 @@ +--- +name: Bank Account Aggregate +summary: | + The aggregate for managing individual bank accounts +tags: + - label: 'aggregate' +--- + +The bank account aggregate is responsible for validating incoming commands and emitting the appropriate events. + + \ No newline at end of file diff --git a/eventcatalog/services/Bank Account Projector/index.md b/eventcatalog/services/Bank Account Projector/index.md new file mode 100644 index 0000000..96ce287 --- /dev/null +++ b/eventcatalog/services/Bank Account Projector/index.md @@ -0,0 +1,11 @@ +--- +name: Bank Account Projector +summary: | + The projector responsible for creating bank account read model +tags: + - label: 'projector' +--- + +This projector monitors bank account events and projects the corresponding read model. + + \ No newline at end of file diff --git a/projector/.cargo/config.toml b/projector/.cargo/config.toml new file mode 100644 index 0000000..4905f77 --- /dev/null +++ b/projector/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "wasm32-unknown-unknown" + +[net] +git-fetch-with-cli = true \ No newline at end of file diff --git a/projector/.gitignore b/projector/.gitignore new file mode 100644 index 0000000..262ca9a --- /dev/null +++ b/projector/.gitignore @@ -0,0 +1,16 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +build/ diff --git a/projector/.keys/bankaccount_projector_module.nk b/projector/.keys/bankaccount_projector_module.nk new file mode 100644 index 0000000..24f7801 --- /dev/null +++ b/projector/.keys/bankaccount_projector_module.nk @@ -0,0 +1 @@ +SMADQF4DVD4AUK2WAR5RKDW4G2S4TRNS4MS5ADERYQ6STLK7MANGYNQCPA \ No newline at end of file diff --git a/projector/Cargo.toml b/projector/Cargo.toml new file mode 100644 index 0000000..3f42a65 --- /dev/null +++ b/projector/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "bankaccount-projector" +version = "0.2.0" +authors = ["Cosmonic Team"] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +name = "bankaccount_projector" + +[dependencies] +anyhow = "1.0.40" +async-trait = "0.1" +futures = { version = "0.3", features = ["executor"] } +serde_bytes = "0.11" +serde_json = "1.0.94" +serde = { version = "1.0", features = ["derive"] } +wasmbus-rpc = "0.14.0" +concordance-gen = { git = "https://github.com/cosmonic/concordance"} +wasmcloud-interface-logging = {version = "0.10.0", features = ["sync_macro"]} +wasmcloud-interface-keyvalue = "0.11.0" +regress = "0.7.1" + +[profile.release] +# Optimize for small code size +lto = true +opt-level = "s" +strip = true \ No newline at end of file diff --git a/projector/README.md b/projector/README.md new file mode 100644 index 0000000..07e2314 --- /dev/null +++ b/projector/README.md @@ -0,0 +1,22 @@ +# Bank Account Projector +This projector is responsible for storing read-optimized view data for bank accounts as a function application over the stream of inbound bank account events. + +This projector maintains the following projections: +* **balances** - The current balance of any account can be looked up immediately by querying the key `balance.{account_number}` +* **ledger** - The ledger (chronological transaction history) of any account can be received as a JSON string via the key `ledger.{account_number}` + +⚠️ NOTE: for testing purposes please don't use non-alphanumeric characters for the fake account numbers as it could potentially mess up key value storage depending on the chosen provider's support for complex keys. + +# Configuration +This actor needs to be linked (bound) to two capability providers. One must support the `cosmonic:eventsourcing` contract. The Concordance provider for this contract requires the following configuration: + +* `ROLE` - `projector` +* `INTEREST` - `account_created,funds_deposited,funds_withdrawn,wire_funds_reserved,wire_funds_released` +* `NAME` - `bankaccount_projector` + +Note that stateless event handlers (whether you're using them as projectors, notifiers, gateways, etc) must declare their interest in events _explicitly_ in a comma-delimited list. Because of the use of commas in this data, it's probably easier and more reliable to use `wash ctl put link` rather than using the graphical wasmCloud dashboard. + +This actor will also need to be linked to a `wasmcloud:keyvalue` capability provider, the implementation of which is entirely up to the developer and the configuration is likely specific to the implementation chosen (e.g. Redis vs NATS, etc). + +# Manual Testing +You can start a wasmCloud host, start all of the bank account actors, and then start both the Concordance provider and your key-value provider of choice. Set the link definitions accordingly and then run the `scenario_1.sh` script in the [scripts](../scripts/) directory. You should then see the aggregate state stored in the `CC_STATE` bucket, the resulting events in the `CC_EVENTS` stream, and, assuming you used Redis, you'll see a balance projection in `balance.ABC123` and the ledger JSON structure in `ledger.ABC123`. diff --git a/projector/src/lib.rs b/projector/src/lib.rs new file mode 100644 index 0000000..b73d3ca --- /dev/null +++ b/projector/src/lib.rs @@ -0,0 +1,17 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +concordance_gen::generate!({ + path: "../eventcatalog", + role: "projector", + entity: "bank account" +}); + +mod store; + +#[async_trait] +impl BankAccountProjector for BankAccountProjectorImpl { + async fn handle_account_created(&self, input: AccountCreated) -> Result<()> { + store::initialize_account(input).await + } +} diff --git a/projector/src/store.rs b/projector/src/store.rs new file mode 100644 index 0000000..a43700b --- /dev/null +++ b/projector/src/store.rs @@ -0,0 +1,90 @@ +use std::collections::HashMap; + +use crate::*; + +use serde::{Deserialize, Serialize}; +use wasmbus_rpc::actor::prelude::*; +use wasmcloud_interface_keyvalue::{KeyValue, KeyValueSender, SetRequest}; +use wasmcloud_interface_logging::{debug, error}; + +// Note an invariant: the last() element in a ledger's effective_balance field is +// always the same as the balance stored in the balance.{account} key. + +/// Creates a new AccountLedger instance with an initial transaction as a deposit, +/// sets the current balance to the initial amount +pub async fn initialize_account(event: AccountCreated) -> Result<()> { + debug!("Initializing account {}", event.account_number); + let kv = KeyValueSender::new(); + + let account_number = event.account_number.to_string(); + let ctx = Context::default(); + + let initial_balance = event.initial_balance.unwrap_or_default() as u32; + + // Set up the initial ledger + let ledger_key = format!("ledger.{account_number}"); + let ledger = AccountLedger::new(event.account_number, initial_balance); + let ledger_json = serde_json::to_string(&ledger).unwrap(); // we know this won't fail + + // set the current balance + let balance_key = format!("balance.{account_number}"); + + set(&ctx, &kv, ledger_key, ledger_json).await; + set(&ctx, &kv, balance_key, initial_balance.to_string()).await; + + Ok(()) +} + +async fn set(ctx: &Context, kv: &KeyValueSender, key: String, value: String) { + if let Err(e) = kv + .set( + ctx, + &SetRequest { + key: key.clone(), + value, + expires: 0, + }, + ) + .await + { + error!("Failed to set {key} in store: {e}"); + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct AccountLedger { + pub account_number: String, + pub ledger_lines: Vec, + pub holds: HashMap, +} + +impl AccountLedger { + fn new(account_number: String, initial_balance: u32) -> AccountLedger { + AccountLedger { + account_number, + holds: HashMap::new(), + ledger_lines: vec![LedgerLine { + amount: initial_balance, + tx_type: TransactionType::Deposit, + effective_balance: initial_balance, + }], + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct LedgerLine { + pub amount: u32, + pub tx_type: TransactionType, + pub effective_balance: u32, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +enum TransactionType { + Withdrawal, + Deposit, + Transfer, + FundsReserve, + FundsRelease, + Unknown, +} diff --git a/projector/wasmcloud.toml b/projector/wasmcloud.toml new file mode 100644 index 0000000..48b8c46 --- /dev/null +++ b/projector/wasmcloud.toml @@ -0,0 +1,7 @@ +name = "BankAccountProjector" +language = "rust" +type = "actor" + +[actor] +key_directory = "./.keys" +claims = ["cosmonic:eventsourcing", "wasmcloud:keyvalue", "wasmcloud:builtin:logging"] diff --git a/wadm.yaml b/wadm.yaml index 3897375..0c19113 100644 --- a/wadm.yaml +++ b/wadm.yaml @@ -3,23 +3,96 @@ kind: Application metadata: name: bank-account annotations: - version: v0.0.0 + version: v0.1.0 description: "The concordance bank account example" spec: components: - name: catalog type: actor properties: - image: ghcr.io/cosmonic/cosmonic-gitops/bankaccount_catalog:0.0.0 + image: ghcr.io/cosmonic/cosmonic-gitops/bankaccount_catalog:0.1.0 traits: - - type: spreadscaler + - type: daemonscaler properties: replicas: 3 - type: linkdef properties: target: httpserver + - name: projector + type: actor + properties: + image: ghcr.io/cosmonic/cosmonic-gitops/bankaccount_projector:0.1.0 + traits: + - type: daemonscaler + properties: + replicas: 3 + - type: linkdef + properties: + target: concordance + values: + NAME: bankaccount_projector + ROLE: projector + INTEREST: account_created + - type: linkdef + properties: + target: keyvalue + + - name: aggregate + type: actor + properties: + image: ghcr.io/cosmonic/cosmonic-gitops/bankaccount_aggregate:0.1.0 + traits: + - type: daemonscaler + properties: + replicas: 3 + - type: linkdef + properties: + target: concordance + values: + ROLE: aggregate + INTEREST: bankaccount + NAME: bankaccount + KEY: accountNumber + + - name: concordance + type: capability + properties: + config: + js_domain: cosmonic + nats_url: 0.0.0.0:4222 + contract: cosmonic:eventsourcing + image: registry.hub.docker.com/cosmonic/concordance:0.1.0 + traits: + - properties: + replicas: 1 + spread: + - name: oncosmonic + requirements: + cosmonic_managed: "true" + type: spreadscaler + - name: keyvalue + type: capability + properties: + image: cosmonic.azurecr.io/builtin_keyvalue:0.2.5 + contract: wasmcloud:keyvalue + traits: + - properties: + replicas: 1 + spread: + - name: oncosmonic + requirements: + cosmonic_managed: "true" + type: spreadscaler - name: httpserver type: capability properties: image: cosmonic.azurecr.io/httpserver_wormhole:0.6.2 contract: wasmcloud:httpserver + traits: + - properties: + replicas: 1 + spread: + - name: oncosmonic + requirements: + cosmonic_managed: "true" + type: spreadscaler