Skip to content

Commit

Permalink
feat(*)!: create bank aggregate and projector
Browse files Browse the repository at this point in the history
Signed-off-by: Brooks Townsend <brooksmtownsend@gmail.com>

ensure providers run on cosmonic

Signed-off-by: Brooks Townsend <brooksmtownsend@gmail.com>
  • Loading branch information
brooksmtownsend committed Dec 20, 2023
1 parent 353f4e4 commit 1732d26
Show file tree
Hide file tree
Showing 32 changed files with 611 additions and 9 deletions.
4 changes: 3 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions aggregate/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[build]
target = "wasm32-unknown-unknown"

[net]
git-fetch-with-cli = true
16 changes: 16 additions & 0 deletions aggregate/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
1 change: 1 addition & 0 deletions aggregate/.keys/bankaccount_aggregate_module.nk
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SMANGOYSDZ4P3SG4GDUXECMO2J2GFOIV6NQJWGFOVA3GZBSXMGZT5GWFJ4
27 changes: 27 additions & 0 deletions aggregate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions aggregate/README.md
Original file line number Diff line number Diff line change
@@ -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"}
```

14 changes: 14 additions & 0 deletions aggregate/src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use crate::*;

pub(crate) fn handle_create_account(input: CreateAccount) -> Result<EventList> {
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,
},
)])
}
17 changes: 17 additions & 0 deletions aggregate/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use crate::*;

impl From<AccountCreated> 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<StateAck> {
Ok(StateAck::ok(Some(BankAccountAggregateState::from(input))))
}
38 changes: 38 additions & 0 deletions aggregate/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<BankAccountAggregateState>,
) -> anyhow::Result<EventList> {
commands::handle_create_account(input)
}

// -- Events --
fn apply_account_created(
&self,
input: AccountCreated,
_state: Option<BankAccountAggregateState>,
) -> anyhow::Result<StateAck> {
events::apply_account_created(input)
}
}

const STREAM: &str = "bankaccount";
69 changes: 69 additions & 0 deletions aggregate/src/state.rs
Original file line number Diff line number Diff line change
@@ -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<String, u32>, // 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::<u32>())
.unwrap_or(0)
}

/// Returns the total amount of funds on hold
pub fn total_reserved(&self) -> u32 {
self.reserved_funds.values().sum::<u32>()
}

/// 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
}
}
7 changes: 7 additions & 0 deletions aggregate/wasmcloud.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name = "BankAccountAggregate"
language = "rust"
type = "actor"

[actor]
key_directory = "./.keys"
claims = ["cosmonic:eventsourcing", "wasmcloud:builtin:logging"]
4 changes: 4 additions & 0 deletions eventcatalog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ npm run build
cd actor
wash build
```

## All Events

![All Bank Account Events](./all_events.png)
2 changes: 1 addition & 1 deletion eventcatalog/actor/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion eventcatalog/actor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bankaccountcatalog"
version = "0.3.0"
version = "0.2.0"
authors = ["Cosmonic Team"]
edition = "2021"

Expand Down
Binary file added eventcatalog/all_events.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions eventcatalog/events/AccountCreated/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: AccountCreated
summary: "Indicates the creation of a new bank account"
version: 0.0.1
consumers:
- 'Bank Account Aggregate'
- 'Bank Account Projector'
producers:
- 'Bank Account Aggregate'
tags:
- label: 'event'
externalLinks: []
badges: []
---
Indicates that a bank account has been created. As with all events, this is immutable truth.

<Mermaid />

## Schema
<SchemaViewer />
26 changes: 26 additions & 0 deletions eventcatalog/events/AccountCreated/schema.json
Original file line number Diff line number Diff line change
@@ -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"]
}

17 changes: 17 additions & 0 deletions eventcatalog/events/CreateAccount/index.md
Original file line number Diff line number Diff line change
@@ -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.

<Mermaid />

## Schema
<SchemaViewer />
25 changes: 25 additions & 0 deletions eventcatalog/events/CreateAccount/schema.json
Original file line number Diff line number Diff line change
@@ -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"]
}
4 changes: 2 additions & 2 deletions eventcatalog/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion eventcatalog/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eventcatalog",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "eventcatalog start",
Expand Down
Loading

0 comments on commit 1732d26

Please sign in to comment.