diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index eb9d3199..6403968f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -29,7 +29,7 @@ "extensions": [ "GitHub.vscode-pull-request-github", "mutantdino.resourcemonitor", - "rangav.vscode-thunder-client@2.6.2", + "rangav.vscode-thunder-client@2.5.3", "hashicorp.terraform", "golang.go", "svelte.svelte-vscode", diff --git a/.gitpod.yml b/.gitpod.yml index 1c2ce2af..e00faee0 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -59,7 +59,7 @@ github: addBadge: false vscode: extensions: - - rangav.vscode-thunder-client@2.6.2 + - rangav.vscode-thunder-client@2.5.3 - hashicorp.terraform - golang.go - svelte.svelte-vscode diff --git a/README.md b/README.md index 96dcb055..cb27f14d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,18 @@ systemaccounting optimizes the flow of capital by expediting the discovery of ec **a.** no, and please use the word *replication* **q.** i dont find any words in here used by the media. what is this? -**a.** encryption solves access risk. replication solves single point of failure and inconsistency risk. neither of these solutions are relevant to modeling currency as an electric current. this payment application solves contemporary economic issues by replacing "monetary" policy with a natural physical law. first, currency is modeled as a lightweight, dual positive-negative structured time-series between creditors and debitors respectively. encryption and replication are secondary +**a.** encryption solves access risk. replication solves single point of failure and inconsistency risk. neither of these solutions are relevant to modeling currency as an electric current. this payment application solves contemporary economic issues by replacing "monetary" policy with a natural physical law. first, currency is modeled as a lightweight, dual positive-negative structured time-series between creditors and debitors respectively. encryption and replication are secondary: +```json +{ + "item": "bottled water", + "price": "1.000", + "quantity": "1", + "creditor": "GroceryStore", // positive value (+) + "debitor": "JacobWebb", // negative value (-) + "creditor_approval_time": "2023-03-20T04:58:27.771Z", + "debitor_approval_time": "2023-03-20T05:24:13.465Z" +} +``` **q.** where will i bank? **a.** you dont need a bank. you need accounting. if you still wish to lend your money after receiving the service of accounting, please judge the risk of the loan you intend to offer the recipient by first exploiting your access to their accounting, then assume no one except you will own that risk after you consume it @@ -38,10 +49,10 @@ systemaccounting optimizes the flow of capital by expediting the discovery of ec **a.** you dont need to publish your account activity. publishing account data is a feature primarily intended for 1) businesses owners who wish to signal the demand for capital with an empirical rate of return and 2) government officials who wish to keep citizens informed of the performance of fiscal policies with empirical data **q.** do you have any demos? -**a.** watch the *economic policy as code* video series +**a.** watch the [economic policy as code](https://mxfactorial.video/) video series **q.** how to explain this project to non engineers? -**a.** share the *economic policy as code* video series +**a.** share the [economic policy as code](https://mxfactorial.video/) video series **q.** why is the code public if the license is private? **a.** publicly used code is a public structure @@ -56,19 +67,101 @@ systemaccounting optimizes the flow of capital by expediting the discovery of ec 1. creating, increasing and decreasing account balances from user transfers 1. changing account balances between transacting users 1. realtime reporting -1. a new `GroceryStore` systemaccount is created when the owner transfers, for example, `1,000` from their "Bank of America" account to the united states treasury account -1. a new `JacobWebb` systemaccount is created when the owner transfers `1,000` from their "Chase" account to the united states treasury account -1. `JacobWebb` visits the `GroceryStore` and brings a single `bottled water` priced at `1.000` to the cashier (3 digit decimals used) -1. the `GroceryStore` cashier creates a list of `transaction_items` to be transacted, but first sends it to the `rules` service (see [request & response](https://github.com/systemaccounting/mxfactorial/tree/develop/services/rules#request)) to check for any rules that apply to the proposed transaction (taxes, automated approvals, etc) -1. the `GroceryStore` cashier then sends the rule-applied transaction request to the `request-create` service (see [request & response](https://github.com/systemaccounting/mxfactorial/tree/develop/services/request-create#request)) -1. the `JacobWebb` customer receives a notification and sends their approval to the `request-approve` service (see [request & response](https://github.com/systemaccounting/mxfactorial/tree/develop/services/request-approve#request)) +1. new example accounts: + 1. a `GroceryStore` systemaccount is created when the owner transfers, for example, `1,000` from their "Bank of America" account to the united states treasury account + 1. a `JacobWebb` systemaccount is created when the owner transfers `1,000` from their "Chase" account to the united states treasury account +1. `JacobWebb` visits the `GroceryStore` and brings a single `bottled water` priced at `1.000` (3 digit decimals used) to the cashier +1. the `GroceryStore` cashier authors a single entry list of `transaction_items` to be transacted. the `GroceryStore` account is set as the **creditor** (+) and the `JacobWebb` account is set as as **debitor** (-): + ```json + [ + { // authored by GroceryStore cashier + "item": "bottled water", + "price": "1.000", + "quantity": "1", + "creditor": "GroceryStore", + "debitor": "JacobWebb", + "creditor_approval_time": null, + "debitor_approval_time": null + } + ] + ``` +1. the `GroceryStore` cashier first sends the `transaction_items` list to the `rule` service (see [detailed request & response](https://github.com/systemaccounting/mxfactorial/tree/develop/services/rule#request)) to check for any transaction automation rules that apply to the proposed transaction (taxes, approvals, etc) and receives a response with a creditor-approved state sales tax added to the `transaction_items` list: + ```json + [ + { + "item": "bottled water", + "price": "1.000", + "quantity": "1", + "creditor": "GroceryStore", + "debitor": "JacobWebb", + "creditor_approval_time": null, + "debitor_approval_time": null + }, + { // transaction_item added by rule service + "item": "9% state sales tax", + "price": "0.090", + "quantity": "1", + "creditor": "StateOfCalifornia", + "debitor": "JacobWebb", + "creditor_approval_time": "2023-03-20T03:01:55.812Z", // approval added by rule service + "debitor_approval_time": null + } + ] + ``` +1. the `GroceryStore` cashier then sends the rule-applied transaction request to the `request-create` service (see [detailed request & response](https://github.com/systemaccounting/mxfactorial/tree/develop/services/request-create#request)) to 1) create a transaction request and 2) add an approval for the `GroceryStore` creditor: + ```json + [ // added to database by request-create service + { + "item": "bottled water", + "price": "1.000", + "quantity": "1", + "creditor": "GroceryStore", + "debitor": "JacobWebb", + "creditor_approval_time": "2023-03-20T04:58:27.771Z", // added by request-create service + "debitor_approval_time": null + }, + { + "item": "9% state sales tax", + "price": "0.090", + "quantity": "1", + "creditor": "StateOfCalifornia", + "debitor": "JacobWebb", + "creditor_approval_time": "2023-03-20T03:01:55.812Z", + "debitor_approval_time": null + } + ] + ``` +1. the `JacobWebb` customer receives a notification and sends their approval to the `request-approve` service (see [detailed request & response](https://github.com/systemaccounting/mxfactorial/tree/develop/services/request-approve#request)) + ```json + [ + { + "item": "bottled water", + "price": "1.000", + "quantity": "1", + "creditor": "GroceryStore", + "debitor": "JacobWebb", + "creditor_approval_time": "2023-03-20T04:58:27.771Z", + "debitor_approval_time": "2023-03-20T05:24:13.465Z" // added by request-approve service + }, + { + "item": "9% state sales tax", + "price": "0.090", + "quantity": "1", + "creditor": "StateOfCalifornia", + "debitor": "JacobWebb", + "creditor_approval_time": "2023-03-20T03:01:55.812Z", + "debitor_approval_time": "2023-03-20T05:24:13.465Z" // added by request-approve service + } + ] + ``` 1. the single `1.000 bottled water + 0.090 sales tax = 1.090 total` transaction simultaneously: 1. decreases the `JacobWebb` account by `1.090` 1. increases the `GroceryStore` account by `1.000` 1. increases the `StateOfCalifornia` account by `0.090` -1. all accounts **never** default from systemic risk or experience "monetary" inflation +1. all accounts **never** default from systemic risk or lose value from "monetary" inflation 1. the public has 24 hour access to realtime revenue and expense reporting from the `StateOfCalifornia` account 1. the `GroceryStore` owner may publish account performance anytime to [signal](https://en.wikipedia.org/wiki/Signalling_(economics)) the demand for capital to investors with an **empirical** rate of return, i.e. NOT *pro forma* +1. capital in the financial market is now priced empirically and protected from manipulation by governments and committees ### general use cases public demonstration of the following use cases through a systemaccounting function: diff --git a/docker/dev/bitnami-postgres.Dockerfile b/docker/dev/bitnami-postgres.Dockerfile index c36b777f..9b56c615 100644 --- a/docker/dev/bitnami-postgres.Dockerfile +++ b/docker/dev/bitnami-postgres.Dockerfile @@ -1,4 +1,4 @@ -FROM bitnami/postgresql:latest +FROM bitnami/postgresql:15.3.0 USER root @@ -11,7 +11,7 @@ COPY migrations /tmp/migrations RUN apt update && \ apt install curl -y && \ - curl -LO https://github.com/golang-migrate/migrate/releases/download/v4.15.2/migrate.linux-amd64.deb && \ + curl -LO https://github.com/golang-migrate/migrate/releases/download/v4.16.2/migrate.linux-amd64.deb && \ dpkg -i migrate.linux-amd64.deb && \ rm migrate.linux-amd64.deb && \ apt clean && \ diff --git a/services/rule/makefile b/services/rule/makefile index 9c31c79d..5ce17ac5 100644 --- a/services/rule/makefile +++ b/services/rule/makefile @@ -20,7 +20,7 @@ start: nohup cargo watch --env-file $(ENV_FILE) -w src -w $(RELATIVE_PROJECT_ROOT_PATH)/crates -x run >> $(NOHUP_LOG) & stop: - $(MAKE) -C $(RELATIVE_PROJECT_ROOT_PATH) stop-dev + $(MAKE) -C $(RELATIVE_PROJECT_ROOT_PATH) stop run: @$(MAKE) -C ../../migrations run diff --git a/services/rule/src/main.rs b/services/rule/src/main.rs index 7eee4565..17c8904b 100644 --- a/services/rule/src/main.rs +++ b/services/rule/src/main.rs @@ -151,10 +151,10 @@ async fn apply_approval_rules( .await; // loop through each approval rule and apply - for rule in approval_rules.0.iter() { + for rule_instance in approval_rules.0.iter() { // apply approval rule_instance(s) on behalf of each approver rules::approval::match_approval_rule( - rule, + rule_instance, tr_item, &mut approval, approval_time, @@ -168,7 +168,7 @@ async fn apply_approval_rules( } // attach post rule approvals to each transaction_item - tr_item.approvals = Some(approvals) + tr_item.approvals = Some(approvals); } } @@ -292,128 +292,196 @@ async fn shutdown_signal() { #[cfg(test)] mod tests { - #[tokio::test] - async fn it_applies_transaction_item_rules() { - use super::*; - use axum::async_trait; - use chrono::{DateTime, Utc}; - use std::error::Error; - use types::{ - account::{AccountProfile, AccountProfiles}, - account_role::{AccountRole, DEBITOR_FIRST}, - rule::{RuleInstance, RuleInstances}, - transaction_item::TransactionItem, - }; - struct Stub(); - - #[async_trait] - impl AccountStore for &Stub { - async fn get_account_profiles( - &self, - _accounts: Vec, - ) -> Result> { - Ok(AccountProfiles(vec![ - AccountProfile { - id: Some(String::from("7")), - account_name: String::from("JacobWebb"), - description: Some(String::from("Soccer coach")), - first_name: Some(String::from("Jacob")), - middle_name: Some(String::from("Curtis")), - last_name: Some(String::from("Webb")), - country_name: String::from("United States of America"), - street_number: Some(String::from("205")), - street_name: Some(String::from("N Mccarran Blvd")), - floor_number: None, - unit_number: None, - city_name: String::from("Sparks"), - county_name: Some(String::from("Washoe County")), - region_name: None, - state_name: String::from("Nevada"), - postal_code: String::from("89431"), - latlng: Some(String::from("(39.534552,-119.737825)")), - email_address: String::from("jacob@address.xz"), - telephone_country_code: Some(String::from("1")), - telephone_area_code: Some(String::from("775")), - telephone_number: Some(String::from("5555555")), - occupation_id: Some(String::from("7")), - industry_id: Some(String::from("7")), - }, - AccountProfile { - id: Some(String::from("11")), - account_name: String::from("GroceryStore"), - description: Some(String::from("Sells groceries")), - first_name: Some(String::from("Grocery")), - middle_name: None, - last_name: Some(String::from("Store")), - country_name: String::from("United States of America"), - street_number: Some(String::from("8701")), - street_name: Some(String::from("Lincoln Blvd")), - floor_number: None, - unit_number: None, - city_name: String::from("Los Angeles"), - county_name: Some(String::from("Los Angeles County")), - region_name: None, - state_name: String::from("California"), - postal_code: String::from("90045"), - latlng: Some(String::from("(33.958050,-118.418388)")), - email_address: String::from("grocerystore@address.xz"), - telephone_country_code: Some(String::from("1")), - telephone_area_code: Some(String::from("310")), - telephone_number: Some(String::from("5555555")), - occupation_id: None, - industry_id: Some(String::from("8")), - }, - AccountProfile { - id: Some(String::from("27")), - account_name: String::from("StateOfCalifornia"), - description: Some(String::from("State of California")), - first_name: None, - middle_name: None, - last_name: None, - country_name: String::from("United States of America"), - street_number: Some(String::from("450")), - street_name: Some(String::from("N St")), - floor_number: None, - unit_number: None, - city_name: String::from("Sacramento"), - county_name: Some(String::from("Sacramento County")), - region_name: None, - state_name: String::from("California"), - postal_code: String::from("95814"), - latlng: Some(String::from("(38.5777292,-121.5027026)")), - email_address: String::from("stateofcalifornia@address.xz"), - telephone_country_code: Some(String::from("1")), - telephone_area_code: Some(String::from("916")), - telephone_number: Some(String::from("5555555")), - occupation_id: None, - industry_id: Some(String::from("11")), - }, - ])) + use super::*; + use axum::async_trait; + use chrono::{DateTime, Utc}; + use std::error::Error; + use types::{ + account::{AccountProfile, AccountProfiles}, + account_role::{AccountRole, DEBITOR_FIRST}, + rule::{RuleInstance, RuleInstances}, + transaction_item::TransactionItem, + }; + struct Stub(); + const TEST_TAX_APPROVERS: &[&str] = &["BenRoss", "DanLee", "MiriamLevy"]; + + #[async_trait] + impl AccountStore for &Stub { + async fn get_account_profiles( + &self, + _accounts: Vec, + ) -> Result> { + Ok(AccountProfiles(vec![ + AccountProfile { + id: Some(String::from("7")), + account_name: String::from("JacobWebb"), + description: Some(String::from("Soccer coach")), + first_name: Some(String::from("Jacob")), + middle_name: Some(String::from("Curtis")), + last_name: Some(String::from("Webb")), + country_name: String::from("United States of America"), + street_number: Some(String::from("205")), + street_name: Some(String::from("N Mccarran Blvd")), + floor_number: None, + unit_number: None, + city_name: String::from("Sparks"), + county_name: Some(String::from("Washoe County")), + region_name: None, + state_name: String::from("Nevada"), + postal_code: String::from("89431"), + latlng: Some(String::from("(39.534552,-119.737825)")), + email_address: String::from("jacob@address.xz"), + telephone_country_code: Some(String::from("1")), + telephone_area_code: Some(String::from("775")), + telephone_number: Some(String::from("5555555")), + occupation_id: Some(String::from("7")), + industry_id: Some(String::from("7")), + }, + AccountProfile { + id: Some(String::from("11")), + account_name: String::from("GroceryStore"), + description: Some(String::from("Sells groceries")), + first_name: Some(String::from("Grocery")), + middle_name: None, + last_name: Some(String::from("Store")), + country_name: String::from("United States of America"), + street_number: Some(String::from("8701")), + street_name: Some(String::from("Lincoln Blvd")), + floor_number: None, + unit_number: None, + city_name: String::from("Los Angeles"), + county_name: Some(String::from("Los Angeles County")), + region_name: None, + state_name: String::from("California"), + postal_code: String::from("90045"), + latlng: Some(String::from("(33.958050,-118.418388)")), + email_address: String::from("grocerystore@address.xz"), + telephone_country_code: Some(String::from("1")), + telephone_area_code: Some(String::from("310")), + telephone_number: Some(String::from("5555555")), + occupation_id: None, + industry_id: Some(String::from("8")), + }, + AccountProfile { + id: Some(String::from("27")), + account_name: String::from("StateOfCalifornia"), + description: Some(String::from("State of California")), + first_name: None, + middle_name: None, + last_name: None, + country_name: String::from("United States of America"), + street_number: Some(String::from("450")), + street_name: Some(String::from("N St")), + floor_number: None, + unit_number: None, + city_name: String::from("Sacramento"), + county_name: Some(String::from("Sacramento County")), + region_name: None, + state_name: String::from("California"), + postal_code: String::from("95814"), + latlng: Some(String::from("(38.5777292,-121.5027026)")), + email_address: String::from("stateofcalifornia@address.xz"), + telephone_country_code: Some(String::from("1")), + telephone_area_code: Some(String::from("916")), + telephone_number: Some(String::from("5555555")), + occupation_id: None, + industry_id: Some(String::from("11")), + }, + ])) + } + async fn get_approvers_for_account(&self, account: String) -> Vec { + match account.as_str() { + "StateOfCalifornia" => { + let mut approvers: Vec = vec![]; + for a in TEST_TAX_APPROVERS { + approvers.push(a.to_string()) + } + approvers + } + _ => vec![account], } - async fn get_approvers_for_account(&self, _account: String) -> Vec { - vec!["".to_string()] + } + } + + #[async_trait] + impl RuleInstanceStore for &Stub { + async fn get_profile_state_rule_instances( + &self, + account_role: AccountRole, + _state_name: String, + ) -> RuleInstances { + if account_role == AccountRole::Debitor { + return RuleInstances(vec![]); } + RuleInstances(vec![RuleInstance { + id: Some(String::from("1")), + rule_type: String::from("transaction_item"), + rule_name: String::from("multiplyItemValue"), + rule_instance_name: String::from("NinePercentSalesTax"), + variable_values: vec![ + String::from("ANY"), + String::from("StateOfCalifornia"), + String::from("9% state sales tax"), + String::from("0.09"), + ], + account_role: AccountRole::Creditor, + item_id: None, + price: None, + quantity: None, + unit_of_measurement: None, + units_measured: None, + account_name: None, + first_name: None, + middle_name: None, + last_name: None, + country_name: None, + street_id: None, + street_name: None, + floor_number: None, + unit_id: None, + city_name: None, + county_name: None, + region_name: None, + state_name: Some(String::from("California")), + postal_code: None, + latlng: None, + email_address: None, + telephone_country_code: None, + telephone_area_code: None, + telephone_number: None, + occupation_id: None, + industry_id: None, + disabled_time: None, + removed_time: None, + created_at: Some(TZTime( + DateTime::parse_from_rfc3339("2023-02-28T04:21:08.363Z") + .unwrap() + .with_timezone(&Utc), + )), + }]) } - #[async_trait] - impl RuleInstanceStore for &Stub { - async fn get_profile_state_rule_instances( - &self, - account_role: AccountRole, - _state_name: String, - ) -> RuleInstances { - if account_role == AccountRole::Debitor { - return RuleInstances(vec![]); - } - RuleInstances(vec![RuleInstance { + async fn get_rule_instances_by_type_role_account( + &self, + _account_role: AccountRole, + _account: String, + ) -> RuleInstances { + RuleInstances(vec![]) + } + async fn get_approval_rule_instances( + &self, + _account_role: AccountRole, + account: String, + ) -> RuleInstances { + if TEST_TAX_APPROVERS.contains(&account.as_str()) { + return RuleInstances(vec![RuleInstance { id: Some(String::from("1")), - rule_type: String::from("transaction_item"), - rule_name: String::from("multiplyItemValue"), - rule_instance_name: String::from("NinePercentSalesTax"), + rule_type: String::from("approval"), + rule_name: String::from("approveAnyCreditItem"), + rule_instance_name: String::from("ApproveAllCaliforniaCredit"), variable_values: vec![ - String::from("ANY"), String::from("StateOfCalifornia"), - String::from("9% state sales tax"), - String::from("0.09"), + String::from("creditor"), + account, ], account_role: AccountRole::Creditor, item_id: None, @@ -433,7 +501,7 @@ mod tests { city_name: None, county_name: None, region_name: None, - state_name: Some(String::from("California")), + state_name: None, postal_code: None, latlng: None, email_address: None, @@ -449,24 +517,15 @@ mod tests { .unwrap() .with_timezone(&Utc), )), - }]) - } - async fn get_rule_instances_by_type_role_account( - &self, - _account_role: AccountRole, - _account: String, - ) -> RuleInstances { - RuleInstances(vec![]) - } - async fn get_approval_rule_instances( - &self, - _account_role: AccountRole, - _account: String, - ) -> RuleInstances { - RuleInstances(vec![]) + }]); + } else { + return RuleInstances(vec![]); } } + } + #[tokio::test] + async fn it_applies_transaction_item_rules() { let stub = Stub(); let tr_items = TransactionItems(vec![ TransactionItem { @@ -490,34 +549,7 @@ mod tests { creditor_rejection_time: None, debitor_expiration_time: None, creditor_expiration_time: None, - approvals: Some(Approvals(vec![ - Approval { - id: None, - rule_instance_id: None, - transaction_id: None, - transaction_item_id: None, - account_name: String::from("JacobWebb"), - account_role: AccountRole::Debitor, - device_id: None, - device_latlng: None, - approval_time: None, - rejection_time: None, - expiration_time: None, - }, - Approval { - id: None, - rule_instance_id: None, - transaction_id: None, - transaction_item_id: None, - account_name: String::from("GroceryStore"), - account_role: AccountRole::Creditor, - device_id: None, - device_latlng: None, - approval_time: None, - rejection_time: None, - expiration_time: None, - }, - ])), + approvals: None, }, TransactionItem { id: None, @@ -540,34 +572,7 @@ mod tests { creditor_rejection_time: None, debitor_expiration_time: None, creditor_expiration_time: None, - approvals: Some(Approvals(vec![ - Approval { - id: None, - rule_instance_id: None, - transaction_id: None, - transaction_item_id: None, - account_name: String::from("JacobWebb"), - account_role: AccountRole::Debitor, - device_id: None, - device_latlng: None, - approval_time: None, - rejection_time: None, - expiration_time: None, - }, - Approval { - id: None, - rule_instance_id: None, - transaction_id: None, - transaction_item_id: None, - account_name: String::from("GroceryStore"), - account_role: AccountRole::Creditor, - device_id: None, - device_latlng: None, - approval_time: None, - rejection_time: None, - expiration_time: None, - }, - ])), + approvals: None, }, ]); @@ -601,8 +606,154 @@ mod tests { let want_total = 19.62; assert_eq!( got_total, want_total, - "want {}, got {}", + "got {}, want {}", got_length, want_total ); } + + #[tokio::test] + async fn it_applies_approval_rules() { + let test_approval_time = TZTime::now(); + let stub = Stub(); + let mut got_tr_items = TransactionItems(vec![ + TransactionItem { + id: None, + transaction_id: None, + item_id: String::from("bread"), + price: String::from("3.000"), + quantity: String::from("2"), + debitor_first: Some(false), + rule_instance_id: None, + rule_exec_ids: Some(vec![]), + unit_of_measurement: None, + units_measured: None, + debitor: String::from("JacobWebb"), + creditor: String::from("GroceryStore"), + debitor_profile_id: None, + creditor_profile_id: None, + debitor_approval_time: None, + creditor_approval_time: None, + debitor_rejection_time: None, + creditor_rejection_time: None, + debitor_expiration_time: None, + creditor_expiration_time: None, + approvals: None, + }, + TransactionItem { + id: None, + transaction_id: None, + item_id: String::from("milk"), + price: String::from("4.000"), + quantity: String::from("3"), + debitor_first: Some(false), + rule_instance_id: None, + rule_exec_ids: Some(vec![]), + unit_of_measurement: None, + units_measured: None, + debitor: String::from("JacobWebb"), + creditor: String::from("GroceryStore"), + debitor_profile_id: None, + creditor_profile_id: None, + debitor_approval_time: None, + creditor_approval_time: None, + debitor_rejection_time: None, + creditor_rejection_time: None, + debitor_expiration_time: None, + creditor_expiration_time: None, + approvals: None, + }, + TransactionItem { + id: None, + transaction_id: None, + item_id: String::from("9% state sales tax"), + price: String::from("0.270"), + quantity: String::from("2.000"), + debitor_first: Some(false), + rule_instance_id: None, + rule_exec_ids: Some(vec![]), + unit_of_measurement: None, + units_measured: None, + debitor: String::from("JacobWebb"), + creditor: String::from("StateOfCalifornia"), + debitor_profile_id: None, + creditor_profile_id: None, + debitor_approval_time: None, + creditor_approval_time: None, + debitor_rejection_time: None, + creditor_rejection_time: None, + debitor_expiration_time: None, + creditor_expiration_time: None, + approvals: None, + }, + TransactionItem { + id: None, + transaction_id: None, + item_id: String::from("9% state sales tax"), + price: String::from("0.360"), + quantity: String::from("3.000"), + debitor_first: Some(false), + rule_instance_id: None, + rule_exec_ids: Some(vec![]), + unit_of_measurement: None, + units_measured: None, + debitor: String::from("JacobWebb"), + creditor: String::from("StateOfCalifornia"), + debitor_profile_id: None, + creditor_profile_id: None, + debitor_approval_time: None, + creditor_approval_time: None, + debitor_rejection_time: None, + creditor_rejection_time: None, + debitor_expiration_time: None, + creditor_expiration_time: None, + approvals: None, + }, + ]); + + // test function + apply_approval_rules(&stub, DEBITOR_FIRST, &mut got_tr_items, &test_approval_time).await; + + // assert #1 + // save length of tax item approvals + let got_length = got_tr_items + .0 + .clone() + .into_iter() + .nth(3) + .unwrap() + .approvals + .unwrap() + .0 + .len(); + // want length of approvals vec on tax transaction item to be 4 (started with 0) + let want_length: usize = 4; + assert_eq!( + got_length, want_length, + "got {}, want {}", + got_length, want_length + ); + + // assert #2 + // save approval time from first approval + let got_approval_time = got_tr_items + .0 + .into_iter() + .nth(3) + .unwrap() + .approvals + .unwrap() + .0 + .into_iter() + .nth(3) + .unwrap() + .approval_time + .unwrap(); + // want approval time + let want_approval_time = test_approval_time.clone(); + assert_eq!( + got_approval_time, want_approval_time, + "got {:?}, want {:?}", + got_approval_time, want_approval_time + ); + } }