Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(*)!: create bank aggregate and projector (v0.0.0 -> v0.1.0) #1

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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