diff --git a/arma/server/addons/bank/XEH_preInit.sqf b/arma/server/addons/bank/XEH_preInit.sqf index c623f86..9e2fd5c 100644 --- a/arma/server/addons/bank/XEH_preInit.sqf +++ b/arma/server/addons/bank/XEH_preInit.sqf @@ -35,12 +35,6 @@ PREP_RECOMPILE_END; GVAR(BankStore) call ["deposit", [_uid, _amount]]; }] call CFUNC(addEventHandler); -[QGVAR(requestPayment), { - params [["_uid", "", [""]], ["_amount", 0, [0]]]; - - GVAR(BankStore) call ["payment", [_uid, _amount]]; -}] call CFUNC(addEventHandler); - [QGVAR(requestSubmitPin), { params [["_uid", "", [""]], ["_pin", "", [""]]]; diff --git a/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf b/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf index fcd4f49..517bff3 100644 --- a/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf +++ b/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf @@ -37,6 +37,14 @@ GVAR(BankPayloadBuilder) = createHashMapObject [[ _context set ["fromField", _from]; _context }], + ["buildCheckoutContext", compileFinal { + params [["_source", "bank", [""]], ["_commit", false, [false]]]; + + createHashMapFromArray [ + ["commit", _commit], + ["sourceField", toLowerANSI _source] + ] + }], ["resolveOrgState", compileFinal { params [["_uid", "", [""]]]; diff --git a/arma/server/addons/bank/functions/fnc_initStore.sqf b/arma/server/addons/bank/functions/fnc_initStore.sqf index 8f7b9e0..4ad374c 100644 --- a/arma/server/addons/bank/functions/fnc_initStore.sqf +++ b/arma/server/addons/bank/functions/fnc_initStore.sqf @@ -145,35 +145,23 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [ params [["_uid", "", [""]], ["_source", "cash", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]]; private _result = createHashMapFromArray [["success", false], ["message", "Unable to process bank payment."], ["patch", createHashMap]]; - private _field = switch (toLowerANSI _source) do { - case "cash": { "cash" }; - case "bank": { "bank" }; - default { "" }; - }; + if (_uid isEqualTo "") exitWith { _result }; - if (_field isEqualTo "") exitWith { - _result set ["message", "Selected bank payment source is unsupported."]; + private _checkoutContext = GVAR(BankPayloadBuilder) call ["buildCheckoutContext", [_source, _commit]]; + private _envelope = _self call [ + "callHotBankEnvelope", + [ + "bank:hot:charge_checkout", + [_uid, str _amount, toJSON _checkoutContext] + ] + ]; + private _mutationResult = _envelope getOrDefault ["data", createHashMap]; + private _patch = _self call ["finalizeMutation", [_uid, _mutationResult, false]]; + if (_patch isEqualTo createHashMap) exitWith { + _result set ["message", _envelope getOrDefault ["error", "Bank checkout payment failed."]]; _result }; - private _account = _self call ["get", [_uid, ""]]; - if (_account isEqualTo createHashMap) exitWith { - _result set ["message", "Bank account data is unavailable for checkout."]; - _result - }; - - private _balance = _account getOrDefault [_field, 0]; - if (_balance < _amount) exitWith { - _result set ["message", ["Bank balance cannot cover this checkout.", "Cash on hand cannot cover this checkout."] select (_field isEqualTo "cash")]; - _result - }; - - private _patch = createHashMapFromArray [[_field, (_balance - _amount)]]; - if (_commit) then { - private _result = _self call ["callHotBank", ["bank:hot:patch", [_uid, toJSON _patch]]]; - _patch = _self call ["finalizeMutation", [_uid, _result, false]]; - }; - _result set ["success", true]; _result set ["message", ""]; _result set ["patch", _patch]; @@ -214,17 +202,19 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [ private _playerName = if (isNull _player) then { "Unknown" } else { name _player }; ["bank:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to check if bank account %1 exists! Using fallback account.", _uid]] call EFUNC(common,log); - - private _fallbackAccount = GVAR(BankModel) call ["fromPlayer", [_player]]; - _fallbackAccount set ["uid", _uid]; - if ((_fallbackAccount getOrDefault ["name", ""]) isEqualTo "") then { - _fallbackAccount set ["name", _playerName]; - }; - - _fallbackAccount = _self call ["normalizeAccount", [_uid, _fallbackAccount, _playerName]]; - GVAR(BankMessenger) call ["sendAccountSync", [_uid, _fallbackAccount, CRPC(bank,responseInitBank)]]; - _fallbackAccount + ["ERROR", format ["Failed to check if bank account %1 exists in backend.", _uid]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank backend is unavailable right now."]]; + createHashMap + }; + if !(_result isEqualType "") exitWith { + ["ERROR", format ["Bank exists check for %1 returned an invalid response.", _uid]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank backend returned an invalid response."]]; + createHashMap + }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Bank exists check for %1 failed: %2", _uid, _result]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _result select [7]]]; + createHashMap }; private _finalAccount = createHashMap; @@ -241,23 +231,29 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [ private _json = _self call ["toJSON", [_finalAccount]]; ["bank:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"]; if (!_createSuccess) exitWith { - ["ERROR", format ["Failed to create bank account %1! Using fallback account.", _uid]] call EFUNC(common,log); - - _finalAccount = _self call ["normalizeAccount", [_uid, _finalAccount, _playerName]]; - GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalAccount, CRPC(bank,responseInitBank)]]; - _finalAccount + ["ERROR", format ["Failed to create bank account %1 in backend.", _uid]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Failed to create bank account in backend."]]; + createHashMap + }; + if !(_createResult isEqualType "") exitWith { + ["ERROR", format ["Bank create for %1 returned an invalid response.", _uid]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank backend returned an invalid create response."]]; + createHashMap + }; + if ((_createResult find "Error:") == 0) exitWith { + ["ERROR", format ["Bank create for %1 failed: %2", _uid, _createResult]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _createResult select [7]]]; + createHashMap }; _finalAccount = _self call ["loadHotBank", [_uid, true, _playerName]]; ["INFO", format ["Created new bank account for %1", _uid]] call EFUNC(common,log); }; - if (_finalAccount isEqualTo createHashMap) then { - _finalAccount = GVAR(BankModel) call ["fromPlayer", [_player]]; - _finalAccount set ["uid", _uid]; - if ((_finalAccount getOrDefault ["name", ""]) isEqualTo "") then { - _finalAccount set ["name", _playerName]; - }; + if (_finalAccount isEqualTo createHashMap) exitWith { + ["ERROR", format ["Failed to initialize bank hot state for %1.", _uid]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank account hot state could not be initialized."]]; + createHashMap }; _finalAccount = _self call ["normalizeAccount", [_uid, _finalAccount, _playerName]]; @@ -294,25 +290,17 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [ params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { createHashMap }; - private _account = _self call ["callHotBank", ["bank:hot:save", [_uid]]]; - if (_account isEqualTo createHashMap) exitWith { _account }; + private _envelope = _self call ["callHotBankEnvelope", ["bank:hot:save", [_uid]]]; + private _account = _envelope getOrDefault ["data", createHashMap]; + if (_account isEqualTo createHashMap) exitWith { + private _message = _envelope getOrDefault ["error", "Bank save failed."]; + ["ERROR", format ["Failed to save bank account %1: %2", _uid, _message]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _message]]; + createHashMap + }; _self call ["normalizeAccount", [_uid, _account, ""]] }], - ["payment", compileFinal { - params [["_uid", "", [""]], ["_amount", 0, [0]]]; - - _self call [ - "runMutation", - [ - _uid, - "bank:hot:payment", - [_uid, str _amount], - false, - format ["Paid $%1", [_amount] call EFUNC(common,formatNumber)] - ] - ] - }], ["transfer", compileFinal { params [["_uid", "", [""]], ["_target", "", [""]], ["_amount", 0, [0]], ["_context", createHashMap, [createHashMap]]]; @@ -325,7 +313,13 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [ ] ]; private _result = _envelope getOrDefault ["data", createHashMap]; - if (_result isEqualTo createHashMap) exitWith { false }; + if (_result isEqualTo createHashMap) exitWith { + private _message = _envelope getOrDefault ["error", "Bank transfer failed."]; + if (_message isNotEqualTo "") then { + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _message]]; + }; + false + }; private _sourceAccount = _result getOrDefault ["sourceAccount", createHashMap]; private _targetAccount = _result getOrDefault ["targetAccount", createHashMap]; diff --git a/arma/server/extension/src/actor.rs b/arma/server/extension/src/actor.rs index 1158926..e621056 100644 --- a/arma/server/extension/src/actor.rs +++ b/arma/server/extension/src/actor.rs @@ -9,6 +9,7 @@ use forge_services::{ActorHotStateService, ActorService}; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -104,8 +105,13 @@ pub(crate) fn save_hot_actor(call_context: CallContext, key: String) -> String { None => return format!("Error: Failed to resolve UID for key: {}", key), }; - match HOT_ACTOR_SERVICE.save_actor(resolved_uid) { - Ok(saved_actor) => serialize_hot_actor(saved_actor), + match HOT_ACTOR_SERVICE.get_actor(resolved_uid.clone()) { + Ok(actor) => { + enqueue_persistence_task("actor", move || { + HOT_ACTOR_SERVICE.save_actor(resolved_uid).map(|_| ()) + }); + serialize_hot_actor(actor) + } Err(error) => format!("Error: {}", error), } } diff --git a/arma/server/extension/src/bank.rs b/arma/server/extension/src/bank.rs index 3f591a5..dce90e0 100644 --- a/arma/server/extension/src/bank.rs +++ b/arma/server/extension/src/bank.rs @@ -5,14 +5,15 @@ use arma_rs::{CallContext, Group}; use forge_models::{ - BankMutationResult, BankOperationContext, BankPinContext, BankTransferContext, - BankTransferResult, + BankCheckoutContext, BankMutationResult, BankOperationContext, BankPinContext, + BankTransferContext, BankTransferResult, }; use forge_repositories::{InMemoryBankHotRepository, RedisBankRepository}; use forge_services::{BankHotStateService, BankService}; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -51,9 +52,9 @@ pub fn group() -> Group { .command("get", get_hot_bank) .command("override", override_hot_bank) .command("patch", patch_hot_bank) + .command("charge_checkout", charge_checkout_hot_bank) .command("deposit", deposit_hot_bank) .command("withdraw", withdraw_hot_bank) - .command("payment", payment_hot_bank) .command("deposit_earnings", deposit_earnings_hot_bank) .command("transfer", transfer_hot_bank) .command("validate_pin", validate_pin_hot_bank) @@ -99,6 +100,11 @@ fn parse_transfer_context(json_context: String) -> Result Result { + serde_json::from_str(&json_context) + .map_err(|error| format!("Invalid bank checkout context: {}", error)) +} + fn parse_pin_context(json_context: String) -> Result { serde_json::from_str(&json_context) .map_err(|error| format!("Invalid bank PIN context: {}", error)) @@ -156,6 +162,32 @@ pub(crate) fn patch_hot_bank(call_context: CallContext, key: String, json_patch: } } +pub(crate) fn charge_checkout_hot_bank( + call_context: CallContext, + key: String, + amount: String, + json_context: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let amount = match parse_amount(amount, "checkout") { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + let context = match parse_checkout_context(json_context) { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + + match HOT_BANK_SERVICE.charge_checkout(resolved_uid, amount, context) { + Ok(result) => serialize_hot_bank_mutation(result), + Err(error) => format!("Error: {}", error), + } +} + pub(crate) fn deposit_hot_bank( call_context: CallContext, key: String, @@ -208,23 +240,6 @@ pub(crate) fn withdraw_hot_bank( } } -pub(crate) fn payment_hot_bank(call_context: CallContext, key: String, amount: String) -> String { - let resolved_uid = match resolve_uid(&key, &call_context) { - Some(uid) => uid, - None => return format!("Error: Failed to resolve UID for key: {}", key), - }; - - let amount = match parse_amount(amount, "payment") { - Ok(value) => value, - Err(error) => return format!("Error: {}", error), - }; - - match HOT_BANK_SERVICE.payment(resolved_uid, amount) { - Ok(result) => serialize_hot_bank_mutation(result), - Err(error) => format!("Error: {}", error), - } -} - pub(crate) fn deposit_earnings_hot_bank( call_context: CallContext, key: String, @@ -308,8 +323,13 @@ pub(crate) fn save_hot_bank(call_context: CallContext, key: String) -> String { None => return format!("Error: Failed to resolve UID for key: {}", key), }; - match HOT_BANK_SERVICE.save_bank(resolved_uid) { - Ok(saved_bank) => serialize_hot_bank(saved_bank), + match HOT_BANK_SERVICE.get_bank(resolved_uid.clone()) { + Ok(bank) => { + enqueue_persistence_task("bank", move || { + HOT_BANK_SERVICE.save_bank(resolved_uid).map(|_| ()) + }); + serialize_hot_bank(bank) + } Err(error) => format!("Error: {}", error), } } diff --git a/arma/server/extension/src/garage.rs b/arma/server/extension/src/garage.rs index b7b9414..dcc6204 100644 --- a/arma/server/extension/src/garage.rs +++ b/arma/server/extension/src/garage.rs @@ -10,6 +10,7 @@ use std::collections::HashMap; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -105,8 +106,13 @@ pub(crate) fn save_hot_garage(call_context: CallContext, key: String) -> String None => return format!("Error: Failed to resolve UID for key: {}", key), }; - match HOT_GARAGE_SERVICE.save_garage(resolved_uid) { - Ok(saved_garage) => serialize_hot_vehicles(saved_garage), + match HOT_GARAGE_SERVICE.get_garage(resolved_uid.clone()) { + Ok(garage) => { + enqueue_persistence_task("garage", move || { + HOT_GARAGE_SERVICE.save_garage(resolved_uid).map(|_| ()) + }); + serialize_hot_vehicles(garage) + } Err(error) => format!("Error: {}", error), } } diff --git a/arma/server/extension/src/lib.rs b/arma/server/extension/src/lib.rs index a44717c..4c1fabe 100644 --- a/arma/server/extension/src/lib.rs +++ b/arma/server/extension/src/lib.rs @@ -37,7 +37,7 @@ static CONTEXT: LazyLock>> = LazyLock::new(|| TokioR static REDIS_POOL: OnceLock = OnceLock::new(); /// Global multi-threaded Tokio runtime used to execute async operations from /// command handlers and startup tasks. -static RUNTIME: LazyLock = LazyLock::new(|| { +pub(crate) static RUNTIME: LazyLock = LazyLock::new(|| { Builder::new_multi_thread() .enable_all() .build() @@ -54,6 +54,21 @@ enum ConnectionState { static CONNECTION_STATE: LazyLock> = LazyLock::new(|| StdRwLock::new(ConnectionState::Initializing)); +pub(crate) fn enqueue_persistence_task(module: &'static str, job: F) +where + F: FnOnce() -> Result<(), String> + Send + 'static, +{ + RUNTIME.spawn_blocking(move || { + if let Err(error) = job() { + crate::log::log( + module, + "ERROR", + &format!("Async persistence failed: {}", error), + ); + } + }); +} + #[arma] /// Initializes the extension, registers commands/groups, and asynchronously /// creates the Redis connection pool on the global runtime. diff --git a/arma/server/extension/src/locker.rs b/arma/server/extension/src/locker.rs index 3244f78..99ea271 100644 --- a/arma/server/extension/src/locker.rs +++ b/arma/server/extension/src/locker.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -98,8 +99,13 @@ pub(crate) fn save_hot_locker(call_context: CallContext, key: String) -> String None => return format!("Error: Failed to resolve UID for key: {}", key), }; - match HOT_LOCKER_SERVICE.save_locker(resolved_uid) { - Ok(saved_locker) => serialize_hot_items(saved_locker), + match HOT_LOCKER_SERVICE.get_locker(resolved_uid.clone()) { + Ok(locker) => { + enqueue_persistence_task("locker", move || { + HOT_LOCKER_SERVICE.save_locker(resolved_uid).map(|_| ()) + }); + serialize_hot_items(locker) + } Err(error) => format!("Error: {}", error), } } diff --git a/arma/server/extension/src/org.rs b/arma/server/extension/src/org.rs index 3c3eae7..ac2fe06 100644 --- a/arma/server/extension/src/org.rs +++ b/arma/server/extension/src/org.rs @@ -10,6 +10,7 @@ use forge_services::{OrgHotStateService, OrgService}; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::log::log; /// Global organization service instance. @@ -104,8 +105,11 @@ pub(crate) fn override_hot_org(org_id: String, json_data: String) -> String { } pub(crate) fn save_hot_org(org_id: String) -> String { - match HOT_ORG_SERVICE.save_org(org_id) { - Ok(org) => serialize_hot_org(org), + match HOT_ORG_SERVICE.get_org(org_id.clone()) { + Ok(org) => { + enqueue_persistence_task("org", move || HOT_ORG_SERVICE.save_org(org_id).map(|_| ())); + serialize_hot_org(org) + } Err(error) => format!("Error: {}", error), } } diff --git a/arma/server/extension/src/transport.rs b/arma/server/extension/src/transport.rs index 4e2f6c6..1ebbfe2 100644 --- a/arma/server/extension/src/transport.rs +++ b/arma/server/extension/src/transport.rs @@ -253,6 +253,15 @@ fn route_command( arguments[1].clone(), )) } + "bank:hot:charge_checkout" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(bank::charge_checkout_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } "bank:hot:deposit" => { expect_arg_count(function_name, &arguments, 3)?; Ok(bank::deposit_hot_bank( @@ -271,14 +280,6 @@ fn route_command( arguments[2].clone(), )) } - "bank:hot:payment" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(bank::payment_hot_bank( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } "bank:hot:deposit_earnings" => { expect_arg_count(function_name, &arguments, 3)?; Ok(bank::deposit_earnings_hot_bank( diff --git a/arma/server/extension/src/v_garage.rs b/arma/server/extension/src/v_garage.rs index 34900ac..5e55594 100644 --- a/arma/server/extension/src/v_garage.rs +++ b/arma/server/extension/src/v_garage.rs @@ -5,6 +5,7 @@ use forge_services::{VGarageHotStateService, VGarageService}; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -121,8 +122,13 @@ pub(crate) fn save_hot_vgarage(call_context: CallContext, key: String) -> String None => return format!("Error: Failed to resolve UID for key: {}", key), }; - match HOT_VGARAGE_SERVICE.save_garage(&resolved_uid) { - Ok(garage) => serialize_hot_vgarage(garage), + match HOT_VGARAGE_SERVICE.fetch_garage(&resolved_uid) { + Ok(garage) => { + enqueue_persistence_task("owned_garage", move || { + HOT_VGARAGE_SERVICE.save_garage(&resolved_uid).map(|_| ()) + }); + serialize_hot_vgarage(garage) + } Err(error) => format!("Error: {}", error), } } diff --git a/arma/server/extension/src/v_locker.rs b/arma/server/extension/src/v_locker.rs index f863f67..22d06be 100644 --- a/arma/server/extension/src/v_locker.rs +++ b/arma/server/extension/src/v_locker.rs @@ -5,6 +5,7 @@ use forge_services::{VLockerHotStateService, VLockerService}; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -120,8 +121,13 @@ pub(crate) fn save_hot_vlocker(call_context: CallContext, key: String) -> String None => return format!("Error: Failed to resolve UID for key: {}", key), }; - match HOT_VLOCKER_SERVICE.save_locker(&resolved_uid) { - Ok(locker) => serialize_hot_vlocker(locker), + match HOT_VLOCKER_SERVICE.fetch_locker(&resolved_uid) { + Ok(locker) => { + enqueue_persistence_task("owned_locker", move || { + HOT_VLOCKER_SERVICE.save_locker(&resolved_uid).map(|_| ()) + }); + serialize_hot_vlocker(locker) + } Err(error) => format!("Error: {}", error), } } diff --git a/lib/models/src/bank.rs b/lib/models/src/bank.rs index 2e33d15..fcabed4 100644 --- a/lib/models/src/bank.rs +++ b/lib/models/src/bank.rs @@ -45,6 +45,13 @@ pub struct BankTransferContext { pub from_field: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankCheckoutContext { + pub source_field: String, + pub commit: bool, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BankPinContext { diff --git a/lib/models/src/lib.rs b/lib/models/src/lib.rs index 9f1f008..4bc404c 100644 --- a/lib/models/src/lib.rs +++ b/lib/models/src/lib.rs @@ -9,8 +9,8 @@ pub mod v_locker; pub use actor::Actor; pub use bank::{ - Bank, BankMutationResult, BankOperationContext, BankPinContext, BankTransferContext, - BankTransferResult, + Bank, BankCheckoutContext, BankMutationResult, BankOperationContext, BankPinContext, + BankTransferContext, BankTransferResult, }; pub use cad::{ CadActivityEntry, CadAssignmentMutationResult, CadDispatchOrderContextSeed, diff --git a/lib/services/src/bank.rs b/lib/services/src/bank.rs index 193bcf0..9d1f00e 100644 --- a/lib/services/src/bank.rs +++ b/lib/services/src/bank.rs @@ -6,8 +6,8 @@ //! For full documentation, architecture, and examples, see the [crate README](../README.md). use forge_models::{ - Bank, BankMutationResult, BankOperationContext, BankPinContext, BankTransferContext, - BankTransferResult, + Bank, BankCheckoutContext, BankMutationResult, BankOperationContext, BankPinContext, + BankTransferContext, BankTransferResult, }; use forge_repositories::{BankHotRepository, BankRepository}; use serde_json::{Value, json}; @@ -96,6 +96,51 @@ impl BankHotStateService { }) } + pub fn charge_checkout( + &self, + key: String, + amount: f64, + context: BankCheckoutContext, + ) -> Result { + if amount <= 0.0 { + return Err("Checkout amount must be greater than zero".to_string()); + } + + let mut bank = self.get_bank(key)?; + let source_field = match context.source_field.trim().to_ascii_lowercase().as_str() { + "cash" => "cash", + "bank" => "bank", + _ => return Err("Selected bank payment source is unsupported.".to_string()), + }; + + let source_balance = match source_field { + "cash" => bank.cash, + _ => bank.bank, + }; + if source_balance < amount { + return Err(match source_field { + "cash" => "Cash on hand cannot cover this checkout.".to_string(), + _ => "Bank balance cannot cover this checkout.".to_string(), + }); + } + + match source_field { + "cash" => bank.cash -= amount, + _ => bank.bank -= amount, + } + + bank.validate() + .map_err(|e| format!("Validation failed: {}", e))?; + if context.commit { + self.repository.save(&bank)?; + } + + Ok(BankMutationResult { + account: bank.clone(), + patch: build_patch(&bank, &[source_field])?, + }) + } + pub fn deposit( &self, key: String, @@ -152,23 +197,6 @@ impl BankHotStateService { }) } - pub fn payment(&self, key: String, amount: f64) -> Result { - if amount <= 0.0 { - return Err("Payment amount must be greater than zero".to_string()); - } - - let mut bank = self.get_bank(key)?; - bank.bank += amount; - bank.validate() - .map_err(|e| format!("Validation failed: {}", e))?; - self.repository.save(&bank)?; - - Ok(BankMutationResult { - account: bank.clone(), - patch: build_patch(&bank, &["bank"])?, - }) - } - pub fn deposit_earnings( &self, key: String,