├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── costs-derive ├── Cargo.toml └── src │ └── lib.rs ├── examples └── create_new_user_and_company.rs └── src ├── access.rs ├── costs.rs ├── error.rs ├── lib.rs ├── models ├── account.rs ├── agreement.rs ├── commitment.rs ├── company.rs ├── currency.rs ├── event.rs ├── intent.rs ├── lib │ ├── agent.rs │ ├── basis_model.rs │ └── mod.rs ├── member.rs ├── mod.rs ├── occupation.rs ├── process.rs ├── process_spec.rs ├── resource.rs ├── resource_group.rs ├── resource_group_link.rs ├── resource_spec.rs └── user.rs ├── system ├── mod.rs ├── ubi.rs └── vote.rs ├── transactions ├── account.rs ├── agreement.rs ├── commitment.rs ├── company.rs ├── currency.rs ├── event │ ├── accounting.rs │ ├── delivery.rs │ ├── mod.rs │ ├── modification.rs │ ├── production.rs │ ├── service.rs │ ├── transfer.rs │ └── work.rs ├── intent.rs ├── member.rs ├── mod.rs ├── occupation.rs ├── process.rs ├── process_spec.rs ├── resource.rs ├── resource_spec.rs └── user.rs └── util ├── measure.rs ├── mod.rs ├── number.rs ├── test.rs └── time.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | /play 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["costs-derive"] 3 | 4 | [package] 5 | name = "basis-core" 6 | version = "0.1.0" 7 | authors = ["Andrew Danger Lyon "] 8 | edition = "2018" 9 | license-file = "LICENSE" 10 | description = "The core datastructures, algorithms, and logic for the Basis system" 11 | homepage = "https://basisproject.gitlab.io/public/" 12 | repository = "https://gitlab.com/basisproject/core" 13 | readme = "README.md" 14 | keywords = ["economics", "socialism", "communism", "democracy"] 15 | categories = ["algorithms", "data-structures"] 16 | 17 | [features] 18 | with_serde = ["serde", "serde_derive", "vf-rs/with_serde"] 19 | 20 | [dependencies] 21 | chrono = { version = "0.4", features = ["serde"] } 22 | costs-derive = { path = "./costs-derive" } 23 | derive_builder = "0.9" 24 | getset = "0.1" 25 | om2 = "0.1.9" 26 | rust_decimal = { version = "1.6", features = ["serde-float"] } 27 | rust_decimal_macros = "1.6" 28 | serde = { version = "1.0", optional = true } 29 | serde_derive = { version = "1.0", optional = true } 30 | thiserror = "1.0" 31 | url = { version = "2.1", features = ["serde"] } 32 | vf-rs = { version = "0.3.16", default-features = false, features = ["getset_getmut", "getset_setters"] } 33 | 34 | [dev-dependencies] 35 | serde_json = "1.0" 36 | uuid = { version = "0.8", features = ["v4"] } 37 | 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean release doc build run test test-panic test-st macros 2 | 3 | # non-versioned include 4 | VARS ?= vars.mk 5 | -include $(VARS) 6 | 7 | CARGO ?= $(shell which cargo) 8 | override CARGO_BUILD_ARGS += --features "$(FEATURES)" 9 | 10 | all: build 11 | 12 | build: 13 | $(CARGO) build $(CARGO_BUILD_ARGS) 14 | 15 | release: override CARGO_BUILD_ARGS += --release 16 | release: build 17 | 18 | doc: 19 | cargo doc 20 | 21 | test-release: override CARGO_BUILD_ARGS += --release 22 | test-release: 23 | $(CARGO) test $(TEST) $(CARGO_BUILD_ARGS) -- --nocapture 24 | 25 | test: 26 | $(CARGO) test $(TEST) $(CARGO_BUILD_ARGS) -- --nocapture 27 | 28 | test-panic: override FEATURES += panic-on-error 29 | test-panic: 30 | RUST_BACKTRACE=1 \ 31 | $(CARGO) test \ 32 | $(TEST) \ 33 | $(CARGO_BUILD_ARGS) -- \ 34 | --nocapture 35 | 36 | test-st: 37 | $(CARGO) test $(TEST) $(CARGO_BUILD_ARGS) -- --nocapture --test-threads 1 38 | 39 | clean: 40 | rm -rf target/ 41 | cargo clean 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Basis core 2 | 3 | This project holds the core datastructures, algorithms, and logic for the Basis 4 | system. The goal is to implement as much as Basis as possible without worrying 5 | about storage, networking, consensus, etc. This exists as a functionally-pure 6 | library that drives Basis without concerning itself about all the other silly 7 | details. 8 | 9 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - solidify access system 2 | - create a `RoleHasPermission` trait where Role (global, company) implement the 3 | `can()` function 4 | - create a `CanAccess` trait, which objects with roles (users, members) implement 5 | and just requires retuning a set of roles implementing `RoleHasPermission` 6 | - when processing EconomicEvents, moved Costs can be higher than the source Costs, 7 | resulting in "zeroing out" the source costs. 8 | - should this be allowed? 9 | - no 10 | 11 | -------------------------------------------------------------------------------- /costs-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "costs-derive" 3 | version = "0.1.0" 4 | authors = ["Andrew Danger Lyon "] 5 | edition = "2018" 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | proc-macro2 = "1.0" 12 | quote = "1.0" 13 | syn = "1.0" 14 | 15 | -------------------------------------------------------------------------------- /costs-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::{ 4 | DeriveInput, 5 | Ident, 6 | parse_macro_input, 7 | }; 8 | 9 | #[derive(Debug)] 10 | struct Field { 11 | name: Ident, 12 | hash_key: Ident, 13 | hash_val: Ident, 14 | } 15 | 16 | /// Derive our costs impl. 17 | /// 18 | /// Effectively, we collect any HashMap fields in the struct (ignoring others) 19 | /// and implement things like new_with_ or get_ as well as Add/Div 20 | /// and our other math stuff. 21 | #[proc_macro_derive(Costs)] 22 | pub fn derive_costs(input: TokenStream) -> TokenStream { 23 | let input = parse_macro_input!(input as DeriveInput); 24 | let name = &input.ident; 25 | 26 | // grab our HashMap fields from the input 27 | let fields: Vec = match &input.data { 28 | syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(syn::FieldsNamed { named: fields, .. }), .. }) => { 29 | fields.iter() 30 | .map(|field| { 31 | ( 32 | field.ident.as_ref().unwrap().clone(), 33 | match &field.ty { 34 | syn::Type::Path(syn::TypePath { path: syn::Path { segments, .. }, .. }) => { 35 | Some(segments[0].clone()) 36 | } 37 | _ => None, 38 | } 39 | ) 40 | }) 41 | .filter(|fieldspec| { 42 | match &fieldspec.1 { 43 | Some(path) => { 44 | path.ident == syn::Ident::new("HashMap", proc_macro2::Span::call_site()) 45 | } 46 | None => false, 47 | } 48 | }) 49 | .map(|(fieldname, segment)| { 50 | let segment = segment.unwrap(); 51 | let args = match segment.arguments { 52 | syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { args, .. }) => { 53 | args.iter() 54 | .map(|arg| { 55 | match arg { 56 | syn::GenericArgument::Type(syn::Type::Path(syn::TypePath { path: syn::Path { segments, .. }, .. })) => { 57 | segments[0].ident.clone() 58 | } 59 | _ => panic!("costs-derive::derive_costs() -- error parsing HashMap args"), 60 | } 61 | }) 62 | .collect::>() 63 | } 64 | _ => panic!("costs-derive::derive_costs() -- error parsing HashMap fields"), 65 | }; 66 | Field { 67 | name: fieldname, 68 | hash_key: args[0].clone(), 69 | hash_val: args[1].clone(), 70 | } 71 | }) 72 | .collect::>() 73 | } 74 | _ => panic!("costs-derive::derive_costs() -- can only derive costs on a struct"), 75 | }; 76 | 77 | let fn_get = fields.iter().map(|f| format_ident!("get_{}", f.name)).collect::>(); 78 | let fn_get_comment = fields.iter().map(|f| format!("Get a {} value out of this cost object, defaulting to zero if not found", f.name)).collect::>(); 79 | let field_name = fields.iter().map(|f| f.name.clone()).collect::>(); 80 | let field_name_mut = fields.iter().map(|f| format_ident!("{}_mut", f.name)).collect::>(); 81 | let field_hashkey = fields.iter().map(|f| f.hash_key.clone()).collect::>(); 82 | let field_hashval = fields.iter().map(|f| f.hash_val.clone()).collect::>(); 83 | 84 | let cost_impl = quote! { 85 | impl #name { 86 | #( 87 | #[doc = #fn_get_comment] 88 | pub fn #fn_get>(&self, id: T) -> #field_hashval { 89 | *self.#field_name().get(&id.into()).unwrap_or(&#field_hashval::zero()) 90 | } 91 | )* 92 | 93 | /// Test if we have an empty cost set 94 | pub fn is_zero(&self) -> bool { 95 | #( 96 | for (_, val) in self.#field_name().iter() { 97 | if val > &#field_hashval::zero() { 98 | return false; 99 | } 100 | } 101 | )* 102 | true 103 | } 104 | 105 | /// Remove all zero values from our ranks. 106 | fn dezero(&mut self) { 107 | #( 108 | let mut remove = vec![]; 109 | for (key, val) in self.#field_name().iter() { 110 | if val == &#field_hashval::zero() { 111 | remove.push(key.clone()); 112 | } 113 | } 114 | for key in remove { 115 | self.#field_name_mut().remove(&key); 116 | } 117 | )* 118 | } 119 | 120 | /// round all values to a standard decimal place 121 | fn round(&mut self) { 122 | let credits = self.credits_mut(); 123 | *credits = Costs::do_round(credits); 124 | #( 125 | for val in self.#field_name_mut().values_mut() { 126 | *val = Costs::do_round(val); 127 | } 128 | )* 129 | } 130 | 131 | /// Strip zeros from our Costs values 132 | fn strip(&mut self) { 133 | let credits = self.credits_mut(); 134 | *credits = credits.normalize(); 135 | #( 136 | for val in self.#field_name_mut().values_mut() { 137 | *val = val.normalize(); 138 | } 139 | )* 140 | } 141 | 142 | /// Determine if subtracting one set of costs from another results 143 | /// in any negative values 144 | pub fn is_sub_lt_0(costs1: &Costs, costs2: &Costs) -> bool { 145 | let costs3 = costs1.clone() - costs2.clone(); 146 | #( 147 | for (_, v) in costs3.#field_name().iter() { 148 | if *v < #field_hashval::zero() { 149 | return true; 150 | } 151 | } 152 | )* 153 | false 154 | } 155 | 156 | /// Determine if a set of costs is greater than 0. 157 | pub fn is_gt_0(&self) -> bool { 158 | let mut count = 0; 159 | #( 160 | for (_, v) in self.#field_name().iter() { 161 | if *v > #field_hashval::zero() { 162 | // count how many positive values we have 163 | count += 1; 164 | } else if *v <= #field_hashval::zero() { 165 | // return on ANY negative or 0 vals 166 | return false; 167 | } 168 | } 169 | )* 170 | // if we have fields and they're all > 0 then this will be true 171 | count > 0 172 | } 173 | 174 | /// Determine if any of our costs are below 0 175 | pub fn is_lt_0(&self) -> bool { 176 | #( 177 | for (_, v) in self.#field_name().iter() { 178 | if *v < #field_hashval::zero() { 179 | return true; 180 | } 181 | } 182 | )* 183 | false 184 | } 185 | 186 | /// Determine if dividing one set of costs by another will result in 187 | /// a divide-by-zero panic. 188 | pub fn is_div_by_0(costs1: &Costs, costs2: &Costs) -> bool { 189 | #( 190 | for (k, v) in costs1.#field_name().iter() { 191 | let div = costs2.#fn_get(k.clone()); 192 | if v == &#field_hashval::zero() { 193 | continue; 194 | } 195 | if div == #field_hashval::zero() { 196 | return true; 197 | } 198 | } 199 | )* 200 | false 201 | } 202 | } 203 | 204 | impl Add for Costs { 205 | type Output = Self; 206 | 207 | fn add(mut self, other: Self) -> Self { 208 | self.credits += other.credits().clone(); 209 | #( 210 | for k in other.#field_name().keys() { 211 | let entry = self.#field_name_mut().entry(k.clone()).or_insert(#field_hashval::zero()); 212 | *entry += other.#field_name().get(k).unwrap(); 213 | } 214 | )* 215 | self.normalize(); 216 | self 217 | } 218 | } 219 | 220 | impl Sub for Costs { 221 | type Output = Self; 222 | 223 | fn sub(mut self, other: Self) -> Self { 224 | self.credits -= other.credits().clone(); 225 | #( 226 | for k in other.#field_name().keys() { 227 | let entry = self.#field_name_mut().entry(k.clone()).or_insert(#field_hashval::zero()); 228 | *entry -= other.#field_name().get(k).unwrap(); 229 | } 230 | )* 231 | self.normalize(); 232 | self 233 | } 234 | } 235 | 236 | impl Mul for Costs { 237 | type Output = Self; 238 | 239 | fn mul(mut self, rhs: rust_decimal::Decimal) -> Self { 240 | self.credits *= rhs.clone(); 241 | #( 242 | for (_, val) in self.#field_name_mut().iter_mut() { 243 | *val *= rhs; 244 | } 245 | )* 246 | self.normalize(); 247 | self 248 | } 249 | } 250 | 251 | impl Div for Costs { 252 | type Output = Self; 253 | 254 | fn div(mut self, rhs: Decimal) -> Self::Output { 255 | if self.is_zero() { 256 | return self; 257 | } 258 | if rhs == Decimal::zero() { 259 | panic!("Costs::div() -- divide by zero"); 260 | } 261 | self.credits /= rhs.clone(); 262 | #( 263 | for (_, v) in self.#field_name_mut().iter_mut() { 264 | *v /= rhs; 265 | } 266 | )* 267 | self.normalize(); 268 | self 269 | } 270 | } 271 | }; 272 | TokenStream::from(cost_impl) 273 | } 274 | 275 | -------------------------------------------------------------------------------- /examples/create_new_user_and_company.rs: -------------------------------------------------------------------------------- 1 | use basis_core::{ 2 | error::Result, 3 | models::{ 4 | Op, 5 | 6 | account::AccountID, 7 | company::{Company, CompanyID}, 8 | member::{Member, MemberID, MemberClass, MemberWorker}, 9 | occupation::{Occupation, OccupationID}, 10 | user::{User, UserID}, 11 | }, 12 | transactions::{ 13 | company, 14 | occupation, 15 | user, 16 | }, 17 | system::vote::Vote, 18 | }; 19 | use chrono::Utc; 20 | 21 | /// Normally our system would be seeded with occupation data already, but we're 22 | /// starting with a blank slate so we need to add an occupation. 23 | fn create_voted_occupation(label: &str) -> Result { 24 | let voter = Vote::systemic(UserID::new("f8636701-2ec0-46e9-bff3-5cff3d7f97cf"), &Utc::now())?; 25 | let mods = occupation::create(voter.user(), OccupationID::new("e8677b3c-e125-4fb2-8cf1-04bcdae162b7"), label.into(), "Adding our first occupation", true, &Utc::now())?.into_vec(); 26 | mods[0].clone().expect_op::(Op::Create) 27 | } 28 | 29 | fn example() -> Result<(User, Member, Company)> { 30 | // run the user creation transaction and grab our user from the modification 31 | // list. 32 | // 33 | // transactions don't pass back models, but rather modifications on models 34 | // (aka, "add User" or "Update company" or "delete Member" etc). 35 | let mods = user::create(UserID::new("389f9613-1ac6-435d-9d73-e96118e0ea71"), "user-8171287127nnx78.233b2c@basisproject.net", "Jerry", AccountID::new("9b7c9b40-b759-4a15-8615-4012be92f06a"), true, &Utc::now())?.into_vec(); 36 | let user = mods[0].clone().expect_op::(Op::Create)?; 37 | 38 | // create our first occupation (by democratic vote) 39 | let occupation = create_voted_occupation("President")?; 40 | 41 | // now create our company, which also creates a member record that links the 42 | // calling user to the company as a worker 43 | let founder = company::Founder::new(MemberID::new("7100be67-2ac1-4b83-b7af-6fec9294c4ee"), MemberClass::Worker(MemberWorker::new(occupation.id().clone(), None)), true); 44 | let mods = company::create(&user, CompanyID::new("89e1aff5-84c7-4e84-b3d6-f7f5924d0e53"), "Widget Extravaganza", "info@widgetextravaganza.com", true, founder, &Utc::now())?.into_vec(); 45 | let company = mods[0].clone().expect_op::(Op::Create)?; 46 | let member = mods[1].clone().expect_op::(Op::Create)?; 47 | Ok((user, member, company)) 48 | } 49 | 50 | fn main() { 51 | let (user, member, company) = example().unwrap(); 52 | println!("Hi, {}, founder of {} (member {}), I'm Dad!", user.name(), company.inner().name(), member.id().as_str()); 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/access.rs: -------------------------------------------------------------------------------- 1 | //! The access module defines the various top-level permissions within the 2 | //! system and the roles that contain those permissions. 3 | //! 4 | //! Roles can have multiple Permission objects. Permissions are additive, 5 | //! meaning everyone starts with *no* permissions (returning 6 | //! [Error::InsufficientPrivileges][err_priv]) and permissions are added 7 | //! (allowed) from there. 8 | //! 9 | //! Generally, the access system just applies to [Users]. 10 | //! 11 | //! [err_priv]: ../error/enum.Error.html#variant.InsufficientPrivileges 12 | //! [Users]: ../models/user/struct.User.html 13 | 14 | use crate::{ 15 | error::{Error, Result}, 16 | }; 17 | #[cfg(feature = "with_serde")] 18 | use serde::{Serialize, Deserialize}; 19 | 20 | /// Define the system-wide permissions. 21 | /// 22 | /// Note there may be per-model permissions that are handled separately. 23 | #[derive(Clone, Debug, PartialEq)] 24 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 25 | pub enum Permission { 26 | All, 27 | AllBut(Vec), 28 | 29 | AccountCreate, 30 | AccountDelete, 31 | AccountSetOwners, 32 | AccountTransfer, 33 | AccountUBIClaim, 34 | AccountUpdate, 35 | 36 | CompanyCreate, 37 | CompanyDelete, 38 | CompanyPayroll, 39 | CompanyUpdate, 40 | CompanyUpdateAgreements, 41 | CompanyUpdateCommitments, 42 | CompanyUpdateIntents, 43 | CompanyUpdateMembers, 44 | CompanyUpdateResources, 45 | CompanyUpdateResourceSpecs, 46 | CompanyUpdateProcesses, 47 | CompanyUpdateProcessSpecs, 48 | 49 | CurrencyCreate, 50 | CurrencyDelete, 51 | CurrencyUpdate, 52 | 53 | EventCreate, 54 | EventUpdate, 55 | 56 | UserAdminCreate, 57 | UserAdminUpdate, 58 | UserCreate, 59 | UserDelete, 60 | UserSetRoles, 61 | UserUpdate, 62 | 63 | ResourceSpecCreate, 64 | ResourceSpecDelete, 65 | ResourceSpecUpdate, 66 | 67 | OccupationCreate, 68 | OccupationDelete, 69 | OccupationUpdate, 70 | } 71 | 72 | /// Define the system-wide roles users can have. 73 | #[derive(Clone, Debug, PartialEq)] 74 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 75 | pub enum Role { 76 | SuperAdmin, 77 | IdentityAdmin, 78 | Bank, 79 | User, 80 | Guest, 81 | } 82 | 83 | impl Role { 84 | /// For a given role, return the permissions that role has access to. 85 | pub fn permissions(&self) -> Vec { 86 | match *self { 87 | Role::SuperAdmin => { 88 | vec![Permission::All] 89 | }, 90 | Role::IdentityAdmin => { 91 | vec![ 92 | Permission::UserUpdate, 93 | Permission::UserSetRoles, 94 | Permission::UserAdminCreate, 95 | Permission::UserAdminUpdate, 96 | Permission::UserDelete, 97 | ] 98 | }, 99 | Role::Bank => { 100 | vec![ 101 | Permission::CurrencyCreate, 102 | Permission::CurrencyUpdate, 103 | Permission::CurrencyDelete, 104 | ] 105 | }, 106 | Role::User => { 107 | vec![ 108 | Permission::UserUpdate, 109 | Permission::UserDelete, 110 | Permission::CompanyCreate, 111 | Permission::CompanyDelete, 112 | Permission::CompanyPayroll, // hey, milton. what's happening. 113 | Permission::CompanyUpdate, 114 | Permission::CompanyUpdateAgreements, 115 | Permission::CompanyUpdateCommitments, 116 | Permission::CompanyUpdateIntents, 117 | Permission::CompanyUpdateMembers, 118 | Permission::CompanyUpdateResourceSpecs, 119 | Permission::CompanyUpdateResources, 120 | Permission::CompanyUpdateProcessSpecs, 121 | Permission::CompanyUpdateProcesses, 122 | Permission::ResourceSpecCreate, 123 | Permission::ResourceSpecUpdate, 124 | Permission::ResourceSpecDelete, 125 | Permission::AccountCreate, 126 | Permission::AccountUBIClaim, 127 | Permission::AccountUpdate, 128 | Permission::AccountSetOwners, 129 | Permission::AccountTransfer, 130 | Permission::AccountDelete, 131 | Permission::EventCreate, 132 | Permission::EventUpdate, 133 | ] 134 | } 135 | Role::Guest => { 136 | vec![ 137 | Permission::UserCreate, 138 | ] 139 | } 140 | } 141 | } 142 | 143 | /// Determine if a role has a specific permission. 144 | pub fn can(&self, perm: &Permission) -> bool { 145 | for p in &self.permissions() { 146 | match p { 147 | Permission::All => { 148 | return true; 149 | } 150 | Permission::AllBut(x) => { 151 | if x.contains(perm) { 152 | return false; 153 | } 154 | return true; 155 | } 156 | _ => { 157 | if p == perm { 158 | return true 159 | } 160 | } 161 | } 162 | } 163 | false 164 | } 165 | } 166 | 167 | /// Check if a guest can perform an action. 168 | pub fn guest_check(perm: Permission) -> Result<()> { 169 | if (Role::Guest).can(&perm) { 170 | Ok(()) 171 | } else { 172 | Err(Error::InsufficientPrivileges) 173 | } 174 | } 175 | 176 | #[cfg(test)] 177 | pub mod tests { 178 | use super::*; 179 | 180 | #[test] 181 | fn permissions_work() { 182 | let super_admin = Role::SuperAdmin; 183 | assert!(super_admin.can(&Permission::All)); 184 | assert!(super_admin.can(&Permission::UserCreate)); 185 | assert!(super_admin.can(&Permission::UserUpdate)); 186 | assert!(super_admin.can(&Permission::UserAdminUpdate)); 187 | assert!(super_admin.can(&Permission::UserDelete)); 188 | assert!(super_admin.can(&Permission::CompanyCreate)); 189 | } 190 | } 191 | 192 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! The main error enum for the project lives here, and documents the various 2 | //! conditions that can arise while interacting with the system. 3 | 4 | use crate::{ 5 | models::{ 6 | event::EventError, 7 | }, 8 | }; 9 | use rust_decimal::Decimal; 10 | #[cfg(feature = "with_serde")] 11 | use serde::{Serialize, Deserialize}; 12 | use thiserror::Error; 13 | 14 | /// This is our error enum. It contains an entry for any part of the system in 15 | /// which an expectation is not met or a problem occurs. 16 | #[derive(Error, Debug, PartialEq)] 17 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 18 | pub enum Error { 19 | /// There was an error while using a builder (likely an internal error) 20 | #[error("error building object {0}")] 21 | BuilderFailed(String), 22 | /// When we try to perform an operation that would erase costs (such as 23 | /// trying to delete a Process that has non-zero costs). 24 | #[error("cannot erase costs")] 25 | CannotEraseCosts, 26 | /// When trying to erase credits from the system. Generally this happens if 27 | /// you try to delete an account that has a non-zero balance. 28 | #[error("cannot erase credits")] 29 | CannotEraseCredits, 30 | /// When you try to do something that requires a commitment but the given 31 | /// commitment doesn't match the action being performed. 32 | #[error("commitment is invalid")] 33 | CommitmentInvalid, 34 | /// An error while processing an event. 35 | #[error("event error {0:?}")] 36 | Event(#[from] EventError), 37 | /// You don't have permission to perform this action 38 | #[error("insufficient privileges")] 39 | InsufficientPrivileges, 40 | /// The given ratio is not a value between 0 and 1 (inclusive) 41 | #[error("invalid ratio {0} (must be 0 <= R <= 1")] 42 | InvalidRatio(Decimal), 43 | /// Happens when an entity tries to take on more costs than is allowed. 44 | #[error("maximum costs reached")] 45 | MaxCostsReached, 46 | /// Happens when trying to operate on two `Measure` objects with different 47 | /// units, such as adding 12 Hours to 16 Kilograms 48 | #[error("operation on measurement with mismatched units")] 49 | MeasureUnitsMismatched, 50 | /// The given `Member` must be a `MemberWorker` class 51 | #[error("the member given must be a worker (not company, user, etc)")] 52 | MemberMustBeWorker, 53 | /// We're missing required fields in a call 54 | #[error("fields missing {0:?}")] 55 | MissingFields(Vec), 56 | /// An account cannot have a negative balance 57 | #[error("operation creates negative account balance")] 58 | NegativeAccountBalance, 59 | /// Negative costs cannot be created, as they would represent a surplus 60 | /// (aka profit). Frowned upon here! 61 | #[error("operation creates negative costs")] 62 | NegativeCosts, 63 | /// Negative measurements cannot be created, as you cannot realistically 64 | /// have -3 widgets. 65 | #[error("operation creates negative measurement")] 66 | NegativeMeasurement, 67 | /// Represents an error that occurs when dealing with a NumericUnion (such 68 | /// as a conversion error when adding two that have different types). 69 | #[error("error operating on NumericUnion: {0}")] 70 | NumericUnionOpError(String), 71 | /// When we try to update an object that has been deleted (or an object 72 | /// attached to the deleted object). 73 | #[error("object {0} is deleted")] 74 | ObjectIsDeleted(String), 75 | /// When we try to update an object that is inactive (or an object attached 76 | /// to the inactive object). 77 | #[error("object {0} is inactive")] 78 | ObjectIsInactive(String), 79 | /// When we try to modify an object that is now in a read-only state. 80 | #[error("object {0} is read-only")] 81 | ObjectIsReadOnly(String), 82 | /// When we ask a `Modification` for a model but the `Op` we give it doesn't 83 | /// match expectation. 84 | #[error("Op does not match expectation")] 85 | OpMismatch, 86 | /// We get this when trying to pull a measure out of a resource and come up 87 | /// blank, for instance when using `consume` on a resource that hasn't had 88 | /// its quantities initialized via `produce`/`raise`/`transfer`/etc. 89 | #[error("a resource measurement (account/onhand quantity) is missing")] 90 | ResourceMeasureMissing, 91 | /// We're trying to perform an action on a UBI account that isn't allowed. 92 | #[error("operation cannot be performed on a UBI account")] 93 | UBIAccountError, 94 | /// A UBI account is required for the action you wish to perform. 95 | #[error("operation can only be performed on a UBI account")] 96 | UBIAccountRequired, 97 | /// When we try to convert an AgentID to another ID type but it fails (like 98 | /// `let company_id: CompanyID = AgentID::UserID(user_id).try_from()?;`). 99 | #[error("AgentID is the wrong type")] 100 | WrongAgentIDType, 101 | /// Tried to convert `Model` to an inner model type but failed (for instance 102 | /// `let company: Company = Model::User(user).try_into()?;`) 103 | #[error("error converting Model to its inner form")] 104 | WrongModelType, 105 | } 106 | 107 | /// Wraps `std::result::Result` around our `Error` enum 108 | pub type Result = std::result::Result; 109 | 110 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Welcome to the Basis Core. 2 | //! 3 | //! This library provides a functional interface for interacting with a graph of 4 | //! economic nodes engaging in a socialist mode of production. What this means 5 | //! is that we start from the concepts that 6 | //! 7 | //! 1. People should be free to determine and fulfill their own needs (bottom-up 8 | //! organization) 9 | //! 1. Companies within this network operate without profit 10 | //! 1. Productive instruments and property are shared and managed by members 11 | //! 12 | //! Effectively, this is a codebase designed to support [the free association 13 | //! of producers][freeassoc], a system of production sought after by Marxists 14 | //! and Anarchists in which people are free to engage in production without 15 | //! coercion. 16 | //! 17 | //! While this ideal is a long ways away, it is nonetheless worth striving for. 18 | //! We also recognize that there will be inevitable transitional periods between 19 | //! our current capitalist system and better arrangements, so this library also 20 | //! contains methods for interacting with capitalist markets in a way that does 21 | //! not require compromising the ideals of the member companies. For more 22 | //! information on the Basis project, see [the project website][basis]. 23 | //! 24 | //! This library does not deal with storage or other external mediums in any way 25 | //! and is fully self-contained. All data being operated on needs to be passed 26 | //! in, and the results of the computations are returned and must be stored in a 27 | //! place of your choosing. This allows Basis to transcend any particular 28 | //! storage medium and exist as a self-contained kernel that can be implemented 29 | //! anywhere its data model is supported. 30 | //! 31 | //! To get started, you will want to look at the [transactions]. Transactions 32 | //! are the main interface for interacting with Basis. 33 | //! 34 | //! [freeassoc]: https://en.wikipedia.org/wiki/Free_association_(Marxism_and_anarchism) 35 | //! [basis]: https://basisproject.net/ 36 | //! [transactions]: transactions/ 37 | 38 | pub mod error; 39 | #[macro_use] 40 | pub mod util; 41 | #[macro_use] 42 | pub mod access; 43 | #[macro_use] 44 | pub mod models; 45 | pub mod costs; 46 | pub mod transactions; 47 | pub mod system; 48 | 49 | -------------------------------------------------------------------------------- /src/models/account.rs: -------------------------------------------------------------------------------- 1 | //! Accounts are a place to hold credits earned through labor. Think of them 2 | //! like a bank account or crypto wallet. 3 | 4 | use chrono::{DateTime, Utc}; 5 | use crate::{ 6 | error::{Error, Result}, 7 | models:: { 8 | user::UserID, 9 | } 10 | }; 11 | use getset::{Getters, Setters}; 12 | use rust_decimal::prelude::*; 13 | #[cfg(feature = "with_serde")] 14 | use serde::{Serialize, Deserialize}; 15 | 16 | /// Describes a multi-signature relationship to an account, allowing the owners 17 | /// of the account to decide how they may manage funds as a group. For instance, 18 | /// a transaction might need 2-of-3 owners' signatures to be validated. 19 | /// 20 | /// This can be used to model things like beneficiaries or set up joint accounts 21 | /// for families. 22 | #[derive(Clone, Debug, PartialEq, Getters, Setters)] 23 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 24 | #[getset(get = "pub", set = "pub(crate)")] 25 | pub struct Multisig { 26 | /// Requires at least N signatures to complete transactions 27 | signatures_required: u64, 28 | } 29 | 30 | impl Multisig { 31 | /// Create a new multisig obj 32 | pub fn new(signatures_required: u64) -> Self { 33 | Self { 34 | signatures_required, 35 | } 36 | } 37 | } 38 | 39 | /// Holds information about a basic income account. 40 | #[derive(Clone, Debug, PartialEq, Getters, Setters)] 41 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 42 | #[getset(get = "pub", set = "pub(crate)")] 43 | pub struct Ubi { 44 | last_claim: DateTime, 45 | } 46 | 47 | impl Ubi { 48 | /// Create a new UBI spec 49 | pub fn new(now: DateTime) -> Self { 50 | Self { 51 | last_claim: now, 52 | } 53 | } 54 | } 55 | 56 | basis_model! { 57 | /// Effectively a bank account or crypto "wallet" which stores credits 58 | /// earned via labor/wages. 59 | /// 60 | /// Can be either a regular account (can transfer value freely) or a UBI 61 | /// account which has some additional restrictions. 62 | pub struct Account { 63 | id: <>, 64 | /// The user ids of the account owners 65 | user_ids: Vec, 66 | /// The multisig capabilities of this account 67 | multisig: Vec, 68 | /// The account's name for identification purposes 69 | name: String, 70 | /// A description of the account 71 | description: String, 72 | /// The account's balance 73 | balance: Decimal, 74 | /// Whether or not this is a UBI account, and if so, some information 75 | /// about the UBI 76 | ubi: Option, 77 | } 78 | AccountBuilder 79 | } 80 | 81 | impl Account { 82 | /// Adjust the account's balance. Can be positive or negative. The balance 83 | /// cannot go below zero. Returns the updated balance on success. 84 | pub(crate) fn adjust_balance>(&mut self, amount: T) -> Result<&Decimal> { 85 | let new_amount = self.balance().clone() + amount.into(); 86 | if new_amount < Decimal::zero() { 87 | Err(Error::NegativeAccountBalance)?; 88 | } 89 | self.set_balance(new_amount); 90 | Ok(self.balance()) 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::*; 97 | use crate::{ 98 | util::{self, test::*}, 99 | }; 100 | 101 | #[test] 102 | fn account_cannot_go_negative() { 103 | let now = util::time::now(); 104 | let mut account = make_account(&AccountID::create(), &UserID::create(), num!(50.0), "my account", &now); 105 | let amount = account.adjust_balance(num!(-49)).unwrap(); 106 | assert_eq!(amount, &num!(1)); 107 | assert_eq!(account.balance(), &num!(1)); 108 | let amount = account.adjust_balance(num!(-0.6)).unwrap(); 109 | assert_eq!(amount, &num!(0.4)); 110 | assert_eq!(account.balance(), &num!(0.4)); 111 | let amount = account.adjust_balance(num!(-0.4)).unwrap(); 112 | assert_eq!(amount, &num!(0)); 113 | assert_eq!(account.balance(), &num!(0)); 114 | let res = account.adjust_balance(num!(-0.1)); 115 | assert_eq!(res, Err(Error::NegativeAccountBalance)); 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /src/models/agreement.rs: -------------------------------------------------------------------------------- 1 | //! Agreements respresent a larger transaction between two agents. Think of an 2 | //! agreement like an order, and that order can be made up of multiple 3 | //! deliverables, modeled as `Commitment`s and `EconomicEvent`s. 4 | 5 | use crate::{ 6 | models::{ 7 | lib::agent::AgentID, 8 | }, 9 | }; 10 | use vf_rs::vf; 11 | 12 | basis_model! { 13 | /// An agreement between two or more parties. This model is a very thin 14 | /// wrapper around the [ValueFlows Agreement][vfagreement] object. It has no 15 | /// concept of trying to parse or contain agreement text or clauses, but 16 | /// rather acts as a simple pointer to *some agreement somewhere* that the 17 | /// affected parties have shared access to. 18 | /// 19 | /// [vfagreement]: https://valueflo.ws/introduction/exchanges.html#agreements 20 | pub struct Agreement { 21 | id: <>, 22 | /// The inner vf Agreement object 23 | inner: vf::Agreement, 24 | /// A list of the participants in the agreement. This allows quickly 25 | /// checking to see if an event or commitment is part of an agreement. 26 | /// Note that this might also allow the storage layer to have a list of 27 | /// signatures needed in order to materially change the agreement. 28 | participants: Vec, 29 | } 30 | AgreementBuilder 31 | } 32 | 33 | impl Agreement { 34 | /// Determines if the given agent is a participant in this agreement. 35 | pub fn has_participant(&self, agent_id: &AgentID) -> bool { 36 | self.participants().contains(agent_id) 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/models/commitment.rs: -------------------------------------------------------------------------------- 1 | //! A commitment represents one economic entity's *commitment* to perform some 2 | //! future action (an `Event`). 3 | //! 4 | //! - An [Intent] represents "we want something to happen" 5 | //! - A `Commitment` represents "we agree that something will happen" 6 | //! - An [Event] represents "something did happen" 7 | //! 8 | //! [Intent]: ../intent/struct.Intent.html 9 | //! [Event]: ../event/struct.Event.html 10 | 11 | use crate::{ 12 | costs::Costs, 13 | models::{ 14 | agreement::AgreementID, 15 | lib::agent::AgentID, 16 | process::ProcessID, 17 | resource::ResourceID, 18 | resource_spec::ResourceSpecID, 19 | } 20 | }; 21 | use url::Url; 22 | use vf_rs::vf; 23 | 24 | basis_model! { 25 | /// The `Commitment` model is a wrapper around the [ValueFlows commitment][vfcomm] 26 | /// object. It is effectively what an [Event] looks like *before the event 27 | /// happens*. 28 | /// 29 | /// [Event]: ../event/struct.Event.html 30 | /// [vfcomm]: https://valueflo.ws/introduction/flows.html#commitment 31 | pub struct Commitment { 32 | id: <>, 33 | /// The commitments's core VF type 34 | inner: vf::Commitment, 35 | /// The amount of costs committed to be moved. One could think of this 36 | /// somewhat like a negotiated price in the current system. 37 | move_costs: Costs, 38 | } 39 | CommitmentBuilder 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/models/company.rs: -------------------------------------------------------------------------------- 1 | //! A company is a group of one or more people working towards a common economic 2 | //! goal. Companies are the place where costs are accumulated and dispersed into 3 | //! outgoing products and services. 4 | //! 5 | //! Companies have their own set of permissions that allow [Members] to perform 6 | //! actions on the company. Note that while the [access system][access] uses 7 | //! roles to contain various permissions, companies assign permissions directly. 8 | //! This ultimately gives more control to companies to determine their own roles 9 | //! (outside the perview of this library) as needed. 10 | //! 11 | //! [Members]: ../member/struct.Member.html 12 | //! [access]: ../../access/ 13 | 14 | use crate::{ 15 | costs::Costs, 16 | error::{Error, Result}, 17 | models::{ 18 | lib::agent::{Agent, AgentID}, 19 | }, 20 | }; 21 | use rust_decimal::prelude::*; 22 | #[cfg(feature = "with_serde")] 23 | use serde::{Serialize, Deserialize}; 24 | use vf_rs::vf; 25 | 26 | /// A permission gives a Member the ability to perform certain actions 27 | /// within the context of a company they have a relationship (a set of roles) 28 | /// with. 29 | #[derive(Clone, Debug, PartialEq)] 30 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 31 | pub enum Permission { 32 | /// Can do anything 33 | All, 34 | 35 | /// Can accept a resource (for repair) 36 | Accept, 37 | 38 | /// Can create agreements (orders) 39 | AgreementCreate, 40 | /// Can finalize agreements (orders) 41 | AgreementFinalize, 42 | /// Can update agreements (orders) 43 | AgreementUpdate, 44 | 45 | /// Can cite a resource 46 | Cite, 47 | 48 | /// Can create a commitment 49 | CommitmentCreate, 50 | /// Can delete a commitment 51 | CommitmentDelete, 52 | /// Can update a commitment 53 | CommitmentUpdate, 54 | 55 | /// Can delete the company 56 | CompanyDelete, 57 | /// Can update the company's basic info 58 | CompanyUpdate, 59 | 60 | /// Can consume a resource 61 | Consume, 62 | 63 | /// Can deliver a service 64 | DeliverService, 65 | 66 | /// Can drop off (for delivery) a resource 67 | Dropoff, 68 | 69 | /// Can create a new intent 70 | IntentCreate, 71 | /// Can delete an intent 72 | IntentDelete, 73 | /// Can update an intent 74 | IntentUpdate, 75 | 76 | /// Can lower resource quantities within the company 77 | Lower, 78 | 79 | /// Can create new members (hire) 80 | MemberCreate, 81 | /// Can delete a member (fire) 82 | MemberDelete, 83 | /// Can set existing members' company permissions 84 | MemberSetPermissions, 85 | /// Can set a member's compensation (payment) 86 | MemberSetCompensation, 87 | /// Can update existing members' basic info 88 | MemberUpdate, 89 | 90 | /// Can modify a resource (for repair) 91 | Modify, 92 | 93 | /// Can move costs internally within the company 94 | MoveCosts, 95 | /// Can move resources internally within the company 96 | MoveResource, 97 | 98 | /// Can run payroll for this company 99 | Payroll, 100 | 101 | /// Can pick up (for delivery) a resource 102 | Pickup, 103 | 104 | /// Can create a process 105 | ProcessCreate, 106 | /// Can create a process 107 | ProcessDelete, 108 | /// Can create a process 109 | ProcessUpdate, 110 | 111 | /// Can create a process spec 112 | ProcessSpecCreate, 113 | /// Can create a process spec 114 | ProcessSpecDelete, 115 | /// Can create a process spec 116 | ProcessSpecUpdate, 117 | 118 | /// Can produce a resource 119 | Produce, 120 | 121 | /// Can raise resource quantities within the company 122 | Raise, 123 | 124 | /// Can create a resource 125 | ResourceCreate, 126 | /// Can delete a resource 127 | ResourceDelete, 128 | /// Can update a resource 129 | ResourceUpdate, 130 | 131 | /// Can create a resource spec 132 | ResourceSpecCreate, 133 | /// Can delete a resource spec 134 | ResourceSpecDelete, 135 | /// Can update a resource spec 136 | ResourceSpecUpdate, 137 | 138 | /// Transfer ownership/custody to another agent 139 | Transfer, 140 | /// Transfer ownership to another agent 141 | TransferAllRights, 142 | /// Transfer custody to another agent 143 | TransferCustody, 144 | 145 | /// Can use a resource in a productive process 146 | Use, 147 | 148 | /// Can record labor 149 | Work, 150 | /// Can update labor records willy-nilly 151 | WorkAdmin, 152 | } 153 | 154 | basis_model! { 155 | /// A company is a group of one or more people working together for a common 156 | /// purpose. 157 | /// 158 | /// Companies can be planned (exist by the will of the system members), 159 | /// syndicates (exist by the will of the workers of that company), or private 160 | /// (exist completely outside the system). 161 | pub struct Company { 162 | id: <>, 163 | /// The Agent object for this company, stores its name, image, location, 164 | /// etc. 165 | inner: vf::Agent, 166 | /// Primary email address 167 | email: String, 168 | /// A credit value tracking this company's maximum costs 169 | max_costs: Decimal, 170 | /// The total amount of costs this company possesses. Cannot be above 171 | /// `max_costs` when converted to a credit value. 172 | total_costs: Costs, 173 | } 174 | CompanyBuilder 175 | } 176 | 177 | impl Company { 178 | /// Add a set of costs to this company, checking to make sure we are not 179 | /// above `max_costs`. Returns the company's post-op `total_costs` value. 180 | pub(crate) fn increase_costs(&mut self, costs: Costs) -> Result<&Costs> { 181 | if costs.is_lt_0() { 182 | Err(Error::NegativeCosts)?; 183 | } 184 | let new_costs = self.total_costs().clone() + costs; 185 | let credit_value = new_costs.credits(); 186 | if credit_value > self.max_costs() { 187 | Err(Error::MaxCostsReached)?; 188 | } 189 | self.set_total_costs(new_costs); 190 | Ok(self.total_costs()) 191 | } 192 | 193 | /// Subtract a set of costs to this company. Returns the company's post-op 194 | /// `total_costs` value. 195 | /// 196 | /// Note that we don't need to check if we're over our `max_costs` value 197 | /// because we are reducing costs here. 198 | fn decrease_costs(&mut self, costs: Costs) -> Result<&Costs> { 199 | if costs.is_lt_0() { 200 | Err(Error::NegativeCosts)?; 201 | } 202 | let total = self.total_costs().clone(); 203 | if Costs::is_sub_lt_0(&total, &costs) { 204 | Err(Error::NegativeCosts)?; 205 | } 206 | self.set_total_costs(total - costs); 207 | Ok(self.total_costs()) 208 | } 209 | 210 | /// Transfer a set of costs from this company to another. The receiving 211 | /// company must not go over their `max_costs` value. 212 | pub fn transfer_costs_to(&mut self, company_to: &mut Company, costs: Costs) -> Result<&Costs> { 213 | self.decrease_costs(costs.clone())?; 214 | company_to.increase_costs(costs)?; 215 | Ok(self.total_costs()) 216 | } 217 | } 218 | 219 | impl Agent for Company { 220 | fn agent_id(&self) -> AgentID { 221 | self.id().clone().into() 222 | } 223 | } 224 | 225 | #[cfg(test)] 226 | mod tests { 227 | use super::*; 228 | use crate::{ 229 | util::{self, test::*}, 230 | }; 231 | use rust_decimal_macros::*; 232 | 233 | #[test] 234 | fn increase_costs() { 235 | let mut company = make_company(&CompanyID::create(), "jerry's delicious widgets", &util::time::now()); 236 | company.set_max_costs(dec!(1000)); 237 | let costs1 = Costs::new_with_labor("widgetmaker", 500); 238 | let total_costs = company.increase_costs(costs1.clone()).unwrap(); 239 | assert_eq!(total_costs, &costs1); 240 | 241 | let costs2 = Costs::new_with_labor("truck driver", 400); 242 | let total_costs = company.increase_costs(costs2.clone()).unwrap(); 243 | assert_eq!(total_costs, &(costs1.clone() + costs2.clone())); 244 | 245 | let costs3 = Costs::new_with_labor("CEO. THE BEST CEO. BIG HANDS", 200); 246 | let res = company.increase_costs(costs3.clone()); 247 | assert_eq!(res, Err(Error::MaxCostsReached)); 248 | 249 | let costs4 = Costs::new_with_labor("CEO. THE BEST CEO. BIG HANDS", 100); 250 | let total_costs = company.increase_costs(costs4.clone()).unwrap(); 251 | assert_eq!(total_costs, &(costs1.clone() + costs2.clone() + costs4.clone())); 252 | } 253 | 254 | #[test] 255 | fn decrease_costs() { 256 | let mut company = make_company(&CompanyID::create(), "jerry's delicious widgets", &util::time::now()); 257 | company.set_max_costs(dec!(2000)); 258 | let mut costs = Costs::new(); 259 | costs.track_labor("machinist", dec!(500)); 260 | costs.track_labor("ceo", dec!(800)); 261 | company.set_total_costs(costs.clone()); 262 | 263 | let mut costs1 = Costs::new(); 264 | costs1.track_labor("machinist", dec!(100)); 265 | costs1.track_labor("ceo", dec!(100)); 266 | let mut comp = Costs::new(); 267 | comp.track_labor("machinist", dec!(400)); 268 | comp.track_labor("ceo", dec!(700)); 269 | let total_costs = company.decrease_costs(costs1).unwrap(); 270 | assert_eq!(total_costs, &comp); 271 | 272 | let mut costs2 = Costs::new(); 273 | costs2.track_labor("machinist", dec!(350)); 274 | costs2.track_labor("ceo", dec!(600)); 275 | let mut comp = Costs::new(); 276 | comp.track_labor("machinist", dec!(50)); 277 | comp.track_labor("ceo", dec!(100)); 278 | let total_costs = company.decrease_costs(costs2).unwrap(); 279 | assert_eq!(total_costs, &comp); 280 | 281 | let mut costs3 = Costs::new(); 282 | costs3.track_labor("machinist", dec!(400)); 283 | costs3.track_labor("ceo", dec!(600)); 284 | let res = company.decrease_costs(costs3); 285 | assert_eq!(res, Err(Error::NegativeCosts)); 286 | 287 | let mut costs4 = Costs::new(); 288 | costs4.track_labor("marketing", dec!(10)); 289 | let res = company.decrease_costs(costs4); 290 | assert_eq!(res, Err(Error::NegativeCosts)); 291 | 292 | let mut costs5 = Costs::new(); 293 | costs5.track_labor("marketing", dec!(10)); 294 | costs5 = Costs::new() - costs5.clone(); 295 | let res = company.decrease_costs(costs5); 296 | assert_eq!(res, Err(Error::NegativeCosts)); 297 | } 298 | } 299 | 300 | -------------------------------------------------------------------------------- /src/models/currency.rs: -------------------------------------------------------------------------------- 1 | //! The currency module holds the Currency model, used to track various currency 2 | //! types for purposes of [banking]. 3 | //! 4 | //! Note that currencies require global systemic management. 5 | //! 6 | //! [banking]: https://basisproject.gitlab.io/public/paper#chapter-6-banking 7 | 8 | basis_model! { 9 | /// The currency model allows the banking system to track various currencies 10 | /// as they move through the system, which ultimately allows an accurate 11 | /// conversion between the internal credits and external monetary systems. 12 | pub struct Currency { 13 | id: <>, 14 | /// The name of the currency, probably some ISO value. 15 | name: String, 16 | /// How many decimal places this currency uses. 17 | decimal_places: u8, 18 | } 19 | CurrencyBuilder 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/models/intent.rs: -------------------------------------------------------------------------------- 1 | //! An intent represents one economic entity's *desire* for some future event to 2 | //! happen (an `Event`). 3 | //! 4 | //! - An `Intent` represents "we want something to happen" 5 | //! - A [Commitment] represents "we agree that something will happen" 6 | //! - An [Event] represents "something did happen" 7 | //! 8 | //! [Commitment]: ../commitment/struct.Commitment.html 9 | //! [Event]: ../event/struct.Event.html 10 | 11 | use crate::{ 12 | costs::Costs, 13 | models::{ 14 | lib::agent::AgentID, 15 | process::ProcessID, 16 | resource::ResourceID, 17 | resource_spec::ResourceSpecID, 18 | } 19 | }; 20 | use url::Url; 21 | use vf_rs::vf; 22 | 23 | basis_model! { 24 | /// The `Intent` model is a wrapper around the [ValueFlows intent][vfintent] 25 | /// object. It is effectively what an [Event] looks like *before the event 26 | /// has been commited to*. 27 | /// 28 | /// [Event]: ../event/struct.Event.html 29 | /// [vfintent]: https://valueflo.ws/introduction/flows.html#intent 30 | pub struct Intent { 31 | id: <>, 32 | /// Our inner VF intent type 33 | inner: vf::Intent, 34 | /// If this event is an input/output of a process or resource, move some 35 | /// fixed amount of costs between the two objects. 36 | move_costs: Option, 37 | } 38 | IntentBuilder 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/models/lib/agent.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::{Result, Error}, 3 | models::{ 4 | company::CompanyID, 5 | member::MemberID, 6 | lib::basis_model::Model, 7 | user::UserID, 8 | }, 9 | }; 10 | #[cfg(feature = "with_serde")] 11 | use serde::{Serialize, Deserialize}; 12 | use std::convert::TryFrom; 13 | 14 | /// A trait that holds common agent functionality, generally applied to models 15 | /// with ID types implemented in `AgentID`. 16 | pub trait Agent: Model { 17 | /// Convert the model's ID to and AgentID. 18 | fn agent_id(&self) -> AgentID; 19 | } 20 | 21 | /// VF (correctly) assumes different types of actors in the economic network 22 | /// that have "agency" so here we define the objects that have agency within the 23 | /// Basis system. This lets us use a more generic `AgentID` object that fulfills 24 | /// VF's model while still constraining ourselves to a limited set of actors. 25 | #[derive(Clone, Debug, PartialEq)] 26 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 27 | pub enum AgentID { 28 | #[cfg_attr(feature = "with_serde", serde(rename = "company"))] 29 | CompanyID(CompanyID), 30 | #[cfg_attr(feature = "with_serde", serde(rename = "member"))] 31 | MemberID(MemberID), 32 | #[cfg_attr(feature = "with_serde", serde(rename = "user"))] 33 | UserID(UserID), 34 | } 35 | 36 | /// Implements `From for AgentID` and also `TryFrom for ModelID` 37 | macro_rules! impl_agent_for_model_id { 38 | ($idty:ident) => { 39 | impl From<$idty> for AgentID { 40 | fn from(val: $idty) -> Self { 41 | AgentID::$idty(val) 42 | } 43 | } 44 | 45 | impl TryFrom for $idty { 46 | type Error = Error; 47 | 48 | fn try_from(val: AgentID) -> Result { 49 | Ok(match val { 50 | AgentID::$idty(id) => id, 51 | _ => Err(Error::WrongAgentIDType)?, 52 | }) 53 | } 54 | } 55 | }; 56 | } 57 | 58 | impl_agent_for_model_id! { CompanyID } 59 | impl_agent_for_model_id! { MemberID } 60 | impl_agent_for_model_id! { UserID } 61 | 62 | -------------------------------------------------------------------------------- /src/models/lib/basis_model.rs: -------------------------------------------------------------------------------- 1 | /// A trait that all model IDs implement. 2 | pub trait ModelID: Into + From + Clone + PartialEq + Eq + std::hash::Hash {} 3 | 4 | /// A trait that all models implement which handles common functionality 5 | pub trait Model: Clone + PartialEq { 6 | /// Checks whether or not this model has been deleted. 7 | fn is_deleted(&self) -> bool; 8 | 9 | /// Determine if this model is active. This checks both the `active` and 10 | /// `deleted` fields for the model. 11 | fn is_active(&self) -> bool; 12 | 13 | /// Set the model's deleted value 14 | fn set_deleted(&mut self, deleted: Option>); 15 | 16 | /// Set the model's active value 17 | fn set_active(&mut self, active: bool); 18 | } 19 | 20 | macro_rules! basis_model { 21 | ( 22 | $(#[$struct_meta:meta])* 23 | pub struct $model:ident { 24 | id: <<$id:ident>>, 25 | $($fields:tt)* 26 | } 27 | $builder:ident 28 | 29 | ) => { 30 | /// ID type for this model. 31 | #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd)] 32 | #[cfg_attr(feature = "with_serde", derive(serde::Serialize, serde::Deserialize))] 33 | #[cfg_attr(feature = "with_serde", serde(transparent))] 34 | pub struct $id(String); 35 | 36 | impl $id { 37 | /// Create a new id from a val 38 | pub fn new>(id: T) -> Self { 39 | Self(id.into()) 40 | } 41 | 42 | /// Create a new random id (UUIDv4) 43 | #[allow(dead_code)] 44 | #[cfg(test)] 45 | pub(crate) fn create() -> Self { 46 | Self(uuid::Uuid::new_v4().to_hyphenated().encode_lower(&mut uuid::Uuid::encode_buffer()).to_string()) 47 | } 48 | 49 | /// Convert this ID to a string 50 | pub fn to_string(self) -> String { 51 | self.into() 52 | } 53 | 54 | /// Return a string ref for this ID 55 | pub fn as_str(&self) -> &str { 56 | self.0.as_str() 57 | } 58 | } 59 | 60 | impl std::convert::From<$id> for String { 61 | fn from(id: $id) -> Self { 62 | let $id(val) = id; 63 | val 64 | } 65 | } 66 | 67 | impl std::convert::From for $id { 68 | fn from(id: String) -> Self { 69 | Self(id) 70 | } 71 | } 72 | 73 | impl std::convert::From<&str> for $id { 74 | fn from(id: &str) -> Self { 75 | Self(id.to_string()) 76 | } 77 | } 78 | 79 | impl std::cmp::Ord for $id { 80 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 81 | self.0.cmp(&other.0) 82 | } 83 | } 84 | 85 | impl crate::models::lib::basis_model::ModelID for $id {} 86 | 87 | pub(crate) mod inner { 88 | use super::*; 89 | 90 | basis_model_inner! { 91 | $(#[$struct_meta])* 92 | #[derive(Clone, Debug, PartialEq, getset::Getters, getset::MutGetters, getset::Setters, derive_builder::Builder)] 93 | #[cfg_attr(feature = "with_serde", derive(serde::Serialize, serde::Deserialize))] 94 | #[builder(pattern = "owned", setter(into))] 95 | #[getset(get = "pub", get_mut = "pub(crate)", set = "pub(crate)")] 96 | pub struct $model { 97 | /// The model's ID, used to link to it from other models 98 | id: $id, 99 | $($fields)* 100 | /// The `active` field allows a model to be deactivated, 101 | /// which keeps it in data while only allowing it to be used 102 | /// or altered in specific ways. 103 | #[builder(default)] 104 | active: bool, 105 | /// Notes when the model was created. 106 | created: chrono::DateTime, 107 | /// Notes when the model was last updated. 108 | updated: chrono::DateTime, 109 | /// Notes if the model has been deleted, which has the same 110 | /// effect of deactivation, but is permanent. 111 | deleted: Option>, 112 | } 113 | } 114 | 115 | impl $model { 116 | /// Returns a builder for this model 117 | #[allow(dead_code)] 118 | pub(crate) fn builder() -> $builder { 119 | $builder::default() 120 | } 121 | } 122 | 123 | 124 | impl crate::models::lib::basis_model::Model for $model { 125 | fn is_deleted(&self) -> bool { 126 | self.deleted.is_some() 127 | } 128 | 129 | fn is_active(&self) -> bool { 130 | self.active && !self.is_deleted() 131 | } 132 | 133 | fn set_deleted(&mut self, deleted: Option>) { 134 | $model::set_deleted(self, deleted); 135 | } 136 | 137 | fn set_active(&mut self, active: bool) { 138 | $model::set_active(self, active); 139 | } 140 | } 141 | 142 | impl std::convert::From<$model> for crate::models::Model { 143 | fn from(val: $model) -> Self { 144 | crate::models::Model::$model(val) 145 | } 146 | } 147 | 148 | impl std::convert::TryFrom for $model { 149 | type Error = crate::error::Error; 150 | 151 | fn try_from(val: crate::models::Model) -> std::result::Result { 152 | match val { 153 | crate::models::Model::$model(val) => Ok(val), 154 | _ => Err(crate::error::Error::WrongModelType), 155 | } 156 | } 157 | } 158 | } 159 | pub use inner::$model; 160 | } 161 | } 162 | 163 | /// Applies meta to various model fields depending on their type 164 | macro_rules! basis_model_inner { 165 | // grab Vec fields and apply special meta 166 | ( 167 | @parse_fields ($($parsed_fields:tt)*) 168 | $(#[$struct_meta:meta])* 169 | pub struct $name:ident { 170 | $(#[$field_meta:meta])* 171 | $field_name:ident: Vec<$field_type:ty>, 172 | 173 | $($fields:tt)* 174 | } 175 | ) => { 176 | basis_model_inner! { 177 | @parse_fields ( 178 | $($parsed_fields)* 179 | 180 | $(#[$field_meta])* 181 | #[builder(default)] 182 | #[cfg_attr(feature = "with_serde", serde(default = "Default::default", skip_serializing_if = "Vec::is_empty"))] 183 | $field_name: Vec<$field_type>, 184 | ) 185 | $(#[$struct_meta])* 186 | pub struct $name { 187 | $($fields)* 188 | } 189 | } 190 | }; 191 | 192 | // grab Option fields and apply special meta 193 | ( 194 | @parse_fields ($($parsed_fields:tt)*) 195 | $(#[$struct_meta:meta])* 196 | pub struct $name:ident { 197 | $(#[$field_meta:meta])* 198 | $field_name:ident: Option<$field_type:ty>, 199 | 200 | $($fields:tt)* 201 | } 202 | ) => { 203 | basis_model_inner! { 204 | @parse_fields ( 205 | $($parsed_fields)* 206 | 207 | $(#[$field_meta])* 208 | #[builder(default)] 209 | #[cfg_attr(feature = "with_serde", serde(default = "Default::default", skip_serializing_if = "Option::is_none"))] 210 | $field_name: Option<$field_type>, 211 | ) 212 | $(#[$struct_meta])* 213 | pub struct $name { 214 | $($fields)* 215 | } 216 | } 217 | }; 218 | 219 | // parse "normal" fields 220 | ( 221 | @parse_fields ($($parsed_fields:tt)*) 222 | $(#[$struct_meta:meta])* 223 | pub struct $name:ident { 224 | $(#[$field_meta:meta])* 225 | $field_name:ident: $field_type:ty, 226 | 227 | $($fields:tt)* 228 | } 229 | ) => { 230 | basis_model_inner! { 231 | @parse_fields ( 232 | $($parsed_fields)* 233 | 234 | $(#[$field_meta])* 235 | $field_name: $field_type, 236 | ) 237 | $(#[$struct_meta])* 238 | pub struct $name { 239 | $($fields)* 240 | } 241 | } 242 | }; 243 | 244 | // all done 245 | ( 246 | @parse_fields ($($parsed_fields:tt)*) 247 | $(#[$struct_meta:meta])* 248 | pub struct $name:ident {} 249 | ) => { 250 | $(#[$struct_meta])* 251 | pub struct $name { 252 | $($parsed_fields)* 253 | } 254 | }; 255 | 256 | // entry 257 | ( 258 | $(#[$struct_meta:meta])* 259 | pub struct $name:ident { 260 | $($fields:tt)* 261 | } 262 | ) => { 263 | basis_model_inner! { 264 | @parse_fields () 265 | $(#[$struct_meta])* 266 | pub struct $name { 267 | $($fields)* 268 | } 269 | } 270 | }; 271 | } 272 | 273 | -------------------------------------------------------------------------------- /src/models/lib/mod.rs: -------------------------------------------------------------------------------- 1 | /// A macro that standardizes including, exporting, and creating wrapper type(s) 2 | /// for our heroic models. 3 | macro_rules! load_models { 4 | ( 5 | @pub mod 6 | $( ($path:ident, $($_rest:tt)*), )* 7 | ) => { 8 | $( 9 | pub mod $path; 10 | )* 11 | }; 12 | 13 | // create an enum that wraps our models in CUD 14 | ( 15 | @pub enum $enumname:ident 16 | $( ($path:ident, $model:ident, $($_extratypes:ident),*), )* 17 | ) => { 18 | /// An enum that allows returning *any* model type. This is mainly used 19 | /// along with [Op](enum.Op.html) to specify modifications (ie 20 | /// `[Op::Create, User]`). 21 | #[derive(Debug, Clone, PartialEq)] 22 | #[cfg_attr(feature = "with_serde", derive(serde::Serialize, serde::Deserialize))] 23 | pub enum $enumname { 24 | $( 25 | $model(crate::models::$path::$model), 26 | )* 27 | } 28 | }; 29 | 30 | // entry point 31 | ($($load_type:tt)*) => { 32 | load_models! { 33 | @$($load_type)* 34 | (account, Account, AccountID), 35 | (agreement, Agreement, AgreementID), 36 | (commitment, Commitment, CommitmentID), 37 | (company, Company, CompanyID), 38 | (member, Member, MemberID), 39 | (currency, Currency, CurrencyID), 40 | (event, Event, EventID), 41 | (intent, Intent, IntentID), 42 | (occupation, Occupation, OccupationID), 43 | (process, Process, ProcessID), 44 | (process_spec, ProcessSpec, ProcessSpecID), 45 | (resource, Resource, ResourceID), 46 | (resource_spec, ResourceSpec, ResourceSpecID, Dimensions), 47 | (user, User, UserID), 48 | 49 | //(resource_group, ResourceGroup, ResourceGroupID), 50 | //(resource_group_link, ResourceGroupLink, ResourceGroupLinkID), 51 | } 52 | }; 53 | } 54 | 55 | #[macro_use] 56 | pub(crate) mod basis_model; 57 | pub mod agent; 58 | 59 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | //! Models are the "atom" datatypes for Basis. They represent the objects in the 2 | //! system and their relationships to each other (via IDs). Each model has a 3 | //! struct (ie `User`) and an ID object (ie `UserID`). The id object allows 4 | //! models to link to each other without having to embed the graph into the 5 | //! model data itself. 6 | //! 7 | //! Models are read-only and can only be created or updated using 8 | //! [transactions]. 9 | //! 10 | //! In some cases models contain business logic (like [Event]) that define 11 | //! various interactions. For the most part though, models define data structure 12 | //! and relationships. 13 | //! 14 | //! This module also contains some utilities for enumerating changes to models 15 | //! (like [Modifications]) and the classes that support them. 16 | //! 17 | //! Note that because this crate relies heavily on the [ValueFlows ontology][vf] 18 | //! that many of the models have an `inner` field which represents the 19 | //! corresponding ValueFlows type associated with the model. Composition is used 20 | //! as the default pattern here, which offers a fairly clean implementation but 21 | //! with the small sacrifice of having to sometimes to `model.inner().val()` 22 | //! instead of just `model.val()`. The tradeoff is that the VF types are cleanly 23 | //! separated from the Basis models. 24 | //! 25 | //! [transactions]: ../transactions 26 | //! [Event]: event/struct.Event.html 27 | //! [Modifications]: struct.Modifications.html 28 | //! [vf]: https://valueflo.ws/ 29 | 30 | use crate::{ 31 | error::{Error, Result}, 32 | }; 33 | #[cfg(feature = "with_serde")] 34 | use serde::{Serialize, Deserialize}; 35 | use std::convert::TryFrom; 36 | 37 | #[macro_use] 38 | pub(crate) mod lib; 39 | 40 | pub use lib::agent::{Agent, AgentID}; 41 | 42 | // load all of our pub mod ; ... lines 43 | load_models!{ pub mod } 44 | 45 | // create an enum that contains all of our model types 46 | load_models!{ pub enum Model } 47 | 48 | /// A type for determining if a model should be created, updated, or deleted. 49 | #[derive(Debug, Clone, PartialEq)] 50 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 51 | pub enum Op { 52 | /// Create a model 53 | Create, 54 | /// Update a model 55 | Update, 56 | /// Delete a model 57 | Delete, 58 | } 59 | 60 | /// Documents a modification to a model. 61 | #[derive(Debug, Clone, PartialEq)] 62 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 63 | pub struct Modification { 64 | /// The type of modification 65 | op: Op, 66 | /// The model we're modifying 67 | model: Model, 68 | } 69 | 70 | impl Modification { 71 | /// Create a new modification 72 | pub(crate) fn new(op: Op, model: Model) -> Self { 73 | Self { op, model } 74 | } 75 | 76 | /// Turn this modification into a pair. Good for implementing saving logic: 77 | /// 78 | /// ```rust 79 | /// use basis_core::{ 80 | /// models::{ 81 | /// Model, 82 | /// Modification, 83 | /// Op, 84 | /// account::AccountID, 85 | /// user::{User, UserID}, 86 | /// }, 87 | /// transactions, 88 | /// }; 89 | /// use chrono::Utc; 90 | /// 91 | /// fn save_mod(modification: Modification) -> Result<(), String> { 92 | /// match modification.into_pair() { 93 | /// (Op::Create, Model::User(user)) => { /* create a user in your db ... */ } 94 | /// (Op::Update, Model::Process(process)) => { /* update a process in your db ... */ } 95 | /// (Op::Delete, Model::Resource(resource)) => { /* delete a resource in your db ... */ } 96 | /// _ => {} 97 | /// } 98 | /// Ok(()) 99 | /// } 100 | /// 101 | /// let mods = transactions::user::create(UserID::new("eb5af35f-8f48-4794-8d75-0cd07d7c6650"), "andrew@lyonbros.com", "andrew", AccountID::new("5fcf7f71-d965-4f10-a4af-5a289335c586"), true, &Utc::now()).unwrap(); 102 | /// for modification in mods { 103 | /// save_mod(modification).unwrap(); 104 | /// } 105 | /// ``` 106 | pub fn into_pair(self) -> (Op, Model) { 107 | (self.op, self.model) 108 | } 109 | 110 | /// Consume this modification, and verify that the `Op` matches the one 111 | /// passed in, then return the *unwrapped* model (ie, not `Model::User(user)` 112 | /// but `user as User`). 113 | /// 114 | /// Very handy for testing: 115 | /// ```rust 116 | /// use basis_core::{ 117 | /// models::{ 118 | /// Op, 119 | /// account::AccountID, 120 | /// user::{User, UserID}, 121 | /// }, 122 | /// transactions, 123 | /// }; 124 | /// use chrono::Utc; 125 | /// 126 | /// let mods = transactions::user::create(UserID::new("571c5e2b-1fde-43d4-a15b-9cbcb929849f"), "andrew@lyonbros.com", "andrew", AccountID::new("af1d3c55-a737-4979-b1b5-0ad18a810bb0"), true, &Utc::now()).unwrap().into_vec(); 127 | /// // verifies that the first modification is User Create, and returns the 128 | /// // User model. 129 | /// let user = mods[0].clone().expect_op::(Op::Create).unwrap(); 130 | /// assert_eq!(user.name(), "andrew"); 131 | /// ``` 132 | pub fn expect_op>(self, verify_op: Op) -> Result { 133 | let (op, model) = self.into_pair(); 134 | if op != verify_op { 135 | Err(Error::OpMismatch)?; 136 | } 137 | // NOTE: I do not know why I have to map this error. Seems dumb. 138 | Ok(T::try_from(model).map_err(|_| Error::WrongModelType)?) 139 | } 140 | } 141 | 142 | /// A set of modifications we want to make to any number of models. 143 | /// 144 | /// This is passed back by successfully run transactions. You can use a set of 145 | /// modifications either by converting into a vec (`into_vec()`), or using an 146 | /// iterator. 147 | #[derive(Debug, Default, Clone, PartialEq)] 148 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 149 | pub struct Modifications { 150 | /// The model modifications we're making 151 | modifications: Vec, 152 | } 153 | 154 | impl Modifications { 155 | /// Create a new modification set 156 | pub(crate) fn new() -> Self { 157 | Self::default() 158 | } 159 | 160 | /// Create a new modification set with a single mod 161 | pub(crate) fn new_single>(op: Op, model: T) -> Self { 162 | let mut mods = Self::new(); 163 | mods.push(op, model); 164 | mods 165 | } 166 | 167 | /// Consume the modification set and return the list of modifications 168 | pub fn into_vec(self) -> Vec { 169 | self.modifications 170 | } 171 | 172 | /// Push a raw modification object into the mods list. 173 | pub(crate) fn push_raw(&mut self, modification: Modification) { 174 | self.modifications.push(modification); 175 | } 176 | 177 | /// Push a modification into the list with a `Op` and `Model` (bypasses 178 | /// having to create a `Modification` by hand) 179 | pub(crate) fn push>(&mut self, op: Op, model: T) { 180 | self.push_raw(Modification::new(op, model.into())); 181 | } 182 | } 183 | 184 | impl IntoIterator for Modifications { 185 | type Item = Modification; 186 | type IntoIter = std::vec::IntoIter; 187 | 188 | fn into_iter(self) -> Self::IntoIter { 189 | self.modifications.into_iter() 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use super::*; 196 | use crate::{ 197 | models::{ 198 | process::Process, 199 | user::{User, UserID}, 200 | }, 201 | util::{self, test::*}, 202 | }; 203 | 204 | #[test] 205 | fn modifications() { 206 | let now = util::time::now(); 207 | let user = make_user(&UserID::new("slappy"), None, &now); 208 | let mut modifications = Modifications::new_single(Op::Create, user.clone()); 209 | modifications.push(Op::Update, user); 210 | 211 | for modi in modifications.clone() { 212 | match modi.into_pair() { 213 | (_, Model::User(_)) => {} 214 | _ => panic!("modification mismatch"), 215 | } 216 | } 217 | 218 | let mods = modifications.into_vec(); 219 | let user = mods[0].clone().expect_op::(Op::Create).unwrap(); 220 | assert_eq!(user.id(), &UserID::new("slappy")); 221 | let user = mods[1].clone().expect_op::(Op::Update).unwrap(); 222 | assert_eq!(user.id(), &UserID::new("slappy")); 223 | let res = mods[0].clone().expect_op::(Op::Create); 224 | assert_eq!(res, Err(Error::WrongModelType)); 225 | let res = mods[1].clone().expect_op::(Op::Update); 226 | assert_eq!(res, Err(Error::WrongModelType)); 227 | let res = mods[0].clone().expect_op::(Op::Update); 228 | assert_eq!(res, Err(Error::OpMismatch)); 229 | let res = mods[1].clone().expect_op::(Op::Create); 230 | assert_eq!(res, Err(Error::OpMismatch)); 231 | let res = mods[0].clone().expect_op::(Op::Update); 232 | assert_eq!(res, Err(Error::OpMismatch)); 233 | } 234 | } 235 | 236 | -------------------------------------------------------------------------------- /src/models/occupation.rs: -------------------------------------------------------------------------------- 1 | //! An occupation represents a type of job that we desire to track. Occupations 2 | //! are tracked by id in the cost tracking system, allowing only a set amount of 3 | //! job types to be accounted for (as opposed to using freeform entry). 4 | //! 5 | //! Note that occupations require global systemic management. 6 | 7 | use vf_rs::vf; 8 | 9 | basis_model! { 10 | /// The occupation model assigns an `OccupationID` to a job title and allows 11 | /// future-proof cost tracking of that job type. 12 | pub struct Occupation { 13 | id: <>, 14 | /// The inner VF type which holds our `role_label` field used to hold 15 | /// the occupation name. 16 | inner: vf::AgentRelationshipRole, 17 | } 18 | OccupationBuilder 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/models/process.rs: -------------------------------------------------------------------------------- 1 | //! Processes are aggregators of costs via their inputs, labor and resources, 2 | //! and dividers/subtractors of costs via their outputs, resources and services. 3 | 4 | use crate::{ 5 | costs::{Costs, CostMover}, 6 | models::{ 7 | company::CompanyID, 8 | lib::agent::AgentID, 9 | process_spec::ProcessSpecID, 10 | }, 11 | }; 12 | use url::Url; 13 | use vf_rs::vf; 14 | 15 | basis_model! { 16 | /// The `Process` model wraps the [vf::Process][vfprocess] object and adds 17 | /// cost tracking in. Processes are the places where inputs are transformed 18 | /// into outputs. 19 | /// 20 | /// Processes must reference a [ProcessSpec], which acts as a grouping of 21 | /// processes of similar type, but can also (in special cases) act as a 22 | /// transformer of various tracked raw resources (ie crude oil -> diesel, 23 | /// jet fuel, etc). 24 | /// 25 | /// [vfprocess]: https://valueflo.ws/introduction/processes.html 26 | /// [ProcessSpec]: ../process_spec/struct.ProcessSpec.html 27 | pub struct Process { 28 | id: <>, 29 | /// The inner VF process 30 | inner: vf::Process, 31 | /// The company this process belongs to 32 | company_id: CompanyID, 33 | /// Our costs tally for this process 34 | costs: Costs, 35 | } 36 | ProcessBuilder 37 | } 38 | 39 | impl CostMover for Process { 40 | fn costs(&self) -> &Costs { 41 | self.costs() 42 | } 43 | 44 | fn set_costs(&mut self, costs: Costs) { 45 | self.set_costs(costs); 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use super::*; 52 | use crate::{ 53 | models::{ 54 | company::CompanyID, 55 | }, 56 | util::{self, test::*}, 57 | }; 58 | 59 | #[test] 60 | fn compare() { 61 | let now = util::time::now(); 62 | let id1 = ProcessID::new("widget1"); 63 | let id2 = ProcessID::new("widget2"); 64 | let company_id1 = CompanyID::new("jerry's widgets"); 65 | let company_id2 = CompanyID::new("frank's widgets"); 66 | let costs = Costs::new_with_labor("machinist", num!(23.2)); 67 | let mut costs2 = costs.clone(); 68 | costs2.track_labor("mayor", num!(2.4)); 69 | let process1 = make_process(&id1, &company_id1, "make widgets", &costs, &now); 70 | let process2 = make_process(&id2, &company_id2, "burn widgets", &costs, &now); 71 | 72 | assert!(process1 == process1); 73 | assert!(process2 == process2); 74 | assert!(process1.clone() == process1.clone()); 75 | assert!(process2.clone() == process2.clone()); 76 | assert!(process1 != process2); 77 | let mut process3 = process2.clone(); 78 | assert!(process1 != process3); 79 | process3.set_id(id1.clone()); 80 | assert!(process1 != process3); 81 | process3.inner_mut().set_name("make widgets".into()); 82 | assert!(process1 != process3); 83 | process3.set_company_id(company_id1.clone().into()); 84 | assert!(process1 == process3); 85 | process3.set_costs(costs2); 86 | assert!(process1 != process3); 87 | process3.set_costs(Costs::new_with_labor("machinist", num!(23.2))); 88 | assert!(process1 == process3); 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/models/process_spec.rs: -------------------------------------------------------------------------------- 1 | //! Process specifications group `Process` models, such that the process spec 2 | //! might be "Make coat" and the process itself would be an *instance* of "Make 3 | //! coat" that happened at a specific time with a specific set of inputs and 4 | //! outputs which are used for cost tracking. 5 | //! 6 | //! Process specifications can also contain resource transformations (such as 7 | //! turning iron into steel). In effect, the transformation acts to *consume* 8 | //! the input resource, whereas in most cases processes just move resources. 9 | 10 | use crate::{ 11 | models::{ 12 | company::CompanyID, 13 | }, 14 | }; 15 | use vf_rs::vf; 16 | 17 | basis_model! { 18 | /// The `ProcessSpec` model 19 | pub struct ProcessSpec { 20 | id: <>, 21 | /// Our VF process object. 22 | inner: vf::ProcessSpecification, 23 | /// The company this process spec belongs to 24 | company_id: CompanyID, 25 | // TODO: implement some concept of a known transformation (ie, refining 26 | // crude oil) 27 | //resource_transform: Option, 28 | } 29 | ProcessSpecBuilder 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/models/resource.rs: -------------------------------------------------------------------------------- 1 | //! A resource is a tangible asset. This can be anything, like a chair, a car, 2 | //! a coat, or a carrot. It does not necessarily have to start with a "c". When 3 | //! trying to understand resources, it's important to note that a resource is an 4 | //! instance of a *resource specification*. When you look at a chair on Wamazon, 5 | //! the page describes a resource specification. When the chair is shipped to 6 | //! you, what you get is a resource (a manifestation of the chair specification). 7 | 8 | use crate::{ 9 | costs::{Costs, CostMover}, 10 | models::{ 11 | lib::agent::AgentID, 12 | process::ProcessID, 13 | resource_spec::ResourceSpecID, 14 | }, 15 | util::measure, 16 | }; 17 | use om2::Unit; 18 | use url::Url; 19 | use vf_rs::vf; 20 | 21 | basis_model! { 22 | /// The resource model. Wraps the [vf::Resource][vfresource] object, and 23 | /// also tracks custody information as well as costs. 24 | /// 25 | /// [vfresource]: https://valueflo.ws/introduction/resources.html 26 | pub struct Resource { 27 | id: <>, 28 | /// The VF object for this product instance 29 | inner: vf::EconomicResource, 30 | /// The agent that has custody of the resource 31 | in_custody_of: AgentID, 32 | /// The costs imbued in this resource. Note that the `inner` field's 33 | /// `vf::EconomicResource` object can contain a measure (ie, 5kg) and 34 | /// the costs attached to this resource are the *total* costs for the 35 | /// total measured resource. For instance, if our costs are `5 hours` 36 | /// and we have a measure of 16g, the `5 hours` cost encompasses all 37 | /// 16g. 38 | costs: Costs, 39 | } 40 | ResourceBuilder 41 | } 42 | 43 | impl Resource { 44 | /// Get this resource's Unit (if it has it) 45 | pub fn get_unit(&self) -> Option { 46 | self.inner().accounting_quantity().clone().or_else(|| self.inner().onhand_quantity().clone()) 47 | .map(|measure| measure.has_unit().clone()) 48 | } 49 | 50 | /// Zero out the accounting/onhand quantity measurements for this resource. 51 | pub fn zero_measures(&mut self) { 52 | self.inner_mut().accounting_quantity_mut().as_mut() 53 | .map(|x| measure::set_zero(x)); 54 | self.inner_mut().onhand_quantity_mut().as_mut() 55 | .map(|x| measure::set_zero(x)); 56 | } 57 | } 58 | 59 | impl CostMover for Resource { 60 | fn costs(&self) -> &Costs { 61 | self.costs() 62 | } 63 | 64 | fn set_costs(&mut self, costs: Costs) { 65 | self.set_costs(costs); 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod tests { 71 | use super::*; 72 | use crate::{ 73 | models::{ 74 | company::CompanyID, 75 | }, 76 | util::{self, test::*}, 77 | }; 78 | use om2::{Measure, Unit}; 79 | 80 | #[test] 81 | fn compare() { 82 | let now = util::time::now(); 83 | let id1 = ResourceID::new("widget1"); 84 | let id2 = ResourceID::new("widget2"); 85 | let company_id1 = CompanyID::new("jerry's widgets"); 86 | let company_id2 = CompanyID::new("frank's widgets"); 87 | let measure = Measure::new(50, Unit::Kilogram); 88 | let costs = Costs::new_with_labor("machinist", num!(23.2)); 89 | let resource1 = make_resource(&id1, &company_id1, &measure, &costs, &now); 90 | let resource2 = make_resource(&id2, &company_id2, &measure, &costs, &now); 91 | 92 | assert!(resource1 == resource1); 93 | assert!(resource2 == resource2); 94 | assert!(resource1.clone() == resource1.clone()); 95 | assert!(resource2.clone() == resource2.clone()); 96 | assert!(resource1 != resource2); 97 | let mut resource3 = resource2.clone(); 98 | assert!(resource1 != resource3); 99 | resource3.set_id(id1.clone()); 100 | assert!(resource1 != resource3); 101 | resource3.set_in_custody_of(company_id1.clone().into()); 102 | assert!(resource1 != resource3); 103 | resource3.inner_mut().set_primary_accountable(Some(company_id1.clone().into())); 104 | assert!(resource1 == resource3); 105 | resource3.set_costs(Costs::new_with_labor("machinist", num!(23.1))); 106 | assert!(resource1 != resource3); 107 | resource3.set_costs(Costs::new_with_labor("machinist", num!(23.2))); 108 | assert!(resource1 == resource3); 109 | } 110 | 111 | #[test] 112 | fn get_unit() { 113 | let now = util::time::now(); 114 | let resource = make_resource(&ResourceID::create(), &CompanyID::create(), &Measure::new(69, Unit::Litre), &Costs::new_with_labor("TUNA", 54), &now); 115 | let mut resource2 = resource.clone(); 116 | resource2.inner_mut().set_accounting_quantity(None); 117 | let mut resource3 = resource.clone(); 118 | resource3.inner_mut().set_onhand_quantity(None); 119 | let mut resource4 = resource2.clone(); 120 | resource4.inner_mut().set_onhand_quantity(None); 121 | assert_eq!(resource.get_unit(), Some(Unit::Litre)); 122 | assert_eq!(resource2.get_unit(), Some(Unit::Litre)); 123 | assert_eq!(resource3.get_unit(), Some(Unit::Litre)); 124 | assert_eq!(resource4.get_unit(), None); 125 | 126 | 127 | } 128 | } 129 | 130 | -------------------------------------------------------------------------------- /src/models/resource_group.rs: -------------------------------------------------------------------------------- 1 | basis_model! { 2 | /// Acts as a group for various products classified as resources. 3 | /// 4 | /// For instance, a group might be "iron", and all the iron produced by iron 5 | /// mines might link to the group. 6 | pub struct ResourceGroup { 7 | /// The name of the group, generally will be some easily-identifiable 8 | /// resource name like "iron" or "silicon" or "fresh water" 9 | name: String, 10 | /// The globally-decided cost (in credits) for products under this group. 11 | credit_cost_per_unit: f64, 12 | } 13 | ResourceGroupID 14 | ResourceGroupBuilder 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/models/resource_group_link.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | models::{ 3 | resource_spec::ResourceSpecID, 4 | resource_group::ResourceGroupID, 5 | }, 6 | }; 7 | basis_model! { 8 | pub struct ResourceGroupLink { 9 | /// The ID of the resource group. 10 | group_id: ResourceGroupID, 11 | /// The ID of the product we're linking to the group. 12 | product_id: ResourceSpecID, 13 | // TODO: at some point, store meta information about the resource 14 | // quantity/renewal/depletion/etc 15 | } 16 | ResourceGroupLinkID 17 | ResourceGroupLinkBuilder 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/models/resource_spec.rs: -------------------------------------------------------------------------------- 1 | //! Resource specifications are meta descriptions of `Resource` objects. If you 2 | //! buy a "Zenith Men's 96.0529.4035/51.M Defy Xtreme Tourbillon Titanium 3 | //! Chronograph Watch" on Wamazon, the watch you get in the mail is the resource 4 | //! and the *resource specification* is the Wamazon product description page. 5 | 6 | use crate::{ 7 | models::{ 8 | company::CompanyID, 9 | }, 10 | }; 11 | use url::Url; 12 | use vf_rs::vf; 13 | 14 | basis_model! { 15 | /// The `ResourceSpec` model wraps our heroic [vf::ResourceSpecification][vfresource] 16 | /// object, with one addition: we add a `CompanyID`, which effectively acts 17 | /// to namespace resource specifications on a per-company basis. 18 | /// 19 | /// [vfresource]: https://valueflo.ws/introduction/resources.html 20 | pub struct ResourceSpec { 21 | id: <>, 22 | inner: vf::ResourceSpecification, 23 | /// products are namespaced by company_id. we have no interest in trying 24 | /// to classify some chair as a Chair that anyone can build, but rather 25 | /// only as a chair built by a specific company. 26 | /// 27 | /// if we want to group products together, we certainly can, but this is 28 | /// not the place for it. 29 | company_id: CompanyID, 30 | } 31 | ResourceSpecBuilder 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/models/user.rs: -------------------------------------------------------------------------------- 1 | //! The user is the point of access to the entire system. Users have 2 | //! permissions, users own accounts, users are linked to companies (via 3 | //! `Member` objects), and much more. 4 | //! 5 | //! Every person in the system (whether they are a member or not) is represented 6 | //! by a `User` object. 7 | 8 | use crate::{ 9 | access::{Permission, Role}, 10 | models::{ 11 | lib::{ 12 | agent::{Agent, AgentID}, 13 | basis_model::Model, 14 | }, 15 | }, 16 | error::{Error, Result}, 17 | }; 18 | 19 | basis_model! { 20 | /// The `User` model describes a user of the system. 21 | pub struct User { 22 | id: <>, 23 | /// Defines this user's roles, ie what permissions they have access to. 24 | roles: Vec, 25 | /// The user's email. Might be best to use a proxy address, since most 26 | /// of this data will be fairly public. 27 | email: String, 28 | /// The user's full name. 29 | name: String, 30 | } 31 | UserBuilder 32 | } 33 | 34 | impl User { 35 | /// Determines if a user can perform an action (base on their roles). 36 | pub fn can(&self, permission: &Permission) -> bool { 37 | if !self.is_active() { 38 | return false; 39 | } 40 | for role in self.roles() { 41 | if role.can(permission) { 42 | return true; 43 | } 44 | } 45 | false 46 | } 47 | 48 | /// Check if this user can perform an action. 49 | pub fn access_check(&self, permission: Permission) -> Result<()> { 50 | if !self.can(&permission) { 51 | Err(Error::InsufficientPrivileges)?; 52 | } 53 | Ok(()) 54 | } 55 | } 56 | 57 | impl Agent for User { 58 | fn agent_id(&self) -> AgentID { 59 | self.id().clone().into() 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use crate::{ 66 | access::{Permission, Role}, 67 | models::{ 68 | user::UserID, 69 | }, 70 | util::{self, test::*}, 71 | }; 72 | 73 | #[test] 74 | fn permissions() { 75 | let now = util::time::now(); 76 | let user = make_user(&UserID::create(), None, &now); 77 | assert!(user.can(&Permission::UserDelete)); 78 | assert!(user.access_check(Permission::UserDelete).is_ok()); 79 | assert!(user.access_check(Permission::UserAdminCreate).is_err()); 80 | 81 | let user2 = make_user(&UserID::create(), Some(vec![Role::User, Role::SuperAdmin]), &now); 82 | assert!(user2.can(&Permission::UserDelete)); 83 | assert!(user2.access_check(Permission::UserDelete).is_ok()); 84 | assert!(user2.access_check(Permission::UserAdminCreate).is_ok()); 85 | 86 | let user3 = make_user(&UserID::create(), Some(vec![]), &now); 87 | assert!(!user3.can(&Permission::UserDelete)); 88 | assert!(user3.access_check(Permission::UserDelete).is_err()); 89 | assert!(user3.access_check(Permission::UserAdminCreate).is_err()); 90 | 91 | let mut user4 = user2.clone(); 92 | user4.set_deleted(Some(now.clone())); 93 | assert!(!user4.can(&Permission::UserDelete)); 94 | assert!(user4.access_check(Permission::UserDelete).is_err()); 95 | assert!(user4.access_check(Permission::UserAdminCreate).is_err()); 96 | 97 | let mut user5 = user2.clone(); 98 | user5.set_active(false); 99 | assert!(!user5.can(&Permission::UserDelete)); 100 | assert!(user5.access_check(Permission::UserDelete).is_err()); 101 | assert!(user5.access_check(Permission::UserAdminCreate).is_err()); 102 | } 103 | } 104 | 105 | -------------------------------------------------------------------------------- /src/system/mod.rs: -------------------------------------------------------------------------------- 1 | //! A module containing objects, methods, or processes that can act on behalf of 2 | //! the system itself. For instance, a voting user/member that acts on behalf of 3 | //! the system or a company, or a user that masks/anonymizes consumer purchases. 4 | 5 | pub mod ubi; 6 | pub mod vote; 7 | 8 | -------------------------------------------------------------------------------- /src/system/ubi.rs: -------------------------------------------------------------------------------- 1 | //! Defines systemic parameters for the Basis UBI, such as how much is paid 2 | //! over time and the upper ceiling on UBI accounts (to prevent endless 3 | //! accumulation). 4 | 5 | use getset::{Getters, Setters}; 6 | use rust_decimal::Decimal; 7 | #[cfg(feature = "with_serde")] 8 | use serde::{Serialize, Deserialize}; 9 | 10 | /// Holds systemic UBI parameters. 11 | #[derive(Clone, Default, Debug, PartialEq, Getters, Setters)] 12 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 13 | #[getset(get = "pub", set = "pub(crate)")] 14 | pub struct UBIParameters { 15 | /// The maximum balance a UBI account can hold 16 | ceiling: Decimal, 17 | /// How much UBI we get over time 18 | balance_per_day: Decimal, 19 | } 20 | 21 | impl UBIParameters { 22 | /// Create a new empty params object 23 | pub fn new() -> Self { 24 | Default::default() 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/system/vote.rs: -------------------------------------------------------------------------------- 1 | //! The vote module allows creating user objects that effectively have network 2 | //! super powers. The idea is that this user can be wielded by whatever system 3 | //! implements the core to represent votes either systemically or within 4 | //! specific companies. 5 | //! 6 | //! The idea here is to provide an interface for democracy without the core 7 | //! needing to know the implementation details. 8 | //! 9 | //! ```rust 10 | //! use basis_core::{ 11 | //! access::Role, 12 | //! models::{ 13 | //! Agent, 14 | //! company::{CompanyID, Permission as CompanyPermission}, 15 | //! member::MemberID, 16 | //! user::UserID, 17 | //! }, 18 | //! system::vote::Vote, 19 | //! }; 20 | //! use chrono::Utc; 21 | //! 22 | //! let user_id = UserID::new("d183116f-85ac-4c6d-a553-b947b1f677a9"); 23 | //! let systemic_voter = Vote::systemic(user_id.clone(), &Utc::now()).unwrap(); 24 | //! assert_eq!(systemic_voter.user().id(), &user_id); 25 | //! assert_eq!(systemic_voter.user().roles(), &vec![Role::SuperAdmin]); 26 | //! assert_eq!(systemic_voter.member(), &None); 27 | //! 28 | //! let user_id = UserID::new("d7142ca2-c7a0-4125-a879-2aace236b2c0"); 29 | //! let member_id = MemberID::new("4826728e-003a-4fde-a5c8-f43084c5d530"); 30 | //! let company_id = CompanyID::new("hairy larry's scrumptious dairies"); 31 | //! let company_voter = Vote::company(user_id.clone(), member_id.clone(), &company_id, &Utc::now()).unwrap(); 32 | //! assert_eq!(company_voter.user().id(), &user_id); 33 | //! assert_eq!(company_voter.user().roles(), &vec![Role::User]); 34 | //! assert_eq!(company_voter.member().as_ref().unwrap().id(), &member_id); 35 | //! assert_eq!(company_voter.member().as_ref().unwrap().inner().subject(), &company_voter.user().agent_id()); 36 | //! assert_eq!(company_voter.member().as_ref().unwrap().inner().object(), &company_id.clone().into()); 37 | //! assert_eq!(company_voter.member().as_ref().unwrap().permissions(), &vec![CompanyPermission::All]); 38 | //! ``` 39 | 40 | use chrono::{DateTime, Utc}; 41 | use crate::{ 42 | access::Role, 43 | error::{Error, Result}, 44 | models::{ 45 | company::{CompanyID, Permission as CompanyPermission}, 46 | lib::agent::{Agent, AgentID}, 47 | member::*, 48 | user::{User, UserID}, 49 | }, 50 | }; 51 | use getset::Getters; 52 | use vf_rs::vf; 53 | 54 | /// An object that holds information about a voting user as well as any extra 55 | /// information we might need. For instance, we might only need a user object if 56 | /// voting on something systemic, but if a vote occurs within a specific company 57 | /// then we need both a user and a member object created for us. 58 | #[derive(Clone, Debug, PartialEq, Getters)] 59 | #[getset(get = "pub")] 60 | pub struct Vote { 61 | /// Holds our voting user 62 | user: User, 63 | /// Holds our voting member, if we have one 64 | member: Option, 65 | } 66 | 67 | impl Vote { 68 | /// Utility function to make a new user with a given role. 69 | fn make_voter(id: UserID, role: Role, now: &DateTime) -> Result { 70 | User::builder() 71 | .roles(vec![role]) 72 | .email(format!("vote-{}@basisproject.net", id.as_str())) 73 | .name(format!("Vote {}", id.as_str())) 74 | .id(id) 75 | .active(true) 76 | .created(now.clone()) 77 | .updated(now.clone()) 78 | .build() 79 | .map_err(|e| Error::BuilderFailed(e)) 80 | } 81 | 82 | /// Create a new systemic voting user. 83 | /// 84 | /// This can be used for systemic changes that don't need a company or a 85 | /// member object. For instance, this might be used to adjust costs of 86 | /// various tracked resources or manage occupation data. This user is given 87 | /// super admin abilities. 88 | /// 89 | /// If you want to vote to run a transaction for a specific company, see 90 | /// the `Vote::company()` method. 91 | pub fn systemic(user_id: UserID, now: &DateTime) -> Result { 92 | let user = Self::make_voter(user_id, Role::SuperAdmin, now)?; 93 | Ok(Self { 94 | user, 95 | member: None, 96 | }) 97 | } 98 | 99 | /// Create a new voting company member. 100 | /// 101 | /// This is specifically for voting to run a transaction internal to a 102 | /// company. This member is given company-wide admin abilities. 103 | pub fn company(user_id: UserID, member_id: MemberID, company_id: &CompanyID, now: &DateTime) -> Result { 104 | let user = Self::make_voter(user_id, Role::User, now)?; 105 | let company_agent_id: AgentID = company_id.clone().into(); 106 | let member = Member::builder() 107 | .id(member_id) 108 | .inner( 109 | vf::AgentRelationship::builder() 110 | .subject(user.agent_id()) 111 | .object(company_agent_id) 112 | .relationship(()) 113 | .build() 114 | .map_err(|e| Error::BuilderFailed(e))? 115 | ) 116 | .class(MemberClass::User(MemberUser::new())) 117 | .permissions(vec![CompanyPermission::All]) 118 | .agreement(None) 119 | .active(true) 120 | .created(now.clone()) 121 | .updated(now.clone()) 122 | .build() 123 | .map_err(|e| Error::BuilderFailed(e))?; 124 | Ok(Self { 125 | user, 126 | member: Some(member), 127 | }) 128 | } 129 | } 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use super::*; 134 | use crate::{ 135 | util, 136 | }; 137 | 138 | #[test] 139 | fn systemic() { 140 | let user_id = UserID::create(); 141 | let now = util::time::now(); 142 | let voter = Vote::systemic(user_id.clone(), &now).unwrap(); 143 | assert_eq!(voter.user().id(), &user_id); 144 | assert_eq!(voter.user().roles(), &vec![Role::SuperAdmin]); 145 | assert_eq!(voter.user().active(), &true); 146 | assert_eq!(voter.user().created(), &now); 147 | assert_eq!(voter.user().updated(), &now); 148 | assert_eq!(voter.member(), &None); 149 | } 150 | 151 | #[test] 152 | fn company() { 153 | let now = util::time::now(); 154 | let user_id = UserID::create(); 155 | let member_id = MemberID::create(); 156 | let company_id = CompanyID::new("hairy larry's scrumptious dairies"); 157 | let voter = Vote::company(user_id.clone(), member_id.clone(), &company_id, &now).unwrap(); 158 | let user = voter.user().clone(); 159 | assert_eq!(user.id(), &user_id); 160 | assert_eq!(user.roles(), &vec![Role::User]); 161 | assert_eq!(user.active(), &true); 162 | assert_eq!(user.created(), &now); 163 | assert_eq!(user.updated(), &now); 164 | 165 | let member = voter.member().clone().unwrap(); 166 | assert_eq!(member.id(), &member_id); 167 | assert_eq!(member.inner().subject(), &user.agent_id()); 168 | assert_eq!(member.inner().object(), &company_id.clone().into()); 169 | match member.class() { 170 | MemberClass::User(_) => {} 171 | _ => panic!("voter::tests::company() -- bad class"), 172 | } 173 | assert_eq!(member.permissions(), &vec![CompanyPermission::All]); 174 | assert_eq!(member.active(), &true); 175 | assert_eq!(member.created(), &now); 176 | assert_eq!(member.updated(), &now); 177 | } 178 | } 179 | 180 | -------------------------------------------------------------------------------- /src/transactions/agreement.rs: -------------------------------------------------------------------------------- 1 | //! An agreement represents a grouping of commitments and events betwixt two 2 | //! agents. 3 | //! 4 | //! In other words, an agreement is basically an order. 5 | //! 6 | //! See the [agreement model.][1] 7 | //! 8 | //! [1]: ../../models/agreement/index.html 9 | 10 | use chrono::{DateTime, Utc}; 11 | use crate::{ 12 | access::Permission, 13 | error::{Error, Result}, 14 | models::{ 15 | Op, 16 | Modifications, 17 | lib::{ 18 | agent::AgentID, 19 | basis_model::Model, 20 | }, 21 | agreement::{Agreement, AgreementID}, 22 | company::{Company, Permission as CompanyPermission}, 23 | member::Member, 24 | user::User, 25 | }, 26 | }; 27 | use vf_rs::vf; 28 | 29 | /// Create a new agreement/order. 30 | /// 31 | /// When updating data connected to an agreement, only agents that are in the 32 | /// agreement's `participants` list will be allowed to complete updates. This 33 | /// makes it so only those involved in the agreement can modify it or any of its 34 | /// data in any way. 35 | pub fn create>(caller: &User, member: &Member, company: &Company, id: AgreementID, participants: Vec, name: T, note: T, created: Option>, active: bool, now: &DateTime) -> Result { 36 | caller.access_check(Permission::CompanyUpdateAgreements)?; 37 | member.access_check(caller.id(), company.id(), CompanyPermission::AgreementCreate)?; 38 | if !company.is_active() { 39 | Err(Error::ObjectIsInactive("company".into()))?; 40 | } 41 | let model = Agreement::builder() 42 | .id(id) 43 | .inner( 44 | vf::Agreement::builder() 45 | .created(created) 46 | .name(Some(name.into())) 47 | .note(Some(note.into())) 48 | .build() 49 | .map_err(|e| Error::BuilderFailed(e))? 50 | ) 51 | .participants(participants) 52 | .active(active) 53 | .created(now.clone()) 54 | .updated(now.clone()) 55 | .build() 56 | .map_err(|e| Error::BuilderFailed(e))?; 57 | Ok(Modifications::new_single(Op::Create, model)) 58 | } 59 | 60 | /// Update an agreement, including the participant list. 61 | pub fn update(caller: &User, member: &Member, company: &Company, mut subject: Agreement, participants: Option>, name: Option, note: Option, created: Option>>, active: Option, now: &DateTime) -> Result { 62 | caller.access_check(Permission::CompanyUpdateAgreements)?; 63 | member.access_check(caller.id(), company.id(), CompanyPermission::AgreementUpdate)?; 64 | if !company.is_active() { 65 | Err(Error::ObjectIsInactive("company".into()))?; 66 | } 67 | if let Some(participants) = participants { 68 | subject.set_participants(participants); 69 | } 70 | if let Some(created) = created { 71 | subject.inner_mut().set_created(created); 72 | } 73 | if let Some(name) = name { 74 | subject.inner_mut().set_name(Some(name)); 75 | } 76 | if let Some(note) = note { 77 | subject.inner_mut().set_note(Some(note)); 78 | } 79 | if let Some(active) = active { 80 | subject.set_active(active); 81 | } 82 | subject.set_updated(now.clone()); 83 | Ok(Modifications::new_single(Op::Update, subject)) 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use super::*; 89 | use crate::{ 90 | models::{ 91 | lib::agent::Agent, 92 | company::CompanyID, 93 | }, 94 | util::{self, test::{self, *}}, 95 | }; 96 | 97 | #[test] 98 | fn can_create() { 99 | let now = util::time::now(); 100 | let id = AgreementID::create(); 101 | let state = TestState::standard(vec![CompanyPermission::AgreementCreate], &now); 102 | let company_from = make_company(&CompanyID::create(), "jerry's widgets", &now); 103 | let participants = vec![state.company().agent_id(), company_from.agent_id()]; 104 | 105 | let testfn = |state: &TestState| { 106 | create(state.user(), state.member(), state.company(), id.clone(), participants.clone(), "order 1234141", "hi i'm jerry. just going to order some widgets. don't mind me, just ordering widgets.", Some(now.clone()), true, &now) 107 | }; 108 | test::standard_transaction_tests(&state, &testfn); 109 | 110 | let mods = testfn(&state).unwrap().into_vec(); 111 | assert_eq!(mods.len(), 1); 112 | 113 | let agreement = mods[0].clone().expect_op::(Op::Create).unwrap(); 114 | assert_eq!(agreement.id(), &id); 115 | assert_eq!(agreement.inner().created(), &Some(now.clone())); 116 | assert_eq!(agreement.inner().name(), &Some("order 1234141".into())); 117 | assert_eq!(agreement.inner().note(), &Some("hi i'm jerry. just going to order some widgets. don't mind me, just ordering widgets.".into())); 118 | assert_eq!(agreement.participants(), &participants); 119 | assert_eq!(agreement.active(), &true); 120 | assert_eq!(agreement.created(), &now); 121 | assert_eq!(agreement.updated(), &now); 122 | assert_eq!(agreement.deleted(), &None); 123 | } 124 | 125 | #[test] 126 | fn can_update() { 127 | let now = util::time::now(); 128 | let id = AgreementID::create(); 129 | let state = TestState::standard(vec![CompanyPermission::AgreementCreate, CompanyPermission::AgreementUpdate], &now); 130 | let company_from = make_company(&CompanyID::create(), "jerry's widgets", &now); 131 | let participants = vec![state.company().agent_id(), company_from.agent_id()]; 132 | 133 | let mods = create(state.user(), state.member(), state.company(), id.clone(), participants.clone(), "order 1234141", "hi i'm jerry. just going to order some widgets. don't mind me, just ordering widgets.", Some(now.clone()), true, &now).unwrap().into_vec(); 134 | let agreement1 = mods[0].clone().expect_op::(Op::Create).unwrap(); 135 | let now2 = util::time::now(); 136 | 137 | let testfn = |state: &TestState| { 138 | update(state.user(), state.member(), state.company(), agreement1.clone(), Some(vec![company_from.agent_id()]), Some("order 1111222".into()), Some("jerry's long-winded order".into()), None, None, &now2) 139 | }; 140 | test::standard_transaction_tests(&state, &testfn); 141 | 142 | let mods = testfn(&state).unwrap().into_vec(); 143 | let agreement2 = mods[0].clone().expect_op::(Op::Update).unwrap(); 144 | 145 | assert_eq!(agreement2.id(), agreement1.id()); 146 | assert_eq!(agreement2.inner().created(), agreement1.inner().created()); 147 | assert_eq!(agreement2.inner().name(), &Some("order 1111222".into())); 148 | assert_eq!(agreement2.inner().note(), &Some("jerry's long-winded order".into())); 149 | assert_eq!(agreement2.participants(), &vec![company_from.agent_id()]); 150 | assert_eq!(agreement2.active(), agreement1.active()); 151 | assert_eq!(agreement2.created(), agreement1.created()); 152 | assert_eq!(agreement2.updated(), &now2); 153 | assert_eq!(agreement2.deleted(), &None); 154 | } 155 | } 156 | 157 | -------------------------------------------------------------------------------- /src/transactions/currency.rs: -------------------------------------------------------------------------------- 1 | //! Currencies track real-world market currencies in the cost tracking system. 2 | //! 3 | //! This set of transactions deals with creating currencies tracked by Basis, 4 | //! such as USD, EUR, etc. 5 | //! 6 | //! See the [currency model.][1] 7 | //! 8 | //! [1]: ../../models/currency/index.html 9 | 10 | use chrono::{DateTime, Utc}; 11 | use crate::{ 12 | access::Permission, 13 | error::{Error, Result}, 14 | models::{ 15 | Op, 16 | Modifications, 17 | currency::{Currency, CurrencyID}, 18 | lib::basis_model::Model, 19 | user::User, 20 | }, 21 | }; 22 | 23 | /// Create a new `Currency`. 24 | pub fn create>(caller: &User, id: CurrencyID, name: T, decimal_places: u8, active: bool, now: &DateTime) -> Result { 25 | caller.access_check(Permission::CurrencyCreate)?; 26 | let model = Currency::builder() 27 | .id(id) 28 | .name(name.into()) 29 | .decimal_places(decimal_places) 30 | .active(active) 31 | .created(now.clone()) 32 | .updated(now.clone()) 33 | .build() 34 | .map_err(|e| Error::BuilderFailed(e))?; 35 | Ok(Modifications::new_single(Op::Create, model)) 36 | } 37 | 38 | /// Update an existing `Currency` 39 | pub fn update(caller: &User, mut subject: Currency, name: Option, decimal_places: Option, active: Option, now: &DateTime) -> Result { 40 | caller.access_check(Permission::CurrencyUpdate)?; 41 | if let Some(name) = name { 42 | subject.set_name(name); 43 | } 44 | if let Some(decimal_places) = decimal_places { 45 | subject.set_decimal_places(decimal_places); 46 | } 47 | if let Some(active) = active { 48 | subject.set_active(active); 49 | } 50 | subject.set_updated(now.clone()); 51 | Ok(Modifications::new_single(Op::Update, subject)) 52 | } 53 | 54 | /// Delete a `Currency` 55 | pub fn delete(caller: &User, mut subject: Currency, now: &DateTime) -> Result { 56 | caller.access_check(Permission::CurrencyDelete)?; 57 | if subject.is_deleted() { 58 | Err(Error::ObjectIsDeleted("currency".into()))?; 59 | } 60 | subject.set_deleted(Some(now.clone())); 61 | Ok(Modifications::new_single(Op::Delete, subject)) 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | use crate::{ 68 | access::Role, 69 | models::{ 70 | Op, 71 | 72 | currency::Currency, 73 | }, 74 | util::{self, test::{self, *}}, 75 | }; 76 | 77 | #[test] 78 | fn can_create() { 79 | let id = CurrencyID::create(); 80 | let now = util::time::now(); 81 | let mut state = TestState::standard(vec![], &now); 82 | state.user_mut().set_roles(vec![Role::SuperAdmin]); 83 | 84 | let testfn = |state: &TestState| { 85 | create(state.user(), id.clone(), "usd", 2, true, &now) 86 | }; 87 | 88 | let mods = testfn(&state).unwrap().into_vec(); 89 | assert_eq!(mods.len(), 1); 90 | 91 | let currency = mods[0].clone().expect_op::(Op::Create).unwrap(); 92 | assert_eq!(currency.id(), &id); 93 | assert_eq!(currency.name(), "usd"); 94 | assert_eq!(currency.decimal_places(), &2); 95 | assert_eq!(currency.active(), &true); 96 | assert_eq!(currency.created(), &now); 97 | assert_eq!(currency.updated(), &now); 98 | assert_eq!(currency.deleted(), &None); 99 | 100 | let mut state2 = state.clone(); 101 | state2.user_mut().set_roles(vec![Role::User]); 102 | let res = testfn(&state2); 103 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 104 | } 105 | 106 | #[test] 107 | fn can_update() { 108 | let id = CurrencyID::create(); 109 | let now = util::time::now(); 110 | let mut state = TestState::standard(vec![], &now); 111 | state.user_mut().set_roles(vec![Role::SuperAdmin]); 112 | let mods = create(state.user(), id.clone(), "usdz", 1, false, &now).unwrap().into_vec(); 113 | let currency = mods[0].clone().expect_op::(Op::Create).unwrap(); 114 | state.model = Some(currency); 115 | 116 | let now2 = util::time::now(); 117 | let testfn = |state: &TestState| { 118 | update(state.user(), state.model().clone(), Some("usd".into()), Some(2), Some(true), &now2) 119 | }; 120 | 121 | // not truly an update but ok 122 | let mods = testfn(&state).unwrap().into_vec(); 123 | let currency2 = mods[0].clone().expect_op::(Op::Update).unwrap(); 124 | assert_eq!(currency2.id(), state.model().id()); 125 | assert_eq!(currency2.name(), "usd"); 126 | assert_eq!(currency2.decimal_places(), &2); 127 | assert_eq!(currency2.active(), &true); 128 | assert_eq!(currency2.created(), state.model().created()); 129 | assert_eq!(currency2.created(), &now); 130 | assert_eq!(currency2.updated(), &now2); 131 | assert_eq!(currency2.deleted(), &None); 132 | 133 | let mut state2 = state.clone(); 134 | state2.user_mut().set_roles(vec![Role::User]); 135 | let res = testfn(&state2); 136 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 137 | } 138 | 139 | #[test] 140 | fn can_delete() { 141 | let id = CurrencyID::create(); 142 | let now = util::time::now(); 143 | let mut state = TestState::standard(vec![], &now); 144 | state.user_mut().set_roles(vec![Role::SuperAdmin]); 145 | let mods = create(state.user(), id.clone(), "usd", 2, true, &now).unwrap().into_vec(); 146 | let currency = mods[0].clone().expect_op::(Op::Create).unwrap(); 147 | state.model = Some(currency); 148 | 149 | let now2 = util::time::now(); 150 | let testfn = |state: &TestState| { 151 | delete(state.user(), state.model().clone(), &now2) 152 | }; 153 | test::double_deleted_tester(&state, "currency", &testfn); 154 | 155 | let mods = testfn(&state).unwrap().into_vec(); 156 | assert_eq!(mods.len(), 1); 157 | let currency2 = mods[0].clone().expect_op::(Op::Delete).unwrap(); 158 | assert_eq!(currency2.id(), state.model().id()); 159 | assert_eq!(currency2.name(), "usd"); 160 | assert_eq!(currency2.decimal_places(), &2); 161 | assert_eq!(currency2.active(), &true); 162 | assert_eq!(currency2.created(), state.model().created()); 163 | assert_eq!(currency2.updated(), state.model().updated()); 164 | assert_eq!(currency2.deleted(), &Some(now2)); 165 | 166 | let mut state2 = state.clone(); 167 | state2.user_mut().set_roles(vec![Role::User]); 168 | let res = testfn(&state2); 169 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 170 | } 171 | } 172 | 173 | -------------------------------------------------------------------------------- /src/transactions/event/delivery.rs: -------------------------------------------------------------------------------- 1 | //! Delivery is about physically moving resources between agents. 2 | //! 3 | //! For instance, if shipping a box of widgets between two companies, a shipping 4 | //! company would use the actions in this module to describe the process and 5 | //! account for the costs along the way. 6 | 7 | use chrono::{DateTime, Utc}; 8 | use crate::{ 9 | access::Permission, 10 | costs::Costs, 11 | error::{Error, Result}, 12 | models::{ 13 | Op, 14 | Modifications, 15 | event::{Event, EventID, EventProcessState}, 16 | company::{Company, Permission as CompanyPermission}, 17 | member::Member, 18 | lib::basis_model::Model, 19 | process::Process, 20 | resource::Resource, 21 | user::User, 22 | }, 23 | util::number::Ratio, 24 | }; 25 | use vf_rs::{vf, geo::SpatialThing}; 26 | 27 | /// Signifies that a delivery has been dropped off at the desired location. Note 28 | /// that custody remains with the deliverer until a `transfer-custody` event is 29 | /// created. 30 | /// 31 | /// This operates on a whole resource. 32 | pub fn dropoff(caller: &User, member: &Member, company: &Company, id: EventID, process: Process, resource: Resource, move_costs_ratio: Ratio, new_location: Option, note: Option, now: &DateTime) -> Result { 33 | caller.access_check(Permission::EventCreate)?; 34 | member.access_check(caller.id(), company.id(), CompanyPermission::Dropoff)?; 35 | if !company.is_active() { 36 | Err(Error::ObjectIsInactive("company".into()))?; 37 | } 38 | 39 | let process_id = process.id().clone(); 40 | let resource_id = resource.id().clone(); 41 | let move_costs = process.costs().clone() * move_costs_ratio; 42 | 43 | let state = EventProcessState::builder() 44 | .output_of(process) 45 | .resource(resource) 46 | .build() 47 | .map_err(|e| Error::BuilderFailed(e))?; 48 | let event = Event::builder() 49 | .id(id) 50 | .inner( 51 | vf::EconomicEvent::builder() 52 | .action(vf::Action::Dropoff) 53 | .at_location(new_location) 54 | .has_point_in_time(now.clone()) 55 | .note(note) 56 | .output_of(Some(process_id)) 57 | .provider(company.id().clone()) 58 | .receiver(company.id().clone()) 59 | .resource_inventoried_as(Some(resource_id)) 60 | .build() 61 | .map_err(|e| Error::BuilderFailed(e))? 62 | ) 63 | .move_costs(Some(move_costs)) 64 | .active(true) 65 | .created(now.clone()) 66 | .updated(now.clone()) 67 | .build() 68 | .map_err(|e| Error::BuilderFailed(e))?; 69 | 70 | let evmods = event.process(state, now)?.into_vec(); 71 | let mut mods = Modifications::new(); 72 | mods.push(Op::Create, event); 73 | for evmod in evmods { 74 | mods.push_raw(evmod); 75 | } 76 | Ok(mods) 77 | } 78 | 79 | /// Signifies that a delivery has been picked up from its origin. Note that 80 | /// custody must have been transfered to the picker-upper previously (via the 81 | /// `transfer-custody` event). 82 | /// 83 | /// This operates on a whole resource. 84 | pub fn pickup(caller: &User, member: &Member, company: &Company, id: EventID, resource: Resource, process: Process, note: Option, now: &DateTime) -> Result { 85 | caller.access_check(Permission::EventCreate)?; 86 | member.access_check(caller.id(), company.id(), CompanyPermission::Pickup)?; 87 | if !company.is_active() { 88 | Err(Error::ObjectIsInactive("company".into()))?; 89 | } 90 | 91 | let process_id = process.id().clone(); 92 | let resource_id = resource.id().clone(); 93 | 94 | let state = EventProcessState::builder() 95 | .input_of(process) 96 | .resource(resource) 97 | .build() 98 | .map_err(|e| Error::BuilderFailed(e))?; 99 | let event = Event::builder() 100 | .id(id) 101 | .inner( 102 | vf::EconomicEvent::builder() 103 | .action(vf::Action::Pickup) 104 | .has_point_in_time(now.clone()) 105 | .input_of(Some(process_id)) 106 | .note(note) 107 | .provider(company.id().clone()) 108 | .receiver(company.id().clone()) 109 | .resource_inventoried_as(Some(resource_id)) 110 | .build() 111 | .map_err(|e| Error::BuilderFailed(e))? 112 | ) 113 | .move_costs(Some(Costs::new())) 114 | .active(true) 115 | .created(now.clone()) 116 | .updated(now.clone()) 117 | .build() 118 | .map_err(|e| Error::BuilderFailed(e))?; 119 | 120 | let evmods = event.process(state, now)?.into_vec(); 121 | let mut mods = Modifications::new(); 122 | mods.push(Op::Create, event); 123 | for evmod in evmods { 124 | mods.push_raw(evmod); 125 | } 126 | Ok(mods) 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use super::*; 132 | use crate::{ 133 | models::{ 134 | company::CompanyID, 135 | event::{EventError, EventID}, 136 | lib::agent::Agent, 137 | occupation::OccupationID, 138 | process::ProcessID, 139 | resource::ResourceID, 140 | }, 141 | util::{self, test::{self, *}}, 142 | }; 143 | use om2::{Measure, Unit}; 144 | 145 | #[test] 146 | fn can_dropoff() { 147 | let now = util::time::now(); 148 | let id = EventID::create(); 149 | let mut state = TestState::standard(vec![CompanyPermission::Dropoff], &now); 150 | let occupation_id = OccupationID::new("trucker"); 151 | let costs = Costs::new_with_labor(occupation_id.clone(), num!(42.2)); 152 | let process = make_process(&ProcessID::create(), state.company().id(), "deliver widgets", &costs, &now); 153 | let resource = make_resource(&ResourceID::new("widget"), state.company().id(), &Measure::new(num!(15), Unit::One), &Costs::new_with_labor("machinist", 157), &now); 154 | let move_costs_ratio = Ratio::new(1).unwrap(); 155 | state.model = Some(process); 156 | state.model2 = Some(resource); 157 | 158 | let testfn = |state: &TestState| { 159 | dropoff(state.user(), state.member(), state.company(), id.clone(), state.model().clone(), state.model2().clone(), move_costs_ratio.clone(), Some(state.loc().clone()), Some("memo".into()), &now) 160 | }; 161 | test::standard_transaction_tests(&state, &testfn); 162 | 163 | let mods = testfn(&state).unwrap().into_vec(); 164 | assert_eq!(mods.len(), 3); 165 | let event = mods[0].clone().expect_op::(Op::Create).unwrap(); 166 | let process2 = mods[1].clone().expect_op::(Op::Update).unwrap(); 167 | let resource2 = mods[2].clone().expect_op::(Op::Update).unwrap(); 168 | 169 | assert_eq!(event.id(), &id); 170 | assert_eq!(event.inner().agreed_in(), &None); 171 | assert_eq!(event.inner().has_point_in_time(), &Some(now.clone())); 172 | assert_eq!(event.inner().input_of(), &None); 173 | assert_eq!(event.inner().output_of(), &Some(state.model().id().clone())); 174 | assert_eq!(event.inner().provider().clone(), state.company().agent_id()); 175 | assert_eq!(event.inner().receiver().clone(), state.company().agent_id()); 176 | assert_eq!(event.move_costs(), &Some(state.model().costs().clone())); 177 | assert_eq!(event.active(), &true); 178 | assert_eq!(event.created(), &now); 179 | assert_eq!(event.updated(), &now); 180 | 181 | assert_eq!(process2.id(), state.model().id()); 182 | assert_eq!(process2.company_id(), state.company().id()); 183 | assert_eq!(process2.inner().name(), "deliver widgets"); 184 | assert_eq!(process2.costs(), &Costs::new()); 185 | 186 | let mut costs2 = Costs::new(); 187 | costs2.track_labor(occupation_id.clone(), num!(42.2)); 188 | costs2.track_labor("machinist", 157); 189 | assert_eq!(resource2.id(), state.model2().id()); 190 | assert_eq!(resource2.inner().primary_accountable(), &Some(state.company().agent_id())); 191 | assert_eq!(resource2.in_custody_of(), &state.company().agent_id()); 192 | assert_eq!(resource2.inner().accounting_quantity(), &Some(Measure::new(num!(15), Unit::One))); 193 | assert_eq!(event.inner().note(), &Some("memo".into())); 194 | assert_eq!(resource2.inner().onhand_quantity(), &Some(Measure::new(num!(15), Unit::One))); 195 | assert_eq!(resource2.inner().current_location(), &Some(state.loc().clone())); 196 | assert_eq!(resource2.costs(), &costs2); 197 | 198 | // can't dropoff from a process you don't own 199 | let mut state2 = state.clone(); 200 | state2.model_mut().set_company_id(CompanyID::new("zing")); 201 | let res = testfn(&state2); 202 | assert_eq!(res, Err(Error::Event(EventError::ProcessOwnerMismatch))); 203 | 204 | let mut state3 = state.clone(); 205 | state3.model2_mut().inner_mut().set_primary_accountable(Some(CompanyID::new("ziggy").into())); 206 | let res = testfn(&state3); 207 | assert!(res.is_ok()); 208 | 209 | // a company that doesn't have posession of a resource can't drop it off 210 | let mut state4 = state.clone(); 211 | state4.model2_mut().set_in_custody_of(CompanyID::new("ziggy").into()); 212 | let res = testfn(&state4); 213 | assert_eq!(res, Err(Error::Event(EventError::ResourceCustodyMismatch))); 214 | } 215 | 216 | #[test] 217 | fn can_pickup() { 218 | let now = util::time::now(); 219 | let id = EventID::create(); 220 | let mut state = TestState::standard(vec![CompanyPermission::Pickup], &now); 221 | let resource = make_resource(&ResourceID::new("widget"), state.company().id(), &Measure::new(num!(15), Unit::One), &Costs::new_with_labor("homemaker", 157), &now); 222 | let process = make_process(&ProcessID::create(), state.company().id(), "make widgets", &Costs::new(), &now); 223 | state.model = Some(resource); 224 | state.model2 = Some(process); 225 | 226 | let testfn = |state: &TestState| { 227 | pickup(state.user(), state.member(), state.company(), id.clone(), state.model().clone(), state.model2().clone(), Some("memo".into()), &now) 228 | }; 229 | test::standard_transaction_tests(&state, &testfn); 230 | 231 | let mods = testfn(&state).unwrap().into_vec(); 232 | assert_eq!(mods.len(), 1); 233 | let event = mods[0].clone().expect_op::(Op::Create).unwrap(); 234 | 235 | assert_eq!(event.id(), &id); 236 | assert_eq!(event.inner().agreed_in(), &None); 237 | assert_eq!(event.inner().has_point_in_time(), &Some(now.clone())); 238 | assert_eq!(event.inner().input_of(), &Some(state.model2().id().clone())); 239 | assert_eq!(event.inner().note(), &Some("memo".into())); 240 | assert_eq!(event.inner().provider().clone(), state.company().agent_id()); 241 | assert_eq!(event.inner().receiver().clone(), state.company().agent_id()); 242 | assert_eq!(event.move_costs(), &Some(Costs::new())); 243 | assert_eq!(event.active(), &true); 244 | assert_eq!(event.created(), &now); 245 | assert_eq!(event.updated(), &now); 246 | 247 | let mut state2 = state.clone(); 248 | state2.model2_mut().set_company_id(CompanyID::new("zing")); 249 | let res = testfn(&state2); 250 | assert_eq!(res, Err(Error::Event(EventError::ProcessOwnerMismatch))); 251 | 252 | let mut state3 = state.clone(); 253 | state3.model_mut().inner_mut().set_primary_accountable(Some(CompanyID::new("ziggy").into())); 254 | let res = testfn(&state3); 255 | assert!(res.is_ok()); 256 | 257 | // a company that doesn't have posession of a resource can't pick it up 258 | let mut state4 = state.clone(); 259 | state4.model_mut().set_in_custody_of(CompanyID::new("ziggy").into()); 260 | let res = testfn(&state4); 261 | assert_eq!(res, Err(Error::Event(EventError::ResourceCustodyMismatch))); 262 | } 263 | } 264 | 265 | -------------------------------------------------------------------------------- /src/transactions/event/mod.rs: -------------------------------------------------------------------------------- 1 | //! Events are what move costs/resources through the system. 2 | //! 3 | //! See the [event model.][1] 4 | //! 5 | //! [1]: ../../models/event/index.html 6 | 7 | use crate::{ 8 | models::{ 9 | resource::{ResourceID, Resource}, 10 | }, 11 | }; 12 | #[cfg(feature = "with_serde")] 13 | use serde::{Serialize, Deserialize}; 14 | 15 | /// Helps us signify whether we want an operation that moves a resource from one 16 | /// place to another to a) create a new resource copied from the original or b) 17 | /// update a pre-existing resource. 18 | /// 19 | /// This is used mainly for the move, transfer, transfer-all-rights, and 20 | /// transfer-custody events. 21 | #[derive(Debug, Clone, PartialEq)] 22 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 23 | pub enum ResourceMover { 24 | /// Create a new resource using the given ID 25 | Create(ResourceID), 26 | /// Update the given resource 27 | Update(Resource), 28 | } 29 | 30 | pub mod accounting; 31 | pub mod delivery; 32 | pub mod production; 33 | pub mod modification; 34 | pub mod service; 35 | pub mod transfer; 36 | pub mod work; 37 | 38 | -------------------------------------------------------------------------------- /src/transactions/event/service.rs: -------------------------------------------------------------------------------- 1 | //! Services are processes that require labor but don't create resources. 2 | //! 3 | //! For instance, delivering a package, providing healthcare, or providing legal 4 | //! advice are all services. 5 | 6 | use chrono::{DateTime, Utc}; 7 | use crate::{ 8 | access::Permission, 9 | error::{Error, Result}, 10 | models::{ 11 | Op, 12 | Modifications, 13 | agreement::Agreement, 14 | event::{Event, EventID, EventProcessState}, 15 | company::{Company, Permission as CompanyPermission}, 16 | member::Member, 17 | lib::{ 18 | agent::Agent, 19 | basis_model::Model, 20 | }, 21 | process::Process, 22 | user::User, 23 | }, 24 | util::number::Ratio, 25 | }; 26 | use url::Url; 27 | use vf_rs::vf; 28 | 29 | /// Provide a service to another agent, moving costs along the way. 30 | pub fn deliver_service(caller: &User, member: &Member, company_from: &Company, company_to: &Company, agreement: &Agreement, id: EventID, process_from: Process, process_to: Process, move_costs_ratio: Ratio, agreed_in: Option, note: Option, now: &DateTime) -> Result { 31 | caller.access_check(Permission::EventCreate)?; 32 | member.access_check(caller.id(), company_from.id(), CompanyPermission::DeliverService)?; 33 | if !company_from.is_active() { 34 | Err(Error::ObjectIsInactive("company".into()))?; 35 | } 36 | if !company_to.is_active() { 37 | Err(Error::ObjectIsInactive("company".into()))?; 38 | } 39 | if !agreement.has_participant(&company_from.agent_id()) || !agreement.has_participant(&company_from.agent_id()) { 40 | // can't create an event for an agreement you are not party to 41 | Err(Error::InsufficientPrivileges)?; 42 | } 43 | 44 | let process_from_id = process_from.id().clone(); 45 | let process_to_id = process_to.id().clone(); 46 | let move_costs = process_from.costs().clone() * move_costs_ratio; 47 | 48 | let state = EventProcessState::builder() 49 | .output_of(process_from) 50 | .input_of(process_to) 51 | .build() 52 | .map_err(|e| Error::BuilderFailed(e))?; 53 | let event = Event::builder() 54 | .id(id) 55 | .inner( 56 | vf::EconomicEvent::builder() 57 | .action(vf::Action::DeliverService) 58 | .agreed_in(agreed_in) 59 | .has_point_in_time(now.clone()) 60 | .input_of(Some(process_to_id)) 61 | .note(note) 62 | .provider(company_from.id().clone()) 63 | .realization_of(Some(agreement.id().clone())) 64 | .receiver(company_to.id().clone()) 65 | .output_of(Some(process_from_id)) 66 | .build() 67 | .map_err(|e| Error::BuilderFailed(e))? 68 | ) 69 | .move_costs(Some(move_costs)) 70 | .active(true) 71 | .created(now.clone()) 72 | .updated(now.clone()) 73 | .build() 74 | .map_err(|e| Error::BuilderFailed(e))?; 75 | 76 | let evmods = event.process(state, now)?.into_vec(); 77 | let mut mods = Modifications::new(); 78 | mods.push(Op::Create, event); 79 | for evmod in evmods { 80 | mods.push_raw(evmod); 81 | } 82 | Ok(mods) 83 | } 84 | 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use super::*; 89 | use crate::{ 90 | costs::Costs, 91 | models::{ 92 | agreement::AgreementID, 93 | company::CompanyID, 94 | event::{EventID, EventError}, 95 | lib::agent::Agent, 96 | occupation::OccupationID, 97 | process::{Process, ProcessID}, 98 | }, 99 | util::{self, test::{self, *}}, 100 | }; 101 | 102 | #[test] 103 | fn can_deliver_service() { 104 | let now = util::time::now(); 105 | let id = EventID::create(); 106 | let mut state = TestState::standard(vec![CompanyPermission::DeliverService], &now); 107 | let company_from = state.company().clone(); 108 | let company_to = make_company(&CompanyID::create(), "jinkey's skateboards", &now); 109 | let agreement = make_agreement(&AgreementID::create(), &vec![company_from.agent_id(), company_to.agent_id()], "order 1234", "gotta make some planks", &now); 110 | let agreed_in: Url = "https://legalzoom.com/my-dad-is-suing-your-dad-the-agreement".parse().unwrap(); 111 | let occupation_id = OccupationID::new("lawyer"); 112 | let process_from = make_process(&ProcessID::create(), company_from.id(), "various lawyerings", &Costs::new_with_labor(occupation_id.clone(), num!(177.25)), &now); 113 | let process_to = make_process(&ProcessID::create(), company_to.id(), "employee legal agreement drafting", &Costs::new_with_labor(occupation_id.clone(), num!(804)), &now); 114 | let move_costs_ratio = Ratio::new(num!(0.777777777)).unwrap(); 115 | let costs_to_move = process_from.costs().clone() * move_costs_ratio.clone(); 116 | state.model = Some(process_from); 117 | state.model2 = Some(process_to); 118 | 119 | let testfn_inner = |state: &TestState, company_from: &Company, company_to: &Company, agreement: &Agreement| { 120 | deliver_service(state.user(), state.member(), company_from, company_to, agreement, id.clone(), state.model().clone(), state.model2().clone(), move_costs_ratio.clone(), Some(agreed_in.clone()), Some("making planks lol".into()), &now) 121 | }; 122 | let testfn_from = |state: &TestState| { 123 | testfn_inner(state, state.company(), &company_to, &agreement) 124 | }; 125 | let testfn_to = |state: &TestState| { 126 | testfn_inner(state, &company_from, state.company(), &agreement) 127 | }; 128 | test::standard_transaction_tests(&state, &testfn_from); 129 | 130 | let mods = testfn_from(&state).unwrap().into_vec(); 131 | assert_eq!(mods.len(), 3); 132 | let event = mods[0].clone().expect_op::(Op::Create).unwrap(); 133 | let process_from2 = mods[1].clone().expect_op::(Op::Update).unwrap(); 134 | let process_to2 = mods[2].clone().expect_op::(Op::Update).unwrap(); 135 | 136 | assert_eq!(event.id(), &id); 137 | assert_eq!(event.inner().agreed_in(), &Some(agreed_in.clone())); 138 | assert_eq!(event.inner().has_point_in_time(), &Some(now.clone())); 139 | assert_eq!(event.inner().input_of(), &Some(state.model2().id().clone())); 140 | assert_eq!(event.inner().note(), &Some("making planks lol".into())); 141 | assert_eq!(event.inner().output_of(), &Some(state.model().id().clone())); 142 | assert_eq!(event.inner().provider().clone(), company_from.agent_id()); 143 | assert_eq!(event.inner().realization_of(), &Some(agreement.id().clone())); 144 | assert_eq!(event.inner().receiver().clone(), company_to.agent_id()); 145 | assert_eq!(event.inner().resource_quantity(), &None); 146 | assert_eq!(event.move_costs(), &Some(costs_to_move.clone())); 147 | assert_eq!(event.active(), &true); 148 | assert_eq!(event.created(), &now); 149 | assert_eq!(event.updated(), &now); 150 | 151 | assert_eq!(process_from2.id(), state.model().id()); 152 | assert_eq!(process_from2.company_id(), company_from.id()); 153 | assert_eq!(process_from2.costs(), &(state.model().costs().clone() - costs_to_move.clone())); 154 | 155 | assert_eq!(process_to2.id(), state.model2().id()); 156 | assert_eq!(process_to2.company_id(), company_to.id()); 157 | assert_eq!(process_to2.costs(), &(state.model2().costs().clone() + costs_to_move.clone())); 158 | 159 | // can't move costs from a process you don't own 160 | let mut state2 = state.clone(); 161 | state2.model_mut().set_company_id(CompanyID::new("zing").into()); 162 | let res = testfn_from(&state2); 163 | assert_eq!(res, Err(Error::Event(EventError::ProcessOwnerMismatch))); 164 | 165 | // can't move costs into a process company_to doesnt own 166 | let mut state3 = state.clone(); 167 | state3.model2_mut().set_company_id(CompanyID::new("zing").into()); 168 | let res = testfn_from(&state3); 169 | assert_eq!(res, Err(Error::Event(EventError::ProcessOwnerMismatch))); 170 | 171 | // can't add an event unless both parties are participants in the agreement 172 | let mut agreement2 = agreement.clone(); 173 | agreement2.set_participants(vec![company_to.agent_id()]); 174 | let res = testfn_inner(&state, &company_from, &company_to, &agreement2); 175 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 176 | 177 | let mut state5 = state.clone(); 178 | state5.company = Some(company_to.clone()); 179 | test::deleted_company_tester(&state5, &testfn_to); 180 | } 181 | } 182 | 183 | -------------------------------------------------------------------------------- /src/transactions/event/work.rs: -------------------------------------------------------------------------------- 1 | //! Work events record labor in the system. They are how labor costs (both waged 2 | //! and hourly labor) get attributed to processes (and as a result resources). 3 | //! They also act as the systemic marker for paying company members. Record 4 | //! labor, get paid. 5 | 6 | use chrono::{DateTime, Utc}; 7 | use crate::{ 8 | access::Permission, 9 | costs::Costs, 10 | error::{Error, Result}, 11 | models::{ 12 | Op, 13 | Modifications, 14 | event::{Event, EventID, EventProcessState}, 15 | company::{Company, Permission as CompanyPermission}, 16 | member::Member, 17 | lib::basis_model::Model, 18 | process::Process, 19 | user::User, 20 | }, 21 | }; 22 | use om2::{Measure, Unit}; 23 | use rust_decimal::prelude::*; 24 | use vf_rs::vf; 25 | 26 | /// Create a new work event with the option of passing hourly data, wage data, 27 | /// or both. 28 | /// 29 | /// Most of the time you'll want to pass both wage (`wage_cost`) and hourly 30 | /// (`begin`/`end`) data together, unless you're truly tracking them separately. 31 | /// Sometimes you might not know or care to track detailed hourly data (as with 32 | /// salary) but it can be estimated to some extent using data in the worker's 33 | /// Member record. 34 | /// 35 | /// Note that this creates a full work event with a defined start and end. This 36 | /// function cannot create pending work events. 37 | pub fn work(caller: &User, member: &Member, company: &Company, id: EventID, worker: Member, process: Process, wage_cost: Option, begin: DateTime, end: DateTime, note: Option, now: &DateTime) -> Result { 38 | caller.access_check(Permission::EventCreate)?; 39 | // if we're recording our own work event, we can just check the regular 40 | // `Work` permission, otherwise we need admin privs 41 | if member.id() == worker.id() { 42 | member.access_check(caller.id(), company.id(), CompanyPermission::Work)?; 43 | } else { 44 | member.access_check(caller.id(), company.id(), CompanyPermission::WorkAdmin)?; 45 | } 46 | if !company.is_active() { 47 | Err(Error::ObjectIsInactive("company".into()))?; 48 | } 49 | 50 | let effort = { 51 | let milliseconds = end.timestamp_millis() - begin.timestamp_millis(); 52 | let hours = Decimal::from(milliseconds) / Decimal::from(1000 * 60 * 60); 53 | Measure::new(hours, Unit::Hour) 54 | }; 55 | let occupation_id = worker.occupation_id().ok_or(Error::MemberMustBeWorker)?.clone(); 56 | let costs = match wage_cost { 57 | Some(val) => Costs::new_with_labor(occupation_id, val), 58 | None => Costs::new(), 59 | }; 60 | let process_id = process.id().clone(); 61 | let member_id = worker.id().clone(); 62 | let agreement = worker.agreement().clone(); 63 | 64 | let state = EventProcessState::builder() 65 | .input_of(process) 66 | .provider(worker) 67 | .build() 68 | .map_err(|e| Error::BuilderFailed(e))?; 69 | let event = Event::builder() 70 | .id(id) 71 | .inner( 72 | vf::EconomicEvent::builder() 73 | .action(vf::Action::Work) 74 | .agreed_in(agreement) 75 | .effort_quantity(Some(effort)) 76 | .has_beginning(Some(begin)) 77 | .has_end(Some(end)) 78 | .input_of(Some(process_id)) 79 | .note(note) 80 | .provider(member_id) 81 | .receiver(company.id().clone()) 82 | .build() 83 | .map_err(|e| Error::BuilderFailed(e))? 84 | ) 85 | .move_costs(Some(costs)) 86 | .active(true) 87 | .created(now.clone()) 88 | .updated(now.clone()) 89 | .build() 90 | .map_err(|e| Error::BuilderFailed(e))?; 91 | let evmods = event.process(state, now)?.into_vec(); 92 | let mut mods = Modifications::new(); 93 | mods.push(Op::Create, event); 94 | for evmod in evmods { 95 | mods.push_raw(evmod); 96 | } 97 | Ok(mods) 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use super::*; 103 | use crate::{ 104 | models::{ 105 | company::CompanyID, 106 | member::*, 107 | event::{Event, EventID, EventError}, 108 | lib::agent::Agent, 109 | process::ProcessID, 110 | }, 111 | util::test::{self, *}, 112 | }; 113 | 114 | #[test] 115 | fn can_work() { 116 | let now: DateTime = "2018-06-06T00:00:00Z".parse().unwrap(); 117 | let now2: DateTime = "2018-06-06T06:52:00Z".parse().unwrap(); 118 | let id = EventID::create(); 119 | let mut state = TestState::standard(vec![CompanyPermission::Work], &now); 120 | let occupation_id = state.member().occupation_id().unwrap().clone(); 121 | let worker = state.member().clone(); 122 | let process = make_process(&ProcessID::create(), state.company().id(), "make widgets", &Costs::new_with_labor(occupation_id.clone(), num!(177.5)), &now); 123 | state.model = Some(worker); 124 | state.model2 = Some(process); 125 | 126 | let testfn = |state: &TestState| { 127 | work(state.user(), state.member(), state.company(), id.clone(), state.model().clone(), state.model2().clone(), Some(num!(78.4)), now.clone(), now2.clone(), Some("just doing some work".into()), &now2) 128 | }; 129 | test::standard_transaction_tests(&state, &testfn); 130 | 131 | let mods = testfn(&state).unwrap().into_vec(); 132 | assert_eq!(mods.len(), 2); 133 | let event = mods[0].clone().expect_op::(Op::Create).unwrap(); 134 | 135 | assert_eq!(event.id(), &id); 136 | assert_eq!(event.inner().agreed_in(), state.member().agreement()); 137 | assert_eq!(event.inner().has_beginning(), &Some(now.clone())); 138 | assert_eq!(event.inner().has_end(), &Some(now2.clone())); 139 | assert_eq!(event.inner().input_of(), &Some(state.model2().id().clone())); 140 | assert_eq!(event.inner().note(), &Some("just doing some work".into())); 141 | assert_eq!(event.inner().provider().clone(), state.model().agent_id()); 142 | assert_eq!(event.inner().receiver().clone(), state.company().agent_id()); 143 | assert_eq!(event.move_costs(), &Some(Costs::new_with_labor(occupation_id.clone(), num!(78.4)))); 144 | assert_eq!(event.active(), &true); 145 | assert_eq!(event.created(), &now2); 146 | assert_eq!(event.updated(), &now2); 147 | 148 | let mut costs2 = Costs::new(); 149 | costs2.track_labor(occupation_id.clone(), num!(177.5) + num!(78.4)); 150 | costs2.track_labor_hours(occupation_id.clone(), num!(6.8666666666666666666666666666)); 151 | let process2 = mods[1].clone().expect_op::(Op::Update).unwrap(); 152 | assert_eq!(process2.id(), state.model2().id()); 153 | assert_eq!(process2.company_id(), state.company().id()); 154 | assert_eq!(process2.inner().name(), "make widgets"); 155 | assert_eq!(process2.costs(), &costs2); 156 | 157 | // test worker != member 158 | let mut state2 = state.clone(); 159 | state2.model_mut().set_id(MemberID::create()); 160 | let res = testfn(&state2); 161 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 162 | state2.member_mut().set_permissions(vec![CompanyPermission::WorkAdmin]); 163 | let mods = testfn(&state2).unwrap().into_vec(); 164 | assert_eq!(mods.len(), 2); 165 | let event = mods[0].clone().expect_op::(Op::Create).unwrap(); 166 | 167 | assert_eq!(event.id(), &id); 168 | assert_eq!(event.inner().agreed_in(), state2.member().agreement()); 169 | assert_eq!(event.inner().has_beginning(), &Some(now.clone())); 170 | assert_eq!(event.inner().has_end(), &Some(now2.clone())); 171 | assert_eq!(event.inner().input_of(), &Some(state.model2().id().clone())); 172 | assert_eq!(event.inner().note(), &Some("just doing some work".into())); 173 | assert_eq!(event.inner().provider().clone(), state2.model().agent_id()); 174 | assert_eq!(event.inner().receiver().clone(), state.company().agent_id()); 175 | assert_eq!(event.move_costs(), &Some(Costs::new_with_labor(occupation_id.clone(), num!(78.4)))); 176 | assert_eq!(event.active(), &true); 177 | assert_eq!(event.created(), &now2); 178 | assert_eq!(event.updated(), &now2); 179 | 180 | let mut costs2 = Costs::new(); 181 | costs2.track_labor(occupation_id.clone(), num!(177.5) + num!(78.4)); 182 | costs2.track_labor_hours(occupation_id.clone(), num!(6.8666666666666666666666666666)); 183 | let process2 = mods[1].clone().expect_op::(Op::Update).unwrap(); 184 | 185 | assert_eq!(process2.id(), state.model2().id()); 186 | assert_eq!(process2.company_id(), state.company().id()); 187 | assert_eq!(process2.inner().name(), "make widgets"); 188 | assert_eq!(process2.costs(), &costs2); 189 | 190 | // can't work into a process you don't own 191 | let mut state3 = state.clone(); 192 | state3.model2_mut().set_company_id(CompanyID::new("zing")); 193 | let res = testfn(&state3); 194 | assert_eq!(res, Err(Error::Event(EventError::ProcessOwnerMismatch))); 195 | 196 | let mut state4 = state.clone(); 197 | state4.model_mut().set_class(MemberClass::User(MemberUser::new())); 198 | let res = testfn(&state4); 199 | assert_eq!(res, Err(Error::MemberMustBeWorker)); 200 | state4.model_mut().set_class(MemberClass::Company(MemberCompany::new())); 201 | let res = testfn(&state4); 202 | assert_eq!(res, Err(Error::MemberMustBeWorker)); 203 | } 204 | } 205 | 206 | -------------------------------------------------------------------------------- /src/transactions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Transactions are the primary interface for interacting with the Basis 2 | //! system. They are responsible for taking the needed information (which must 3 | //! be passed in) and returning a list of modifications that the caller is 4 | //! responsible for applying to whatever storage medium they are using. 5 | //! 6 | //! The high-level picture here is that we're creating a functional API for the 7 | //! models within the system and the interactions between them. The logic all 8 | //! lives in the transactions (and in some cases the models) but storage happens 9 | //! somewhere else and we don't touch it here. 10 | //! 11 | //! This means that any storage system that *can* support the Basis data models 12 | //! could (in theory) be used without needing to couple any of the logic to the 13 | //! storage mechanism. 14 | 15 | /// An action that happens between companies. This is used for intents and 16 | /// commitments. 17 | pub enum OrderAction { 18 | /// A service will be delivered 19 | DeliverService, 20 | /// A resource will be transferred (ownership and custody) 21 | Transfer, 22 | /// A resource's custody will be transferred for a period of time (delivery/rental) 23 | TransferCustody, 24 | } 25 | 26 | pub mod account; 27 | pub mod agreement; 28 | pub mod commitment; 29 | pub mod company; 30 | pub mod member; 31 | pub mod currency; 32 | pub mod event; 33 | pub mod intent; 34 | pub mod occupation; 35 | pub mod process; 36 | pub mod process_spec; 37 | pub mod resource; 38 | pub mod resource_spec; 39 | pub mod user; 40 | 41 | -------------------------------------------------------------------------------- /src/transactions/occupation.rs: -------------------------------------------------------------------------------- 1 | //! An occupation is effectively a job title that we want to track in the labor 2 | //! cost tracking. 3 | //! 4 | //! See the [occupation model.][1] 5 | //! 6 | //! [1]: ../../models/occupation/index.html 7 | 8 | use chrono::{DateTime, Utc}; 9 | use crate::{ 10 | access::Permission, 11 | error::{Error, Result}, 12 | models::{ 13 | Op, 14 | Modifications, 15 | lib::basis_model::Model, 16 | occupation::{Occupation, OccupationID}, 17 | user::User, 18 | }, 19 | }; 20 | use vf_rs::vf; 21 | 22 | /// Create a new `Occupation`. 23 | pub fn create>(caller: &User, id: OccupationID, label: T, note: T, active: bool, now: &DateTime) -> Result { 24 | caller.access_check(Permission::OccupationCreate)?; 25 | let model = Occupation::builder() 26 | .id(id) 27 | .inner( 28 | vf::AgentRelationshipRole::builder() 29 | .note(Some(note.into())) 30 | .role_label(label) 31 | .build() 32 | .map_err(|e| Error::BuilderFailed(e))? 33 | ) 34 | .active(active) 35 | .created(now.clone()) 36 | .updated(now.clone()) 37 | .build() 38 | .map_err(|e| Error::BuilderFailed(e))?; 39 | Ok(Modifications::new_single(Op::Create, model)) 40 | } 41 | 42 | /// Update an existing `Occupation` 43 | pub fn update(caller: &User, mut subject: Occupation, label: Option, note: Option, active: Option, now: &DateTime) -> Result { 44 | caller.access_check(Permission::OccupationUpdate)?; 45 | if let Some(label) = label { 46 | subject.inner_mut().set_role_label(label); 47 | } 48 | if let Some(note) = note { 49 | subject.inner_mut().set_note(Some(note)); 50 | } 51 | if let Some(active) = active { 52 | subject.set_active(active); 53 | } 54 | subject.set_updated(now.clone()); 55 | Ok(Modifications::new_single(Op::Update, subject)) 56 | } 57 | 58 | /// Delete an `Occupation` 59 | pub fn delete(caller: &User, mut subject: Occupation, now: &DateTime) -> Result { 60 | caller.access_check(Permission::OccupationDelete)?; 61 | if subject.is_deleted() { 62 | Err(Error::ObjectIsDeleted("occupation".into()))?; 63 | } 64 | subject.set_deleted(Some(now.clone())); 65 | Ok(Modifications::new_single(Op::Delete, subject)) 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use super::*; 71 | use crate::{ 72 | access::Role, 73 | models::{ 74 | Op, 75 | 76 | occupation::Occupation, 77 | }, 78 | util::{self, test::{self, *}}, 79 | }; 80 | 81 | #[test] 82 | fn can_create() { 83 | let id = OccupationID::create(); 84 | let now = util::time::now(); 85 | let mut state = TestState::standard(vec![], &now); 86 | state.user_mut().set_roles(vec![Role::SuperAdmin]); 87 | 88 | let testfn = |state: &TestState| { 89 | create(state.user(), id.clone(), "machinist", "builds things", true, &now) 90 | }; 91 | 92 | let mods = testfn(&state).unwrap().into_vec(); 93 | assert_eq!(mods.len(), 1); 94 | 95 | let occupation = mods[0].clone().expect_op::(Op::Create).unwrap(); 96 | assert_eq!(occupation.id(), &id); 97 | assert_eq!(occupation.inner().role_label(), "machinist"); 98 | assert_eq!(occupation.inner().note(), &Some("builds things".into())); 99 | assert_eq!(occupation.active(), &true); 100 | 101 | let mut state2 = state.clone(); 102 | state2.user_mut().set_roles(vec![Role::User]); 103 | let res = testfn(&state2); 104 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 105 | } 106 | 107 | #[test] 108 | fn can_update() { 109 | let id = OccupationID::create(); 110 | let now = util::time::now(); 111 | let mut state = TestState::standard(vec![], &now); 112 | state.user_mut().set_roles(vec![Role::SuperAdmin]); 113 | 114 | let mods = create(state.user(), id.clone(), "bone spurs in chief", "glorious leader", true, &now).unwrap().into_vec(); 115 | let occupation = mods[0].clone().expect_op::(Op::Create).unwrap(); 116 | state.model = Some(occupation); 117 | 118 | let now2 = util::time::now(); 119 | let testfn = |state: &TestState| { 120 | update(state.user(), state.model().clone(), Some("coward".into()), None, None, &now2) 121 | }; 122 | 123 | // not truly an update but ok 124 | let mods = testfn(&state).unwrap().into_vec(); 125 | let occupation2 = mods[0].clone().expect_op::(Op::Update).unwrap(); 126 | assert_eq!(state.model().created(), occupation2.created()); 127 | assert_eq!(occupation2.created(), &now); 128 | assert_eq!(occupation2.updated(), &now2); 129 | assert_eq!(occupation2.inner().role_label(), "coward"); 130 | assert_eq!(occupation2.inner().note(), &Some("glorious leader".into())); 131 | 132 | let mut state2 = state.clone(); 133 | state2.user_mut().set_roles(vec![Role::User]); 134 | let res = testfn(&state2); 135 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 136 | } 137 | 138 | #[test] 139 | fn can_delete() { 140 | let id = OccupationID::create(); 141 | let now = util::time::now(); 142 | let mut state = TestState::standard(vec![], &now); 143 | state.user_mut().set_roles(vec![Role::SuperAdmin]); 144 | 145 | let mods = create(state.user(), id.clone(), "the best president", "false acquisitions", true, &now).unwrap().into_vec(); 146 | let occupation = mods[0].clone().expect_op::(Op::Create).unwrap(); 147 | state.model = Some(occupation); 148 | 149 | let now2 = util::time::now(); 150 | let testfn = |state: &TestState| { 151 | delete(state.user(), state.model().clone(), &now2) 152 | }; 153 | test::double_deleted_tester(&state, "occupation", &testfn); 154 | 155 | let mods = testfn(&state).unwrap().into_vec(); 156 | assert_eq!(mods.len(), 1); 157 | let occupation2 = mods[0].clone().expect_op::(Op::Delete).unwrap(); 158 | assert_eq!(occupation2.id(), &id); 159 | assert_eq!(occupation2.created(), &now); 160 | assert_eq!(occupation2.deleted(), &Some(now2)); 161 | 162 | let mut state2 = state.clone(); 163 | state2.user_mut().set_roles(vec![Role::User]); 164 | let res = testfn(&state2); 165 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 166 | } 167 | } 168 | 169 | -------------------------------------------------------------------------------- /src/transactions/process.rs: -------------------------------------------------------------------------------- 1 | //! Processes are the means by which resources are created/transformed and labor 2 | //! is applied to these transformations. 3 | //! 4 | //! For instance a process might be making widgets, milling wood, driving a 5 | //! truck, etc. Processes aggregate costs into containers within a company. They 6 | //! can be as detailed or as general as desired. For instance you might have one 7 | //! big process for "run the company and make the things we make" which all 8 | //! costs flow into and out of, or you might have multiple processes for each 9 | //! part of the company's operations. 10 | //! 11 | //! Processes are somewhat time-defined and generally have a beginning and an 12 | //! end. This is not always the case, but generally is. For a more general 13 | //! process "type" see the [process_spec][1] transactions. 14 | //! 15 | //! See the [process model.][2] 16 | //! 17 | //! [1]: ../process_spec/index.html 18 | //! [2]: ../../models/process/index.html 19 | 20 | use chrono::{DateTime, Utc}; 21 | use crate::{ 22 | access::Permission, 23 | costs::Costs, 24 | error::{Error, Result}, 25 | models::{ 26 | Op, 27 | Modifications, 28 | company::{Company, Permission as CompanyPermission}, 29 | member::Member, 30 | lib::{ 31 | agent::AgentID, 32 | basis_model::Model, 33 | }, 34 | process::{Process, ProcessID}, 35 | process_spec::ProcessSpecID, 36 | user::User, 37 | }, 38 | }; 39 | use url::Url; 40 | use vf_rs::vf; 41 | 42 | /// Create a new process 43 | pub fn create>(caller: &User, member: &Member, company: &Company, id: ProcessID, spec_id: ProcessSpecID, name: T, note: T, classifications: Vec, has_beginning: Option>, has_end: Option>, in_scope_of: Vec, active: bool, now: &DateTime) -> Result { 44 | caller.access_check(Permission::CompanyUpdateProcesses)?; 45 | member.access_check(caller.id(), company.id(), CompanyPermission::ProcessCreate)?; 46 | if !company.is_active() { 47 | Err(Error::ObjectIsInactive("company".into()))?; 48 | } 49 | let model = Process::builder() 50 | .id(id) 51 | .inner( 52 | vf::Process::builder() 53 | .based_on(Some(spec_id)) 54 | .classified_as(classifications) 55 | .has_beginning(has_beginning) 56 | .has_end(has_end) 57 | .in_scope_of(in_scope_of) 58 | .name(name) 59 | .note(Some(note.into())) 60 | .build() 61 | .map_err(|e| Error::BuilderFailed(e))? 62 | ) 63 | .company_id(company.id().clone()) 64 | .costs(Costs::new()) 65 | .active(active) 66 | .created(now.clone()) 67 | .updated(now.clone()) 68 | .build() 69 | .map_err(|e| Error::BuilderFailed(e))?; 70 | Ok(Modifications::new_single(Op::Create, model)) 71 | } 72 | 73 | /// Update a process 74 | pub fn update(caller: &User, member: &Member, company: &Company, mut subject: Process, name: Option, note: Option, classifications: Option>, finished: Option, has_beginning: Option>, has_end: Option>, in_scope_of: Option>, active: Option, now: &DateTime) -> Result { 75 | caller.access_check(Permission::CompanyUpdateProcesses)?; 76 | member.access_check(caller.id(), company.id(), CompanyPermission::ProcessUpdate)?; 77 | if !company.is_active() { 78 | Err(Error::ObjectIsInactive("company".into()))?; 79 | } 80 | if let Some(name) = name { 81 | subject.inner_mut().set_name(name); 82 | } 83 | if note.is_some() { 84 | subject.inner_mut().set_note(note); 85 | } 86 | if let Some(classifications) = classifications { 87 | subject.inner_mut().set_classified_as(classifications); 88 | } 89 | if finished.is_some() { 90 | subject.inner_mut().set_finished(finished); 91 | } 92 | if has_beginning.is_some() { 93 | subject.inner_mut().set_has_beginning(has_beginning); 94 | } 95 | if has_end.is_some() { 96 | subject.inner_mut().set_has_end(has_end); 97 | } 98 | if let Some(in_scope_of) = in_scope_of { 99 | subject.inner_mut().set_in_scope_of(in_scope_of); 100 | } 101 | if let Some(active) = active { 102 | subject.set_active(active); 103 | } 104 | subject.set_updated(now.clone()); 105 | Ok(Modifications::new_single(Op::Update, subject)) 106 | } 107 | 108 | /// Delete a process 109 | pub fn delete(caller: &User, member: &Member, company: &Company, mut subject: Process, now: &DateTime) -> Result { 110 | caller.access_check(Permission::CompanyUpdateProcesses)?; 111 | member.access_check(caller.id(), company.id(), CompanyPermission::ProcessDelete)?; 112 | if !company.is_active() { 113 | Err(Error::ObjectIsInactive("company".into()))?; 114 | } 115 | if subject.is_deleted() { 116 | Err(Error::ObjectIsDeleted("process".into()))?; 117 | } 118 | if !subject.costs().is_zero() { 119 | Err(Error::CannotEraseCosts)?; 120 | } 121 | subject.set_deleted(Some(now.clone())); 122 | Ok(Modifications::new_single(Op::Delete, subject)) 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use super::*; 128 | use crate::{ 129 | models::{ 130 | lib::agent::Agent, 131 | process_spec::ProcessSpecID, 132 | }, 133 | util::{self, test::{self, *}}, 134 | }; 135 | 136 | #[test] 137 | fn can_create() { 138 | let now = util::time::now(); 139 | let id = ProcessID::create(); 140 | let state = TestState::standard(vec![CompanyPermission::ProcessCreate], &now); 141 | let spec = make_process_spec(&ProcessSpecID::create(), state.company().id(), "Make Gazelle Freestyle", true, &now); 142 | 143 | let testfn = |state: &TestState| { 144 | create(state.user(), state.member(), state.company(), id.clone(), spec.id().clone(), "Gazelle Freestyle Marathon", "tony making me build five of these stupid things", vec!["https://www.wikidata.org/wiki/Q1141557".parse().unwrap()], Some(now.clone()), None, vec![], true, &now) 145 | }; 146 | test::standard_transaction_tests(&state, &testfn); 147 | 148 | let mods = testfn(&state).unwrap().into_vec(); 149 | assert_eq!(mods.len(), 1); 150 | 151 | let process = mods[0].clone().expect_op::(Op::Create).unwrap(); 152 | assert_eq!(process.id(), &id); 153 | assert_eq!(process.inner().based_on(), &Some(spec.id().clone())); 154 | assert_eq!(process.inner().classified_as(), &vec!["https://www.wikidata.org/wiki/Q1141557".parse().unwrap()]); 155 | assert_eq!(process.inner().has_beginning(), &Some(now.clone())); 156 | assert_eq!(process.inner().has_end(), &None); 157 | assert_eq!(process.inner().in_scope_of(), &vec![]); 158 | assert_eq!(process.inner().name(), "Gazelle Freestyle Marathon"); 159 | assert_eq!(process.inner().note(), &Some("tony making me build five of these stupid things".into())); 160 | assert_eq!(process.company_id(), state.company().id()); 161 | assert!(process.costs().is_zero()); 162 | assert_eq!(process.active(), &true); 163 | assert_eq!(process.created(), &now); 164 | assert_eq!(process.updated(), &now); 165 | assert_eq!(process.deleted(), &None); 166 | } 167 | 168 | #[test] 169 | fn can_update() { 170 | let now = util::time::now(); 171 | let id = ProcessID::create(); 172 | let mut state = TestState::standard(vec![CompanyPermission::ProcessCreate, CompanyPermission::ProcessUpdate], &now); 173 | let spec = make_process_spec(&ProcessSpecID::create(), state.company().id(), "Make Gazelle Freestyle", true, &now); 174 | 175 | let mods = create(state.user(), state.member(), state.company(), id.clone(), spec.id().clone(), "Gazelle Freestyle Marathon", "tony making me build five of these stupid things", vec!["https://www.wikidata.org/wiki/Q1141557".parse().unwrap()], Some(now.clone()), None, vec![], true, &now).unwrap().into_vec(); 176 | let process = mods[0].clone().expect_op::(Op::Create).unwrap(); 177 | state.model = Some(process); 178 | 179 | let now2 = util::time::now(); 180 | let testfn = |state: &TestState| { 181 | update(state.user(), state.member(), state.company(), state.model().clone(), Some("Make a GaZeLLe fReeStYlE".into()), None, None, Some(true), None, Some(now2.clone()), Some(vec![state.company().agent_id()]), Some(false), &now2) 182 | }; 183 | test::standard_transaction_tests(&state, &testfn); 184 | 185 | let mods = testfn(&state).unwrap().into_vec(); 186 | assert_eq!(mods.len(), 1); 187 | 188 | let process2 = mods[0].clone().expect_op::(Op::Update).unwrap(); 189 | assert_eq!(process2.id(), &id); 190 | assert_eq!(process2.inner().based_on(), &Some(spec.id().clone())); 191 | assert_eq!(process2.inner().classified_as(), &vec!["https://www.wikidata.org/wiki/Q1141557".parse().unwrap()]); 192 | assert_eq!(process2.inner().has_beginning(), &Some(now.clone())); 193 | assert_eq!(process2.inner().has_end(), &Some(now2.clone())); 194 | assert_eq!(process2.inner().in_scope_of(), &vec![state.company().agent_id()]); 195 | assert_eq!(process2.inner().name(), "Make a GaZeLLe fReeStYlE"); 196 | assert_eq!(process2.inner().note(), &Some("tony making me build five of these stupid things".into())); 197 | assert_eq!(process2.company_id(), state.company().id()); 198 | assert!(process2.costs().is_zero()); 199 | assert_eq!(process2.active(), &false); 200 | assert_eq!(process2.created(), &now); 201 | assert_eq!(process2.updated(), &now2); 202 | assert_eq!(process2.deleted(), &None); 203 | } 204 | 205 | #[test] 206 | fn can_delete() { 207 | let now = util::time::now(); 208 | let id = ProcessID::create(); 209 | let mut state = TestState::standard(vec![CompanyPermission::CommitmentCreate, CompanyPermission::ProcessCreate, CompanyPermission::ProcessDelete], &now); 210 | let spec = make_process_spec(&ProcessSpecID::create(), state.company().id(), "Make Gazelle Freestyle", true, &now); 211 | 212 | let mods = create(state.user(), state.member(), state.company(), id.clone(), spec.id().clone(), "Gazelle Freestyle Marathon", "tony making me build five of these stupid things", vec!["https://www.wikidata.org/wiki/Q1141557".parse().unwrap()], Some(now.clone()), None, vec![], true, &now).unwrap().into_vec(); 213 | let process = mods[0].clone().expect_op::(Op::Create).unwrap(); 214 | state.model = Some(process); 215 | 216 | let now2 = util::time::now(); 217 | let testfn = |state: &TestState| { 218 | delete(state.user(), state.member(), state.company(), state.model().clone(), &now2) 219 | }; 220 | test::standard_transaction_tests(&state, &testfn); 221 | test::double_deleted_tester(&state, "process", &testfn); 222 | 223 | let mods = testfn(&state).unwrap().into_vec(); 224 | assert_eq!(mods.len(), 1); 225 | 226 | let process2 = mods[0].clone().expect_op::(Op::Delete).unwrap(); 227 | assert_eq!(process2.id(), &id); 228 | assert_eq!(process2.inner().based_on(), &Some(spec.id().clone())); 229 | assert_eq!(process2.inner().classified_as(), &vec!["https://www.wikidata.org/wiki/Q1141557".parse().unwrap()]); 230 | assert_eq!(process2.inner().has_beginning(), &Some(now.clone())); 231 | assert_eq!(process2.inner().has_end(), &None); 232 | assert_eq!(process2.inner().in_scope_of(), &vec![]); 233 | assert_eq!(process2.inner().name(), "Gazelle Freestyle Marathon"); 234 | assert_eq!(process2.inner().note(), &Some("tony making me build five of these stupid things".into())); 235 | assert_eq!(process2.company_id(), state.company().id()); 236 | assert!(process2.costs().is_zero()); 237 | assert_eq!(process2.active(), &true); 238 | assert_eq!(process2.created(), &now); 239 | assert_eq!(process2.updated(), &now); 240 | assert_eq!(process2.deleted(), &Some(now2.clone())); 241 | } 242 | } 243 | 244 | -------------------------------------------------------------------------------- /src/transactions/process_spec.rs: -------------------------------------------------------------------------------- 1 | //! A process specification is a blueprint for a process. Each process is an 2 | //! *instance* of a process specification. 3 | //! 4 | //! For instance, if you make five widgets today, you might have five processes 5 | //! for each widget, but one process specification called "build widgets" that 6 | //! those five processes reference. 7 | //! 8 | //! See the [process spec model.][1] 9 | //! 10 | //! [1]: ../../models/process_spec/index.html 11 | 12 | use chrono::{DateTime, Utc}; 13 | use crate::{ 14 | access::Permission, 15 | error::{Error, Result}, 16 | models::{ 17 | Op, 18 | Modifications, 19 | company::{Company, Permission as CompanyPermission}, 20 | member::Member, 21 | lib::basis_model::Model, 22 | process_spec::{ProcessSpec, ProcessSpecID}, 23 | user::User, 24 | }, 25 | }; 26 | use vf_rs::vf; 27 | 28 | /// Create a new ProcessSpec 29 | pub fn create>(caller: &User, member: &Member, company: &Company, id: ProcessSpecID, name: T, note: T, active: bool, now: &DateTime) -> Result { 30 | caller.access_check(Permission::CompanyUpdateProcessSpecs)?; 31 | member.access_check(caller.id(), company.id(), CompanyPermission::ProcessSpecCreate)?; 32 | if !company.is_active() { 33 | Err(Error::ObjectIsInactive("company".into()))?; 34 | } 35 | let model = ProcessSpec::builder() 36 | .id(id) 37 | .inner( 38 | vf::ProcessSpecification::builder() 39 | .name(name) 40 | .note(Some(note.into())) 41 | .build() 42 | .map_err(|e| Error::BuilderFailed(e))? 43 | ) 44 | .company_id(company.id().clone()) 45 | .active(active) 46 | .created(now.clone()) 47 | .updated(now.clone()) 48 | .build() 49 | .map_err(|e| Error::BuilderFailed(e))?; 50 | Ok(Modifications::new_single(Op::Create, model)) 51 | } 52 | 53 | /// Update a resource spec 54 | pub fn update(caller: &User, member: &Member, company: &Company, mut subject: ProcessSpec, name: Option, note: Option, active: Option, now: &DateTime) -> Result { 55 | caller.access_check(Permission::CompanyUpdateProcessSpecs)?; 56 | member.access_check(caller.id(), company.id(), CompanyPermission::ProcessSpecUpdate)?; 57 | if !company.is_active() { 58 | Err(Error::ObjectIsInactive("company".into()))?; 59 | } 60 | if let Some(name) = name { 61 | subject.inner_mut().set_name(name); 62 | } 63 | if let Some(note) = note { 64 | subject.inner_mut().set_note(Some(note)); 65 | } 66 | if let Some(active) = active { 67 | subject.set_active(active); 68 | } 69 | subject.set_updated(now.clone()); 70 | Ok(Modifications::new_single(Op::Update, subject)) 71 | } 72 | 73 | /// Delete a resource spec 74 | pub fn delete(caller: &User, member: &Member, company: &Company, mut subject: ProcessSpec, now: &DateTime) -> Result { 75 | caller.access_check(Permission::CompanyUpdateProcessSpecs)?; 76 | member.access_check(caller.id(), company.id(), CompanyPermission::ProcessSpecDelete)?; 77 | if !company.is_active() { 78 | Err(Error::ObjectIsInactive("company".into()))?; 79 | } 80 | if subject.is_deleted() { 81 | Err(Error::ObjectIsDeleted("process_spec".into()))?; 82 | } 83 | subject.set_deleted(Some(now.clone())); 84 | Ok(Modifications::new_single(Op::Delete, subject)) 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use super::*; 90 | use crate::{ 91 | models::{ 92 | process_spec::{ProcessSpec, ProcessSpecID}, 93 | }, 94 | util::{self, test::{self, *}}, 95 | }; 96 | 97 | #[test] 98 | fn can_create() { 99 | let now = util::time::now(); 100 | let id = ProcessSpecID::create(); 101 | let state = TestState::standard(vec![CompanyPermission::ProcessSpecCreate], &now); 102 | 103 | let testfn = |state: &TestState| { 104 | create(state.user(), state.member(), state.company(), id.clone(), "SEIZE THE MEANS OF PRODUCTION", "our first process", true, &now) 105 | }; 106 | 107 | let mods = testfn(&state).unwrap().into_vec(); 108 | assert_eq!(mods.len(), 1); 109 | 110 | let recspec = mods[0].clone().expect_op::(Op::Create).unwrap(); 111 | assert_eq!(recspec.id(), &id); 112 | assert_eq!(recspec.inner().name(), "SEIZE THE MEANS OF PRODUCTION"); 113 | assert_eq!(recspec.inner().note(), &Some("our first process".into())); 114 | assert_eq!(recspec.company_id(), state.company().id()); 115 | assert_eq!(recspec.active(), &true); 116 | assert_eq!(recspec.created(), &now); 117 | assert_eq!(recspec.updated(), &now); 118 | assert_eq!(recspec.deleted(), &None); 119 | } 120 | 121 | #[test] 122 | fn can_update() { 123 | let now = util::time::now(); 124 | let id = ProcessSpecID::create(); 125 | let mut state = TestState::standard(vec![CompanyPermission::ProcessSpecCreate, CompanyPermission::ProcessSpecUpdate], &now); 126 | let mods = create(state.user(), state.member(), state.company(), id.clone(), "SEIZE THE MEANS OF PRODUCTION", "our first process", true, &now).unwrap().into_vec(); 127 | let procspec = mods[0].clone().expect_op::(Op::Create).unwrap(); 128 | state.model = Some(procspec); 129 | 130 | let now2 = util::time::now(); 131 | let testfn = |state: &TestState| { 132 | update(state.user(), state.member(), state.company(), state.model().clone(), Some("best widget".into()), None, Some(false), &now2) 133 | }; 134 | 135 | let mods = testfn(&state).unwrap().into_vec(); 136 | assert_eq!(mods.len(), 1); 137 | 138 | let procspec2 = mods[0].clone().expect_op::(Op::Update).unwrap(); 139 | assert_eq!(procspec2.id(), &id); 140 | assert_eq!(procspec2.inner().name(), "best widget"); 141 | assert_eq!(procspec2.inner().note(), &Some("our first process".into())); 142 | assert_eq!(procspec2.company_id(), state.company().id()); 143 | assert_eq!(procspec2.active(), &false); 144 | assert_eq!(procspec2.created(), &now); 145 | assert_eq!(procspec2.updated(), &now2); 146 | assert_eq!(procspec2.deleted(), &None); 147 | } 148 | 149 | #[test] 150 | fn can_delete() { 151 | let now = util::time::now(); 152 | let id = ProcessSpecID::create(); 153 | let mut state = TestState::standard(vec![CompanyPermission::ProcessSpecCreate, CompanyPermission::ProcessSpecDelete], &now); 154 | let mods = create(state.user(), state.member(), state.company(), id.clone(), "SEIZE THE MEANS OF PRODUCTION", "our first process", true, &now).unwrap().into_vec(); 155 | let procspec = mods[0].clone().expect_op::(Op::Create).unwrap(); 156 | state.model = Some(procspec); 157 | 158 | let now2 = util::time::now(); 159 | let testfn = |state: &TestState| { 160 | delete(state.user(), state.member(), state.company(), state.model().clone(), &now2) 161 | }; 162 | test::standard_transaction_tests(&state, &testfn); 163 | test::double_deleted_tester(&state, "process_spec", &testfn); 164 | 165 | let mods = testfn(&state).unwrap().into_vec(); 166 | assert_eq!(mods.len(), 1); 167 | 168 | let procspec2 = mods[0].clone().expect_op::(Op::Delete).unwrap(); 169 | assert_eq!(procspec2.id(), &id); 170 | assert_eq!(procspec2.inner().name(), "SEIZE THE MEANS OF PRODUCTION"); 171 | assert_eq!(procspec2.company_id(), state.company().id()); 172 | assert_eq!(procspec2.active(), &true); 173 | assert_eq!(procspec2.created(), &now); 174 | assert_eq!(procspec2.updated(), &now); 175 | assert_eq!(procspec2.deleted(), &Some(now2.clone())); 176 | } 177 | } 178 | 179 | -------------------------------------------------------------------------------- /src/transactions/resource.rs: -------------------------------------------------------------------------------- 1 | //! A resource is a tangible asset. It can represent a chair, a house, a forest, 2 | //! a widget, a barrel of crude oil, etc. 3 | //! 4 | //! Resources are instances of a [resource_spec][1]. If the resource 5 | //! specification is a product description on an online shop, the resource is 6 | //! the actual delivered good that you receive when you order it. 7 | //! 8 | //! See the [resource model.][2] 9 | //! 10 | //! [1]: ../resource_spec/index.html 11 | //! [2]: ../../models/resource/index.html 12 | 13 | use chrono::{DateTime, Utc}; 14 | use crate::{ 15 | access::Permission, 16 | costs::Costs, 17 | error::{Error, Result}, 18 | models::{ 19 | Op, 20 | Modifications, 21 | company::{Company, Permission as CompanyPermission}, 22 | member::Member, 23 | lib::{ 24 | agent::Agent, 25 | basis_model::Model, 26 | }, 27 | resource::{Resource, ResourceID}, 28 | resource_spec::ResourceSpecID, 29 | user::User, 30 | }, 31 | }; 32 | use om2::Unit; 33 | use url::Url; 34 | use vf_rs::{vf, dfc}; 35 | 36 | /// Create a new resource 37 | pub fn create(caller: &User, member: &Member, company: &Company, id: ResourceID, spec_id: ResourceSpecID, lot: Option, name: Option, tracking_id: Option, classifications: Vec, note: Option, unit_of_effort: Option, active: bool, now: &DateTime) -> Result { 38 | caller.access_check(Permission::CompanyUpdateResources)?; 39 | member.access_check(caller.id(), company.id(), CompanyPermission::ResourceCreate)?; 40 | if !company.is_active() { 41 | Err(Error::ObjectIsInactive("company".into()))?; 42 | } 43 | let model = Resource::builder() 44 | .id(id) 45 | .inner( 46 | vf::EconomicResource::builder() 47 | .classified_as(classifications) 48 | .conforms_to(spec_id) 49 | .lot(lot) 50 | .name(name) 51 | .note(note) 52 | .primary_accountable(Some(company.agent_id())) 53 | .tracking_identifier(tracking_id) 54 | .unit_of_effort(unit_of_effort) 55 | .build() 56 | .map_err(|e| Error::BuilderFailed(e))? 57 | ) 58 | .in_custody_of(company.id().clone()) 59 | .costs(Costs::new()) 60 | .active(active) 61 | .created(now.clone()) 62 | .updated(now.clone()) 63 | .build() 64 | .map_err(|e| Error::BuilderFailed(e))?; 65 | Ok(Modifications::new_single(Op::Create, model)) 66 | } 67 | 68 | /// Update a resource 69 | pub fn update(caller: &User, member: &Member, company: &Company, mut subject: Resource, lot: Option, name: Option, tracking_id: Option, classifications: Option>, note: Option, unit_of_effort: Option, active: Option, now: &DateTime) -> Result { 70 | caller.access_check(Permission::CompanyUpdateResources)?; 71 | member.access_check(caller.id(), company.id(), CompanyPermission::ResourceUpdate)?; 72 | if !company.is_active() { 73 | Err(Error::ObjectIsInactive("company".into()))?; 74 | } 75 | if lot.is_some() { 76 | subject.inner_mut().set_lot(lot); 77 | } 78 | if name.is_some() { 79 | subject.inner_mut().set_name(name); 80 | } 81 | if tracking_id.is_some() { 82 | subject.inner_mut().set_tracking_identifier(tracking_id); 83 | } 84 | if let Some(classifications) = classifications { 85 | subject.inner_mut().set_classified_as(classifications); 86 | } 87 | if note.is_some() { 88 | subject.inner_mut().set_note(note); 89 | } 90 | if unit_of_effort.is_some() { 91 | subject.inner_mut().set_unit_of_effort(unit_of_effort); 92 | } 93 | if let Some(active) = active { 94 | subject.set_active(active); 95 | } 96 | subject.set_updated(now.clone()); 97 | Ok(Modifications::new_single(Op::Update, subject)) 98 | } 99 | 100 | /// Delete a resource 101 | pub fn delete(caller: &User, member: &Member, company: &Company, mut subject: Resource, now: &DateTime) -> Result { 102 | caller.access_check(Permission::CompanyUpdateResources)?; 103 | member.access_check(caller.id(), company.id(), CompanyPermission::ResourceDelete)?; 104 | if !company.is_active() { 105 | Err(Error::ObjectIsInactive("company".into()))?; 106 | } 107 | if subject.is_deleted() { 108 | Err(Error::ObjectIsDeleted("resource".into()))?; 109 | } 110 | if !subject.costs().is_zero() { 111 | Err(Error::CannotEraseCosts)?; 112 | } 113 | subject.set_deleted(Some(now.clone())); 114 | Ok(Modifications::new_single(Op::Delete, subject)) 115 | } 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | use super::*; 120 | use crate::{ 121 | models::{ 122 | resource_spec::ResourceSpecID, 123 | }, 124 | util::{self, test::{self, *}}, 125 | }; 126 | 127 | #[test] 128 | fn can_create() { 129 | let now = util::time::now(); 130 | let id = ResourceID::create(); 131 | let state = TestState::standard(vec![CompanyPermission::ResourceCreate], &now); 132 | let spec = make_resource_spec(&ResourceSpecID::create(), state.company().id(), "widgets, baby", &now); 133 | let lot = dfc::ProductBatch::builder() 134 | .batch_number("123") 135 | .build().unwrap(); 136 | 137 | let testfn = |state: &TestState| { 138 | create(state.user(), state.member(), state.company(), id.clone(), spec.id().clone(), Some(lot.clone()), Some("widget batch".into()), None, vec!["https://www.wikidata.org/wiki/Q605117".parse().unwrap()], Some("niceee".into()), Some(Unit::Hour), true, &now) 139 | }; 140 | 141 | let mods = testfn(&state).unwrap().into_vec(); 142 | assert_eq!(mods.len(), 1); 143 | 144 | let resource = mods[0].clone().expect_op::(Op::Create).unwrap(); 145 | assert_eq!(resource.id(), &id); 146 | assert_eq!(resource.inner().name(), &Some("widget batch".into())); 147 | assert_eq!(resource.inner().lot(), &Some(lot.clone())); 148 | assert_eq!(resource.inner().classified_as(), &vec!["https://www.wikidata.org/wiki/Q605117".parse().unwrap()]); 149 | assert_eq!(resource.inner().primary_accountable(), &Some(state.company().agent_id())); 150 | assert_eq!(resource.inner().tracking_identifier(), &None); 151 | assert_eq!(resource.inner().note(), &Some("niceee".into())); 152 | assert_eq!(resource.inner().unit_of_effort(), &Some(Unit::Hour)); 153 | assert_eq!(resource.in_custody_of(), &state.company().agent_id()); 154 | assert!(resource.costs().is_zero()); 155 | assert_eq!(resource.active(), &true); 156 | assert_eq!(resource.created(), &now); 157 | assert_eq!(resource.updated(), &now); 158 | assert_eq!(resource.deleted(), &None); 159 | } 160 | 161 | #[test] 162 | fn can_update() { 163 | let now = util::time::now(); 164 | let id = ResourceID::create(); 165 | let mut state = TestState::standard(vec![CompanyPermission::ResourceCreate, CompanyPermission::ResourceUpdate], &now); 166 | let spec = make_resource_spec(&ResourceSpecID::create(), state.company().id(), "widgets, baby", &now); 167 | let lot = dfc::ProductBatch::builder() 168 | .batch_number("123") 169 | .build().unwrap(); 170 | let mods = create(state.user(), state.member(), state.company(), id.clone(), spec.id().clone(), Some(lot.clone()), Some("widget batch".into()), None, vec!["https://www.wikidata.org/wiki/Q605117".parse().unwrap()], Some("niceee".into()), Some(Unit::Hour), true, &now).unwrap().into_vec(); 171 | let resource = mods[0].clone().expect_op::(Op::Create).unwrap(); 172 | state.model = Some(resource); 173 | 174 | let now2 = util::time::now(); 175 | let testfn = |state: &TestState| { 176 | update(state.user(), state.member(), state.company(), state.model().clone(), None, Some("better widgets".into()), Some("444-computers-and-equipment".into()), None, None, Some(Unit::WattHour), Some(false), &now2) 177 | }; 178 | 179 | let mods = testfn(&state).unwrap().into_vec(); 180 | assert_eq!(mods.len(), 1); 181 | 182 | let resource2 = mods[0].clone().expect_op::(Op::Update).unwrap(); 183 | assert_eq!(resource2.id(), &id); 184 | assert_eq!(resource2.inner().name(), &Some("better widgets".into())); 185 | assert_eq!(resource2.inner().lot(), &Some(lot.clone())); 186 | assert_eq!(resource2.inner().classified_as(), &vec!["https://www.wikidata.org/wiki/Q605117".parse().unwrap()]); 187 | assert_eq!(resource2.inner().primary_accountable(), &Some(state.company().agent_id())); 188 | assert_eq!(resource2.inner().tracking_identifier(), &Some("444-computers-and-equipment".into())); 189 | assert_eq!(resource2.inner().note(), &Some("niceee".into())); 190 | assert_eq!(resource2.inner().unit_of_effort(), &Some(Unit::WattHour)); 191 | assert_eq!(resource2.in_custody_of(), &state.company().agent_id()); 192 | assert_eq!(resource2.active(), &false); 193 | assert_eq!(resource2.created(), &now); 194 | assert_eq!(resource2.updated(), &now2); 195 | assert_eq!(resource2.deleted(), &None); 196 | } 197 | 198 | #[test] 199 | fn can_delete() { 200 | let now = util::time::now(); 201 | let id = ResourceID::create(); 202 | let mut state = TestState::standard(vec![CompanyPermission::ResourceCreate, CompanyPermission::ResourceDelete], &now); 203 | let spec = make_resource_spec(&ResourceSpecID::create(), state.company().id(), "widgets, baby", &now); 204 | let lot = dfc::ProductBatch::builder() 205 | .batch_number("123") 206 | .build().unwrap(); 207 | let mods = create(state.user(), state.member(), state.company(), id.clone(), spec.id().clone(), Some(lot.clone()), Some("widget batch".into()), None, vec!["https://www.wikidata.org/wiki/Q605117".parse().unwrap()], Some("niceee".into()), Some(Unit::Hour), true, &now).unwrap().into_vec(); 208 | let resource = mods[0].clone().expect_op::(Op::Create).unwrap(); 209 | state.model = Some(resource); 210 | 211 | let now2 = util::time::now(); 212 | let testfn = |state: &TestState| { 213 | delete(state.user(), state.member(), state.company(), state.model().clone(), &now2) 214 | }; 215 | test::standard_transaction_tests(&state, &testfn); 216 | test::double_deleted_tester(&state, "resource", &testfn); 217 | 218 | let mods = testfn(&state).unwrap().into_vec(); 219 | assert_eq!(mods.len(), 1); 220 | 221 | let resource2 = mods[0].clone().expect_op::(Op::Delete).unwrap(); 222 | assert_eq!(resource2.id(), &id); 223 | assert_eq!(resource2.inner().name(), &Some("widget batch".into())); 224 | assert_eq!(resource2.inner().lot(), &Some(lot.clone())); 225 | assert_eq!(resource2.inner().classified_as(), &vec!["https://www.wikidata.org/wiki/Q605117".parse().unwrap()]); 226 | assert_eq!(resource2.inner().primary_accountable(), &Some(state.company().agent_id())); 227 | assert_eq!(resource2.inner().tracking_identifier(), &None); 228 | assert_eq!(resource2.inner().note(), &Some("niceee".into())); 229 | assert_eq!(resource2.inner().unit_of_effort(), &Some(Unit::Hour)); 230 | assert_eq!(resource2.in_custody_of(), &state.company().agent_id()); 231 | assert_eq!(resource2.active(), &true); 232 | assert_eq!(resource2.created(), &now); 233 | assert_eq!(resource2.updated(), &now); 234 | assert_eq!(resource2.deleted(), &Some(now2.clone())); 235 | } 236 | } 237 | 238 | -------------------------------------------------------------------------------- /src/transactions/resource_spec.rs: -------------------------------------------------------------------------------- 1 | //! A resource specification is a general description of a tangible asset. 2 | //! 3 | //! Every [resource][1] is an instance of a resource specification. For 4 | //! instance, a *resource specification* might be a product listing page for a 5 | //! "Haworth Zody" chair, and the *resource* is the cheap knock-off counterfeit 6 | //! that Amazon ships to you when you order it. 7 | //! 8 | //! See the [resource spec model.][2] 9 | //! 10 | //! [1]: ../resource/index.html 11 | //! [2]: ../../models/resource_spec/index.html 12 | 13 | use chrono::{DateTime, Utc}; 14 | use crate::{ 15 | access::Permission, 16 | error::{Error, Result}, 17 | models::{ 18 | Op, 19 | Modifications, 20 | company::{Company, Permission as CompanyPermission}, 21 | member::Member, 22 | lib::basis_model::Model, 23 | resource_spec::{ResourceSpec, ResourceSpecID}, 24 | user::User, 25 | }, 26 | }; 27 | use om2::Unit; 28 | use url::Url; 29 | use vf_rs::vf; 30 | 31 | /// Create a new ResourceSpec 32 | pub fn create>(caller: &User, member: &Member, company: &Company, id: ResourceSpecID, name: T, note: T, classifications: Vec, default_unit_of_effort: Option, default_unit_of_resource: Option, active: bool, now: &DateTime) -> Result { 33 | caller.access_check(Permission::CompanyUpdateResourceSpecs)?; 34 | member.access_check(caller.id(), company.id(), CompanyPermission::ResourceSpecCreate)?; 35 | if !company.is_active() { 36 | Err(Error::ObjectIsInactive("company".into()))?; 37 | } 38 | let model = ResourceSpec::builder() 39 | .id(id) 40 | .inner( 41 | vf::ResourceSpecification::builder() 42 | .default_unit_of_effort(default_unit_of_effort) 43 | .default_unit_of_resource(default_unit_of_resource) 44 | .name(name) 45 | .note(Some(note.into())) 46 | .resource_classified_as(classifications) 47 | .build() 48 | .map_err(|e| Error::BuilderFailed(e))? 49 | ) 50 | .company_id(company.id().clone()) 51 | .active(active) 52 | .created(now.clone()) 53 | .updated(now.clone()) 54 | .build() 55 | .map_err(|e| Error::BuilderFailed(e))?; 56 | Ok(Modifications::new_single(Op::Create, model)) 57 | } 58 | 59 | /// Update a resource spec 60 | pub fn update(caller: &User, member: &Member, company: &Company, mut subject: ResourceSpec, name: Option, note: Option, classifications: Option>, default_unit_of_effort: Option, default_unit_of_resource: Option, active: Option, now: &DateTime) -> Result { 61 | caller.access_check(Permission::CompanyUpdateResourceSpecs)?; 62 | member.access_check(caller.id(), company.id(), CompanyPermission::ResourceSpecUpdate)?; 63 | if !company.is_active() { 64 | Err(Error::ObjectIsInactive("company".into()))?; 65 | } 66 | if let Some(name) = name { 67 | subject.inner_mut().set_name(name); 68 | } 69 | if let Some(note) = note { 70 | subject.inner_mut().set_note(Some(note)); 71 | } 72 | if let Some(classifications) = classifications { 73 | subject.inner_mut().set_resource_classified_as(classifications); 74 | } 75 | if default_unit_of_effort.is_some() { 76 | subject.inner_mut().set_default_unit_of_effort(default_unit_of_effort); 77 | } 78 | if default_unit_of_resource.is_some() { 79 | subject.inner_mut().set_default_unit_of_resource(default_unit_of_resource); 80 | } 81 | if let Some(active) = active { 82 | subject.set_active(active); 83 | } 84 | subject.set_updated(now.clone()); 85 | Ok(Modifications::new_single(Op::Update, subject)) 86 | } 87 | 88 | /// Delete a resource spec 89 | pub fn delete(caller: &User, member: &Member, company: &Company, mut subject: ResourceSpec, now: &DateTime) -> Result { 90 | caller.access_check(Permission::CompanyUpdateResourceSpecs)?; 91 | member.access_check(caller.id(), company.id(), CompanyPermission::ResourceSpecDelete)?; 92 | if !company.is_active() { 93 | Err(Error::ObjectIsInactive("company".into()))?; 94 | } 95 | if subject.is_deleted() { 96 | Err(Error::ObjectIsDeleted("resource_spec".into()))?; 97 | } 98 | subject.set_deleted(Some(now.clone())); 99 | Ok(Modifications::new_single(Op::Delete, subject)) 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | use crate::{ 106 | models::{ 107 | resource_spec::{ResourceSpec, ResourceSpecID}, 108 | }, 109 | util::{self, test::{self, *}}, 110 | }; 111 | 112 | #[test] 113 | fn can_create() { 114 | let now = util::time::now(); 115 | let id = ResourceSpecID::create(); 116 | let state = TestState::standard(vec![CompanyPermission::ResourceSpecCreate], &now); 117 | 118 | let testfn = |state: &TestState| { 119 | create(state.user(), state.member(), state.company(), id.clone(), "Beans", "yummy", vec!["https://www.wikidata.org/wiki/Q379813".parse().unwrap()], Some(Unit::Hour), Some(Unit::Kilogram), true, &now) 120 | }; 121 | test::standard_transaction_tests(&state, &testfn); 122 | 123 | let mods = testfn(&state).unwrap().into_vec(); 124 | assert_eq!(mods.len(), 1); 125 | 126 | let recspec = mods[0].clone().expect_op::(Op::Create).unwrap(); 127 | assert_eq!(recspec.id(), &id); 128 | assert_eq!(recspec.inner().default_unit_of_effort(), &Some(Unit::Hour)); 129 | assert_eq!(recspec.inner().default_unit_of_resource(), &Some(Unit::Kilogram)); 130 | assert_eq!(recspec.inner().name(), "Beans"); 131 | assert_eq!(recspec.inner().note(), &Some("yummy".into())); 132 | assert_eq!(recspec.inner().resource_classified_as(), &vec!["https://www.wikidata.org/wiki/Q379813".parse().unwrap()]); 133 | assert_eq!(recspec.company_id(), state.company().id()); 134 | assert_eq!(recspec.active(), &true); 135 | assert_eq!(recspec.created(), &now); 136 | assert_eq!(recspec.updated(), &now); 137 | assert_eq!(recspec.deleted(), &None); 138 | } 139 | 140 | #[test] 141 | fn can_update() { 142 | let now = util::time::now(); 143 | let id = ResourceSpecID::create(); 144 | let mut state = TestState::standard(vec![CompanyPermission::ResourceSpecCreate, CompanyPermission::ResourceSpecUpdate], &now); 145 | let mods = create(state.user(), state.member(), state.company(), id.clone(), "Beans", "yummy", vec!["https://www.wikidata.org/wiki/Q379813".parse().unwrap()], Some(Unit::Hour), Some(Unit::Kilogram), true, &now).unwrap().into_vec(); 146 | let recspec = mods[0].clone().expect_op::(Op::Create).unwrap(); 147 | state.model = Some(recspec); 148 | 149 | let now2 = util::time::now(); 150 | let testfn = |state: &TestState| { 151 | update(state.user(), state.member(), state.company(), state.model().clone(), Some("best widget".into()), None, None, Some(Unit::WattHour), None, Some(false), &now2) 152 | }; 153 | test::standard_transaction_tests(&state, &testfn); 154 | 155 | let mods = testfn(&state).unwrap().into_vec(); 156 | assert_eq!(mods.len(), 1); 157 | 158 | let recspec2 = mods[0].clone().expect_op::(Op::Update).unwrap(); 159 | assert_eq!(recspec2.id(), &id); 160 | assert_eq!(recspec2.inner().default_unit_of_effort(), &Some(Unit::WattHour)); 161 | assert_eq!(recspec2.inner().default_unit_of_resource(), &Some(Unit::Kilogram)); 162 | assert_eq!(recspec2.inner().name(), "best widget"); 163 | assert_eq!(recspec2.inner().note(), &Some("yummy".into())); 164 | assert_eq!(recspec2.inner().resource_classified_as(), &vec!["https://www.wikidata.org/wiki/Q379813".parse().unwrap()]); 165 | assert_eq!(recspec2.company_id(), state.company().id()); 166 | assert_eq!(recspec2.active(), &false); 167 | assert_eq!(recspec2.created(), &now); 168 | assert_eq!(recspec2.updated(), &now2); 169 | assert_eq!(recspec2.deleted(), &None); 170 | } 171 | 172 | #[test] 173 | fn can_delete() { 174 | let now = util::time::now(); 175 | let id = ResourceSpecID::create(); 176 | let mut state = TestState::standard(vec![CompanyPermission::ResourceSpecCreate, CompanyPermission::ResourceSpecDelete], &now); 177 | let mods = create(state.user(), state.member(), state.company(), id.clone(), "Beans", "yummy", vec!["https://www.wikidata.org/wiki/Q379813".parse().unwrap()], Some(Unit::Hour), Some(Unit::Kilogram), true, &now).unwrap().into_vec(); 178 | let recspec = mods[0].clone().expect_op::(Op::Create).unwrap(); 179 | state.model = Some(recspec); 180 | 181 | let now2 = util::time::now(); 182 | let testfn = |state: &TestState| { 183 | delete(state.user(), state.member(), state.company(), state.model().clone(), &now2) 184 | }; 185 | test::standard_transaction_tests(&state, &testfn); 186 | test::double_deleted_tester(&state, "resource_spec", &testfn); 187 | 188 | let mods = testfn(&state).unwrap().into_vec(); 189 | assert_eq!(mods.len(), 1); 190 | 191 | let recspec2 = mods[0].clone().expect_op::(Op::Delete).unwrap(); 192 | assert_eq!(recspec2.id(), &id); 193 | assert_eq!(recspec2.inner().default_unit_of_effort(), &Some(Unit::Hour)); 194 | assert_eq!(recspec2.inner().default_unit_of_resource(), &Some(Unit::Kilogram)); 195 | assert_eq!(recspec2.inner().name(), "Beans"); 196 | assert_eq!(recspec2.inner().resource_classified_as(), &vec!["https://www.wikidata.org/wiki/Q379813".parse().unwrap()]); 197 | assert_eq!(recspec2.company_id(), state.company().id()); 198 | assert_eq!(recspec2.active(), &true); 199 | assert_eq!(recspec2.created(), &now); 200 | assert_eq!(recspec2.updated(), &now); 201 | assert_eq!(recspec2.deleted(), &Some(now2.clone())); 202 | } 203 | } 204 | 205 | -------------------------------------------------------------------------------- /src/transactions/user.rs: -------------------------------------------------------------------------------- 1 | //! A user represents a person with membership in the system. 2 | //! 3 | //! See the [user model][1]. 4 | //! 5 | //! [1]: ../../models/user/index.html 6 | 7 | use chrono::{DateTime, Utc}; 8 | use crate::{ 9 | access::{self, Permission, Role}, 10 | error::{Error, Result}, 11 | models::{ 12 | Op, 13 | Modifications, 14 | account::{Account, AccountID, Multisig, Ubi}, 15 | lib::basis_model::Model, 16 | user::{User, UserID}, 17 | }, 18 | }; 19 | 20 | /// Create a user (private implementation, meant to be wrapped). 21 | fn create_inner>(id: UserID, roles: Vec, email: T, name: T, ubi_account_id: AccountID, active: bool, now: &DateTime) -> Result { 22 | let model = User::builder() 23 | .id(id.clone()) 24 | .roles(roles) 25 | .email(email) 26 | .name(name) 27 | .active(active) 28 | .created(now.clone()) 29 | .updated(now.clone()) 30 | .build() 31 | .map_err(|e| Error::BuilderFailed(e))?; 32 | let ubi = Account::builder() 33 | .id(ubi_account_id) 34 | .user_ids(vec![id]) 35 | .multisig(vec![Multisig::new(1)]) 36 | .name("UBI") 37 | .description("Your UBI account") 38 | .balance(0) 39 | .ubi(Some(Ubi::new(now.clone()))) 40 | .created(now.clone()) 41 | .updated(now.clone()) 42 | .build() 43 | .map_err(|e| Error::BuilderFailed(e))?; 44 | let mut mods = Modifications::new(); 45 | mods.push(Op::Create, model); 46 | mods.push(Op::Create, ubi); 47 | Ok(mods) 48 | } 49 | 50 | /// Create a new user with a `Role::User` role. No permissions required. 51 | /// 52 | /// Also creates a UBI account for the user. 53 | pub fn create>(id: UserID, email: T, name: T, ubi_account_id: AccountID, active: bool, now: &DateTime) -> Result { 54 | access::guest_check(Permission::UserCreate)?; 55 | create_inner(id, vec![Role::User], email, name, ubi_account_id, active, now) 56 | } 57 | 58 | /// Create a new user with a specific set of permissions using a current user as 59 | /// the originator. Effectively an admin create. Requires the 60 | /// `Permission::UserCreate` permission. 61 | /// 62 | /// Also creates a UBI account for the user. 63 | pub fn create_permissioned>(caller: &User, id: UserID, roles: Vec, email: T, name: T, ubi_account_id: AccountID, active: bool, now: &DateTime) -> Result { 64 | caller.access_check(Permission::UserAdminCreate)?; 65 | create_inner(id, roles, email, name, ubi_account_id, active, now) 66 | } 67 | 68 | /// Update a user object 69 | pub fn update(caller: &User, mut subject: User, email: Option, name: Option, active: Option, now: &DateTime) -> Result { 70 | caller.access_check(Permission::UserAdminUpdate) 71 | .or_else(|_| { 72 | caller.access_check(Permission::UserUpdate) 73 | .and_then(|_| { 74 | if caller.id() == subject.id() { 75 | Ok(()) 76 | } else { 77 | Err(Error::InsufficientPrivileges) 78 | } 79 | }) 80 | })?; 81 | if let Some(email) = email { 82 | subject.set_email(email); 83 | } 84 | if let Some(name) = name { 85 | subject.set_name(name); 86 | } 87 | if let Some(active) = active { 88 | subject.set_active(active); 89 | } 90 | subject.set_updated(now.clone()); 91 | Ok(Modifications::new_single(Op::Update, subject)) 92 | } 93 | 94 | /// Update a user's roles 95 | pub fn set_roles(caller: &User, mut subject: User, roles: Vec, now: &DateTime) -> Result { 96 | caller.access_check(Permission::UserSetRoles)?; 97 | subject.set_roles(roles); 98 | subject.set_updated(now.clone()); 99 | Ok(Modifications::new_single(Op::Update, subject)) 100 | } 101 | 102 | /// Delete a user 103 | pub fn delete(caller: &User, mut subject: User, now: &DateTime) -> Result { 104 | caller.access_check(Permission::UserDelete)?; 105 | if subject.is_deleted() { 106 | Err(Error::ObjectIsDeleted("user".into()))?; 107 | } 108 | subject.set_deleted(Some(now.clone())); 109 | Ok(Modifications::new_single(Op::Delete, subject)) 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | use crate::{ 116 | access::Role, 117 | models::{ 118 | user::User, 119 | }, 120 | util::{self, test::{self, *}}, 121 | }; 122 | 123 | #[test] 124 | fn can_create() { 125 | let id = UserID::create(); 126 | let account_id = AccountID::create(); 127 | let now = util::time::now(); 128 | let mods = create(id.clone(), "zing@lyonbros.com", "leonard", account_id.clone(), true, &now).unwrap().into_vec(); 129 | assert_eq!(mods.len(), 2); 130 | 131 | let model = mods[0].clone().expect_op::(Op::Create).unwrap(); 132 | let account = mods[1].clone().expect_op::(Op::Create).unwrap(); 133 | assert_eq!(model.id(), &id); 134 | assert_eq!(model.email(), "zing@lyonbros.com"); 135 | assert_eq!(model.name(), "leonard"); 136 | assert_eq!(model.active(), &true); 137 | 138 | assert_eq!(account.id(), &account_id); 139 | assert_eq!(account.user_ids(), &vec![id.clone()]); 140 | assert_eq!(account.name(), "UBI"); 141 | assert_eq!(account.balance(), &num!(0)); 142 | assert_eq!(account.ubi(), &Some(Ubi::new(now.clone()))); 143 | assert_eq!(account.created(), &now); 144 | assert_eq!(account.updated(), &now); 145 | assert_eq!(account.deleted(), &None); 146 | } 147 | 148 | #[test] 149 | fn can_create_permissioned() { 150 | let id = UserID::create(); 151 | let account_id = AccountID::create(); 152 | let now = util::time::now(); 153 | let mut state = TestState::standard(vec![], &now); 154 | let user = make_user(&id, Some(vec![Role::IdentityAdmin]), &now); 155 | state.user = Some(user); 156 | 157 | let testfn = |state: &TestState| { 158 | create_permissioned(state.user(), id.clone(), vec![Role::User], "zing@lyonbros.com", "leonard", account_id.clone(), true, &now) 159 | }; 160 | 161 | let mods = testfn(&state).unwrap().into_vec(); 162 | assert_eq!(mods.len(), 2); 163 | let model = mods[0].clone().expect_op::(Op::Create).unwrap(); 164 | let account = mods[1].clone().expect_op::(Op::Create).unwrap(); 165 | assert_eq!(model.id(), &id); 166 | assert_eq!(model.email(), "zing@lyonbros.com"); 167 | assert_eq!(model.name(), "leonard"); 168 | assert_eq!(model.active(), &true); 169 | 170 | assert_eq!(account.id(), &account_id); 171 | assert_eq!(account.user_ids(), &vec![id.clone()]); 172 | assert_eq!(account.name(), "UBI"); 173 | assert_eq!(account.balance(), &num!(0)); 174 | assert_eq!(account.ubi(), &Some(Ubi::new(now.clone()))); 175 | assert_eq!(account.created(), &now); 176 | assert_eq!(account.updated(), &now); 177 | assert_eq!(account.deleted(), &None); 178 | 179 | let mut state2 = state.clone(); 180 | state2.user_mut().set_roles(vec![Role::User]); 181 | let res = testfn(&state2); 182 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 183 | } 184 | 185 | #[test] 186 | fn can_update() { 187 | let id = UserID::create(); 188 | let account_id = AccountID::create(); 189 | let now = util::time::now(); 190 | let mut state = TestState::standard(vec![], &now); 191 | let user = make_user(&id, Some(vec![Role::IdentityAdmin]), &now); 192 | let mods = create_permissioned(&user, id.clone(), vec![Role::User], "zing@lyonbros.com", "leonard", account_id.clone(), true, &now).unwrap().into_vec(); 193 | let new_user = mods[0].clone().expect_op::(Op::Create).unwrap(); 194 | state.user = Some(user); 195 | state.model = Some(new_user); 196 | 197 | let now2 = util::time::now(); 198 | let testfn_inner = |state: &TestState, active: Option| { 199 | update(state.user(), state.model().clone(), Some("obvious_day@camp.stupid".into()), None, active, &now2) 200 | }; 201 | let testfn = |state: &TestState| { 202 | testfn_inner(state, None) 203 | }; 204 | 205 | let mods = testfn(&state).unwrap().into_vec(); 206 | let user2 = mods[0].clone().expect_op::(Op::Update).unwrap(); 207 | assert_eq!(user2.email(), "obvious_day@camp.stupid"); 208 | assert_eq!(user2.name(), "leonard"); 209 | assert_eq!(user2.active(), &true); 210 | assert_eq!(user2.updated(), &now2); 211 | 212 | let mut state2 = state.clone(); 213 | state2.user = Some(user2.clone()); 214 | state2.model = Some(user2); 215 | let mods = testfn_inner(&state2, Some(false)).unwrap().into_vec(); 216 | let user3 = mods[0].clone().expect_op::(Op::Update).unwrap(); 217 | assert_eq!(user3.email(), "obvious_day@camp.stupid"); 218 | assert_eq!(user3.name(), "leonard"); 219 | assert_eq!(user3.active(), &false); 220 | assert_eq!(user3.updated(), &now2); 221 | 222 | let mut state3 = state.clone(); 223 | state3.user = Some(user3.clone()); 224 | let res = testfn(&state3); 225 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 226 | } 227 | 228 | #[test] 229 | fn can_set_roles() { 230 | let id = UserID::create(); 231 | let now = util::time::now(); 232 | let mut state = TestState::standard(vec![], &now); 233 | let user = make_user(&id, Some(vec![Role::IdentityAdmin]), &now); 234 | state.user = Some(user.clone()); 235 | state.model = Some(user); 236 | 237 | let now2 = util::time::now(); 238 | let testfn = |state: &TestState| { 239 | set_roles(state.user(), state.model().clone(), vec![Role::User], &now2) 240 | }; 241 | 242 | let mods = testfn(&state).unwrap().into_vec(); 243 | assert_eq!(mods.len(), 1); 244 | 245 | let user2 = mods[0].clone().expect_op::(Op::Update).unwrap(); 246 | assert_eq!(user2.id(), &id); 247 | assert_eq!(user2.roles(), &vec![Role::User]); 248 | assert_eq!(user2.updated(), &now2); 249 | 250 | // the user changed their roles to not allow setting roles, so when they 251 | // try to set their roles back to identity admin it shuould fail lol 252 | // sucker. 253 | let mut state2 = state.clone(); 254 | state2.user = Some(user2.clone()); 255 | state2.model = Some(user2); 256 | let res = testfn(&state2); 257 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 258 | 259 | // inactive users should not be able to run mods 260 | let mut state3 = state.clone(); 261 | state3.user_mut().set_active(false); 262 | let res = testfn(&state3); 263 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 264 | } 265 | 266 | #[test] 267 | fn can_delete() { 268 | let id = UserID::create(); 269 | let now = util::time::now(); 270 | let mut state = TestState::standard(vec![], &now); 271 | let user = make_user(&id, Some(vec![Role::IdentityAdmin]), &now); 272 | state.user = Some(user.clone()); 273 | state.model = Some(user); 274 | 275 | let testfn = |state: &TestState| { 276 | delete(state.user(), state.model().clone(), &now) 277 | }; 278 | test::double_deleted_tester(&state, "user", &testfn); 279 | 280 | let mods = testfn(&state).unwrap().into_vec(); 281 | assert_eq!(mods.len(), 1); 282 | let user2 = mods[0].clone().expect_op::(Op::Delete).unwrap(); 283 | assert_eq!(user2.deleted(), &Some(now.clone())); 284 | 285 | let mut state2 = state.clone(); 286 | state2.user = Some(user2.clone()); 287 | state2.model = Some(user2); 288 | let res = testfn(&state2); 289 | assert_eq!(res, Err(Error::InsufficientPrivileges)); 290 | } 291 | } 292 | 293 | -------------------------------------------------------------------------------- /src/util/measure.rs: -------------------------------------------------------------------------------- 1 | //! Some helpful utilities for dealing with om2::Measure objects in the context 2 | //! of event processing. 3 | 4 | use crate::{ 5 | error::{Error, Result}, 6 | }; 7 | use om2::{Measure, NumericUnion}; 8 | use rust_decimal::prelude::*; 9 | 10 | /// Decrement a Measure by some other Measure. 11 | /// 12 | /// This will fail if the Measure being decremented falls below zero or if the 13 | /// two Measures have units that don't match. 14 | /// 15 | /// Returns true if the first Measure was modified. 16 | pub fn dec_measure(measure: &mut Measure, dec_by: &Measure) -> Result { 17 | if measure.has_unit() != dec_by.has_unit() { 18 | Err(Error::MeasureUnitsMismatched)?; 19 | } 20 | let from_quantity = measure.has_numerical_value().clone(); 21 | let dec_quantity = dec_by.has_numerical_value().clone(); 22 | if dec_quantity.is_zero() { 23 | return Ok(false); 24 | } 25 | if dec_quantity.is_negative() { 26 | Err(Error::NegativeMeasurement)?; 27 | } 28 | let remaining = from_quantity.clone().sub(dec_quantity.clone()) 29 | .map_err(|e| Error::NumericUnionOpError(e))?; 30 | if remaining.is_negative() { 31 | Err(Error::NegativeMeasurement)?; 32 | } 33 | measure.set_has_numerical_value(remaining); 34 | Ok(true) 35 | } 36 | 37 | /// Increment a Measure by some other Measure. 38 | /// 39 | /// This will fail if the Measure being decremented falls below zero or if the 40 | /// two Measures have units that don't match. 41 | /// 42 | /// Returns true if the first Measure was modified. 43 | pub fn inc_measure(measure: &mut Measure, inc_by: &Measure) -> Result { 44 | if measure.has_unit() != inc_by.has_unit() { 45 | Err(Error::MeasureUnitsMismatched)?; 46 | } 47 | let from_quantity = measure.has_numerical_value().clone(); 48 | let inc_quantity = inc_by.has_numerical_value().clone(); 49 | if inc_quantity.is_zero() { 50 | return Ok(false); 51 | } 52 | if inc_quantity.is_negative() { 53 | Err(Error::NegativeMeasurement)?; 54 | } 55 | let added = from_quantity.clone().add(inc_quantity.clone()) 56 | .map_err(|e| Error::NumericUnionOpError(e))?; 57 | if added.is_negative() { 58 | Err(Error::NegativeMeasurement)?; 59 | } 60 | measure.set_has_numerical_value(added); 61 | Ok(true) 62 | } 63 | 64 | /// Either use the given `measure` if it exists, or create a measure of 0 and 65 | /// return it using the same units/numeric types as `default`. 66 | pub fn unwrap_or_zero(measure: &Option, default: &Measure) -> Measure { 67 | measure 68 | .clone() 69 | .unwrap_or_else(|| { 70 | let unit = default.has_unit().clone(); 71 | let numeric = match default.has_numerical_value().clone() { 72 | NumericUnion::Decimal(_) => NumericUnion::Decimal(Decimal::zero()), 73 | NumericUnion::Double(_) => NumericUnion::Double(f64::zero()), 74 | NumericUnion::Float(_) => NumericUnion::Float(f32::zero()), 75 | NumericUnion::Integer(_) => NumericUnion::Integer(i64::zero()), 76 | }; 77 | Measure::new(numeric, unit) 78 | }) 79 | } 80 | 81 | /// Set a Measure's count to zero (preserves Unit and NumericUnion types). 82 | pub fn set_zero(measure: &mut Measure) { 83 | let num = match measure.has_numerical_value() { 84 | NumericUnion::Decimal(_) => NumericUnion::Decimal(Zero::zero()), 85 | NumericUnion::Double(_) => NumericUnion::Double(Zero::zero()), 86 | NumericUnion::Float(_) => NumericUnion::Float(Zero::zero()), 87 | NumericUnion::Integer(_) => NumericUnion::Integer(Zero::zero()), 88 | }; 89 | measure.set_has_numerical_value(num); 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | //! A set of utility structs and functions used when operating the core. 2 | 3 | pub(crate) mod measure; 4 | #[macro_use] 5 | pub mod number; 6 | pub(crate) mod time; 7 | 8 | #[cfg(test)] 9 | #[macro_use] 10 | pub(crate) mod test; 11 | 12 | -------------------------------------------------------------------------------- /src/util/number.rs: -------------------------------------------------------------------------------- 1 | //! A set of utilities for working with numbers in the Basis costs system. 2 | 3 | use crate::{ 4 | costs::Costs, 5 | error::{Error, Result}, 6 | }; 7 | use rust_decimal::prelude::*; 8 | #[cfg(feature = "with_serde")] 9 | use serde::{Serialize, Deserialize}; 10 | use std::ops::Mul; 11 | 12 | /// Create a number used in the costing system. Internal use only. 13 | /// 14 | /// This is mostly a wrapper around a standard number type that makes it easier 15 | /// to swap out test values/Costs types project-wide without having to change 16 | /// each instance by hand, but can also be used by callers of the core to create 17 | /// numbers more seamlessly. 18 | /// 19 | /// ```rust 20 | /// use basis_core::{ 21 | /// costs::Costs, 22 | /// models::occupation::OccupationID, 23 | /// num 24 | /// }; 25 | /// let costs = Costs::new_with_labor(OccupationID::new("plumber"), num!(45.8)); 26 | /// ``` 27 | /// 28 | /// Right now, this wraps `rust_decimal::Decimal`'s `dec!()` macro. 29 | macro_rules! num { 30 | ($val:expr) => { 31 | rust_decimal_macros::dec!($val) 32 | } 33 | } 34 | 35 | /// Represents a ratio: a value such that `0 <= v <= 1`. 36 | #[derive(Clone, Default, Debug, PartialEq)] 37 | #[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] 38 | pub struct Ratio { 39 | /// The inner ratio value. 40 | inner: Decimal, 41 | } 42 | 43 | impl Ratio { 44 | /// Create a new ratio from a Decimal. 45 | /// 46 | /// If the given `ratio_val` is outside the range 0 <= r <= 1 then we return 47 | /// `Error::InvalidRatio`. 48 | pub fn new>(ratio_val: T) -> Result { 49 | let ratio: Decimal = ratio_val.into(); 50 | if ratio < Decimal::zero() || ratio > num!(1) { 51 | Err(Error::InvalidRatio(ratio))?; 52 | } 53 | Ok(Self { 54 | inner: ratio, 55 | }) 56 | } 57 | 58 | /// Grab this ratio's inner value 59 | pub fn inner(&self) -> &Decimal { 60 | &self.inner 61 | } 62 | } 63 | 64 | impl Mul for Ratio { 65 | type Output = Costs; 66 | 67 | fn mul(self, rhs: Costs) -> Costs { 68 | rhs * self.inner().clone() 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::*; 75 | 76 | #[test] 77 | fn create_ratio() { 78 | Ratio::new(0).unwrap(); 79 | Ratio::new(1).unwrap(); 80 | Ratio::new(num!(0.999999999999999999999999)).unwrap(); 81 | Ratio::new(num!(0.000000000000000000000001)).unwrap(); 82 | Ratio::new(num!(0.5050)).unwrap(); 83 | assert_eq!(Ratio::new(2), Err(Error::InvalidRatio(num!(2)))); 84 | assert_eq!(Ratio::new(-1), Err(Error::InvalidRatio(num!(-1)))); 85 | let val = num!(1.0000000000000000000000001); 86 | assert_eq!(Ratio::new(val.clone()), Err(Error::InvalidRatio(val))); 87 | let val = num!(-0.0000000000000000000000001); 88 | assert_eq!(Ratio::new(val.clone()), Err(Error::InvalidRatio(val))); 89 | } 90 | 91 | #[test] 92 | fn can_multiply_ratio() { 93 | let ratio = Ratio::new(num!(0.5)).unwrap(); 94 | let mut costs = Costs::new(); 95 | costs.track_labor("machinist", num!(16.8)); 96 | costs.track_resource("steel", num!(5000), num!(0.004)); 97 | let mut costs2 = Costs::new(); 98 | costs2.track_labor("machinist", num!(8.4)); 99 | costs2.track_resource("steel", num!(2500), num!(0.004)); 100 | assert_eq!(ratio * costs, costs2); 101 | 102 | let ratio = Ratio::new(num!(0.833912)).unwrap(); 103 | let mut costs = Costs::new(); 104 | costs.track_labor("machinist", num!(73.99)); 105 | costs.track_resource("steel", num!(8773), num!(0.003)); 106 | let mut costs2 = Costs::new(); 107 | costs2.track_labor("machinist", num!(73.99) * num!(0.833912)); 108 | costs2.track_resource("steel", num!(8773) * num!(0.833912), num!(0.003)); 109 | assert_eq!(costs * ratio, costs2); 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /src/util/time.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | 3 | /// Determine if a year (eg `2017`) is a leap year. 4 | #[allow(dead_code)] 5 | pub(crate) fn is_leap_year(year: i64) -> bool { 6 | ((year % 4) == 0 && (year % 100) != 0) || (year % 400) == 0 7 | } 8 | 9 | /// Get the current time. Mainly for testing. 10 | #[allow(dead_code)] 11 | pub(crate) fn now() -> DateTime { 12 | Utc::now() 13 | } 14 | 15 | --------------------------------------------------------------------------------