+This folder documents the Arma client mod. The client side is responsible for
+displaying UI, handling player input, caching client-visible state, and sending
+CBA events to server addons.
-
+Authoritative gameplay state lives on the server side or in the Rust extension.
+Client repositories should be treated as view state, not durable storage.
-# Initial Project Setup!
+## Architecture
+- Each addon declares its own UI resources and CBA extended event handlers.
+- `XEH_preStart.sqf`/`XEH_preInit.sqf` compile functions.
+- `XEH_postInitClient.sqf` initializes client repositories, UI bridges, and
+ response event handlers.
+- Browser UIs send JSON events through A3API.
+- SQF handlers translate browser events into local actions or server RPCs.
+- Server responses update repositories and push browser events back into the UI.
-Delete this section after the project has been initially set up:
+## Addon Docs
+- [Main](../addons/main/README.md)
+- [Common](../addons/common/README.md)
+- [Actor](../addons/actor/README.md)
+- [Bank](../addons/bank/README.md)
+- [CAD](../addons/cad/README.md)
+- [Garage](../addons/garage/README.md)
+- [Locker](../addons/locker/README.md)
+- [Notifications](../addons/notifications/README.md)
+- [Organization](../addons/org/README.md)
+- [Phone](../addons/phone/README.md)
+- [Store](../addons/store/README.md)
-1. Find and replace all instances of `forge-client` with the mod's name.
-2. Find and replace all instances of `MOD_REPO` with the mod's name _and no spaces_.
- - This should be the name of the repository on GitHub.
-3. Find and replace all instances of `forge_client` with the mod's prefix.
- - This should be all lowercase.
-4. Find and replace all instances of `MOD_ACRONYM` with the mod's acronym.
- - This should be all uppercase.
-5. After the initial Steam upload, find and replace all instances of `MOD_ID` with the mod's Steam Workshop id.
-
-For third parties, make sure to also replace `IDSolutions` with your Github username / organization name, and to replace `DartRuffian` with your username.
-
-**forge-client** (MOD_ACRONYM) aims to...
-
-The project is entirely **open-source** and any contributions are welcome.
-
-## Core Features
-
-- Feature
-
-## Contributing
-
-For new contributers, see the [Contributing Setup & Guidelines](./.github/CONTRIBUTING.md).
-
-## License
-
-forge-client is licensed under [APL-ND](./LICENSE.md).
+## Related Docs
+- [Root Client Usage Guide](../../../docs/CLIENT_USAGE_GUIDE.md)
+- [Root Client Main Usage Guide](../../../docs/CLIENT_MAIN_USAGE_GUIDE.md)
+- [Root Client Common Usage Guide](../../../docs/CLIENT_COMMON_USAGE_GUIDE.md)
+- [Root Client Actor Usage Guide](../../../docs/CLIENT_ACTOR_USAGE_GUIDE.md)
+- [Root Client Bank Usage Guide](../../../docs/CLIENT_BANK_USAGE_GUIDE.md)
+- [Root Client CAD Usage Guide](../../../docs/CLIENT_CAD_USAGE_GUIDE.md)
+- [Root Client Garage Usage Guide](../../../docs/CLIENT_GARAGE_USAGE_GUIDE.md)
+- [Root Client Locker Usage Guide](../../../docs/CLIENT_LOCKER_USAGE_GUIDE.md)
+- [Root Client Notifications Usage Guide](../../../docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md)
+- [Root Client Organization Usage Guide](../../../docs/CLIENT_ORG_USAGE_GUIDE.md)
+- [Root Client Phone Usage Guide](../../../docs/CLIENT_PHONE_USAGE_GUIDE.md)
+- [Root Client Store Usage Guide](../../../docs/CLIENT_STORE_USAGE_GUIDE.md)
+- [Shared web UI framework notes](../addons/common/WEB_UI_FRAMEWORK.md)
+- [CAD map integration notes](../addons/cad/MAP_README.md)
+- [Root framework docs](../../../docs/README.md)
diff --git a/arma/client/extra/example_addon/README.md b/arma/client/extra/example_addon/README.md
index 40e0345..8448397 100644
--- a/arma/client/extra/example_addon/README.md
+++ b/arma/client/extra/example_addon/README.md
@@ -1,3 +1,9 @@
-# forge_client_addonName
+# Forge Client Example Addon
-Description for this addon
+This directory is a template for creating a new Forge client addon.
+
+Use it as a starting point for addon structure, config layout, event handler
+files, and function preparation. Replace the component names, display strings,
+and placeholder implementation with the new addon's real feature behavior.
+
+Do not ship this example addon as a gameplay module.
diff --git a/arma/server/addons/economy/README.md b/arma/server/addons/economy/README.md
index 51adab8..988ca10 100644
--- a/arma/server/addons/economy/README.md
+++ b/arma/server/addons/economy/README.md
@@ -2,30 +2,93 @@
## Overview
The economy addon contains server-side systems for world economic interactions
-that are still implemented in SQF.
+that are still implemented in SQF. It owns Arma-world behavior such as active
+refueling sessions, medical spawn occupancy, respawn placement, and death
+inventory handling.
-Current stores cover fuel tracking, medical service behavior, and a placeholder
-service economy store.
+Current stores cover fuel tracking, medical service behavior, and service
+charges such as repairs.
## Dependencies
- `forge_server_main`
-- `forge_server_common` at runtime for logging, formatting, and player lookup
-- `forge_server_bank` at runtime for medical service charges
+- `forge_server_common` for logging, formatting, and player lookup
+- `forge_server_bank` for player-funded medical billing
+- `forge_server_org` for extension-backed organization hot-cache charges
- `forge_client_actor` and `forge_client_notifications` for response RPCs
## Main Components
-- `fnc_initFEconomyStore.sqf` tracks active refueling sessions and reports fuel
- totals.
+- `fnc_initFEconomyStore.sqf` tracks active refueling sessions, calculates fuel
+ totals, charges the player's organization through `OrgStore`, syncs the org
+ patch, and rolls fuel back to the starting level when organization funds
+ cannot cover the refuel.
- `fnc_initMEconomyStore.sqf` manages medical spawn occupancy, healing charges,
- respawn placement, death inventory handling, and body-bag transfer.
-- `fnc_initSEconomyStore.sqf` initializes the service economy placeholder.
+ respawn placement, death inventory handling, and body-bag transfer. Medical
+ charges use player bank/cash first, then organization funds with repayable
+ member debt only when the player cannot cover the service.
+- `fnc_initSEconomyStore.sqf` handles organization-funded service charges and
+ repairs. Repairs only apply after the organization charge succeeds. The
+ shared org-charge helper can also record member debt for medical fallback.
## Event Surface
-The addon registers CBA server events for fuel start/tick/stop, player killed,
-player respawn, and healing. Medical store initialization runs after post-init
-to discover configured medical spawn objects.
+The addon registers CBA server events for fuel start/tick/stop, direct refuel
+service, repair service, player killed, player respawn, and healing. Medical
+store initialization runs after post-init to discover configured medical spawn
+objects.
+
+Repair service requests use:
+
+```sqf
+[QEGVAR(economy,RepairService), [_target, _unit, _cost]] call CBA_fnc_serverEvent;
+```
+
+`_cost` is optional. Passing `-1` uses the configured service repair cost.
+
+Garage refuel service requests use:
+
+```sqf
+[QEGVAR(economy,RefuelService), [_target, _unit]] call CBA_fnc_serverEvent;
+```
+
+This fills the selected live vehicle after organization billing succeeds.
+
+## Billing Rules
+Economy does not own durable money state. It coordinates Arma-world effects
+after the relevant hot-cache charge succeeds.
+
+Fuel and repair services are organization-funded:
+
+1. Resolve the player's organization from actor state.
+2. Ensure the player is a member of that organization hot record.
+3. Call `OrgStore chargeCheckout` with `source = "org_funds"`,
+ `commit = true`, and member service charging enabled.
+4. Send the returned organization patch to online members.
+5. If the charge fails, do not complete the service. Refueling rolls the target
+ back to its starting fuel level; repairs are not applied.
+
+Direct refuel service requests, such as those from the garage UI, calculate
+the missing fuel from `fuelCapacity`, charge the organization, and fill the
+vehicle only after the charge succeeds.
+
+Medical services are player-funded first:
+
+1. Load the player's bank hot state.
+2. Charge the player's bank balance when it can cover the medical bill.
+3. Otherwise charge the player's cash when it can cover the bill.
+4. If neither personal balance can cover the bill, charge organization funds
+ and record the same amount as a debt on the player's organization credit
+ line.
+5. If personal billing is unavailable, or both personal and organization funds
+ fail, do not complete the heal.
+
+The organization fallback reduces org funds immediately and adds the medical
+cost to the player's credit-line balance due. Repayment uses the normal bank
+credit-line repayment flow, which moves player bank funds back into the
+organization treasury.
+
+This keeps money mutation rules in the extension-backed organization service
+and bank service while leaving world interactions in SQF.
## Notes
-The service economy store is currently a stub. Fuel and medical behavior should
-stay server-authoritative because they mutate money, inventory, and respawn
-state.
+Fuel, medical, and service world behavior should stay server-authoritative
+because it mutates inventory, vehicles, and respawn state. Money mutations
+should continue to use extension-backed bank and organization hot state.
diff --git a/arma/server/addons/economy/XEH_PREP.hpp b/arma/server/addons/economy/XEH_PREP.hpp
index 6bbcd10..a377c3a 100644
--- a/arma/server/addons/economy/XEH_PREP.hpp
+++ b/arma/server/addons/economy/XEH_PREP.hpp
@@ -1,2 +1,3 @@
PREP(initFEconomyStore);
PREP(initMEconomyStore);
+PREP(initSEconomyStore);
diff --git a/arma/server/addons/economy/XEH_postInit.sqf b/arma/server/addons/economy/XEH_postInit.sqf
index b912379..96be61d 100644
--- a/arma/server/addons/economy/XEH_postInit.sqf
+++ b/arma/server/addons/economy/XEH_postInit.sqf
@@ -1,3 +1,5 @@
#include "script_component.hpp"
-GVAR(MEconomyStore) call ["init", []];
+if !(isNil QGVAR(MEconomyStore)) then {
+ GVAR(MEconomyStore) call ["init", []];
+};
diff --git a/arma/server/addons/economy/XEH_preInit.sqf b/arma/server/addons/economy/XEH_preInit.sqf
index 8a385c3..34e561b 100644
--- a/arma/server/addons/economy/XEH_preInit.sqf
+++ b/arma/server/addons/economy/XEH_preInit.sqf
@@ -8,7 +8,7 @@ PREP_RECOMPILE_END;
if (isNil QGVAR(MEconomyStore)) then { call FUNC(initMEconomyStore); };
if (isNil QGVAR(FEconomyStore)) then { call FUNC(initFEconomyStore); };
-// if (isNil QGVAR(SEconomyStore)) then { call FUNC(initSEconomyStore); };
+if (isNil QGVAR(SEconomyStore)) then { call FUNC(initSEconomyStore); };
[QGVAR(FuelStart), {
params ["_source", "_target", "_unit"];
@@ -28,6 +28,16 @@ if (isNil QGVAR(FEconomyStore)) then { call FUNC(initFEconomyStore); };
GVAR(FEconomyStore) call ["stop", [_source, _target]];
}] call CFUNC(addEventHandler);
+[QGVAR(RepairService), {
+ params ["_target", "_unit", ["_cost", -1, [0]]];
+ GVAR(SEconomyStore) call ["repair", [_target, _unit, _cost]];
+}] call CFUNC(addEventHandler);
+
+[QGVAR(RefuelService), {
+ params ["_target", "_unit"];
+ GVAR(FEconomyStore) call ["refuel", [_target, _unit]];
+}] call CFUNC(addEventHandler);
+
[QGVAR(onKilled), {
params ["_unit"];
GVAR(MEconomyStore) call ["onKilled", [_unit]];
diff --git a/arma/server/addons/economy/config.cpp b/arma/server/addons/economy/config.cpp
index 05c825e..cbd0a75 100644
--- a/arma/server/addons/economy/config.cpp
+++ b/arma/server/addons/economy/config.cpp
@@ -8,7 +8,8 @@ class CfgPatches {
name = COMPONENT_NAME;
requiredVersion = REQUIRED_VERSION;
requiredAddons[] = {
- "forge_server_main"
+ "forge_server_main",
+ "forge_server_common"
};
units[] = {};
weapons[] = {};
diff --git a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf
index 613ef18..0806220 100644
--- a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf
+++ b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf
@@ -4,20 +4,23 @@
* File: fnc_initFEconomyStore.sqf
* Author: IDSolutions
* Date: 2025-12-20
- * Last Update: 2026-01-03
+ * Last Update: 2026-04-18
* Public: No
*
* Description:
- * No description added yet.
+ * Initializes the fuel economy store. Active refueling sessions remain
+ * server-local; payment is routed through the organization extension hot
+ * cache. Garage service refuels use the same organization billing path
+ * and only fill the vehicle after the charge succeeds.
*
* Parameter(s):
* N/A
*
* Returns:
- * Something [BOOL]
+ * Fuel economy store object [HASHMAP OBJECT]
*
* Example(s):
- * [parameter] call forge_x_component_fnc_myFunction
+ * call forge_server_economy_fnc_initFEconomyStore
*/
#pragma hemtt ignore_variables ["_self"]
@@ -36,23 +39,103 @@ GVAR(FEconomyStore) = createHashMapObject [[
private _uid = getPlayerUID _unit;
private _fuelRegistry = _self getOrDefault ["fuelRegistry", createHashMap];
- _fuelRegistry set [_index, _uid];
+ _fuelRegistry set [_index, createHashMapFromArray [
+ ["uid", _uid],
+ ["initialFuel", fuel _target]
+ ]];
SETVAR(_target,liters,0);
}],
+ ["rollbackFuel", {
+ params [["_target", objNull, [objNull]], ["_initialFuel", 0, [0]]];
+
+ if (isNull _target) exitWith { false };
+
+ _target setFuel (_initialFuel max 0 min 1);
+ SETVAR(_target,liters,0);
+ true
+ }],
+ ["refuel", {
+ params [["_target", objNull, [objNull]], ["_unit", objNull, [objNull]]];
+
+ if (isNull _target || { isNull _unit }) exitWith { false };
+
+ private _currentFuel = fuel _target;
+ private _missingFuel = (1 - _currentFuel) max 0 min 1;
+ if (_missingFuel <= 0.001) exitWith {
+ [CRPC(notifications,recieveNotification), ["info", "Refueling", "Vehicle fuel tank is already full."], _unit] call CFUNC(targetEvent);
+ false
+ };
+
+ if (isNil QGVAR(SEconomyStore)) exitWith {
+ ["ERROR", "Service economy store unavailable for garage refueling charge.", nil, nil] call EFUNC(common,log);
+ [CRPC(notifications,recieveNotification), ["danger", "Refueling", "Organization billing is unavailable. Refueling was not completed."], _unit] call CFUNC(targetEvent);
+ false
+ };
+
+ private _fuelCapacity = getNumber (configOf _target >> "fuelCapacity");
+ if (_fuelCapacity <= 0) then { _fuelCapacity = 100; };
+
+ private _totalLiters = _missingFuel * _fuelCapacity;
+ private _totalCost = _totalLiters * GVAR(FuelCost);
+ private _chargeResult = GVAR(SEconomyStore) call ["chargeOrg", [_unit, _totalCost, "Refueling"]];
+ if !(_chargeResult getOrDefault ["success", false]) exitWith {
+ [CRPC(notifications,recieveNotification), ["danger", "Refueling", _chargeResult getOrDefault ["message", "Organization funds cannot cover this refuel. Refueling was not completed."]], _unit] call CFUNC(targetEvent);
+ false
+ };
+
+ _target setFuel 1;
+ SETVAR(_target,liters,0);
+
+ private _formattedTotalCost = [_totalCost] call EFUNC(common,formatNumber);
+ private _formattedTotalLiters = _totalLiters toFixed 2;
+ [CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L Organization charged $%2.", _formattedTotalLiters, _formattedTotalCost]], _unit] call CFUNC(targetEvent);
+ true
+ }],
["stop", {
params ["_source", "_target"];
private _index = netId _target;
private _fuelRegistry = _self getOrDefault ["fuelRegistry", createHashMap];
- private _uid = _fuelRegistry get _index;
+ private _session = _fuelRegistry getOrDefault [_index, createHashMap];
+ if (_session isEqualType "") then {
+ _session = createHashMapFromArray [["uid", _session], ["initialFuel", fuel _target]];
+ };
+
+ private _uid = _session getOrDefault ["uid", ""];
+ private _initialFuel = _session getOrDefault ["initialFuel", fuel _target];
private _player = [_uid] call EFUNC(common,getPlayer);
private _totalLiters = GETVAR(_target,liters,0);
- private _totalCost = _totalLiters * 5;
+ private _totalCost = _totalLiters * GVAR(FuelCost);
private _formattedTotalCost = [_totalCost] call EFUNC(common,formatNumber);
private _formattedTotalLiters = _totalLiters toFixed 2;
- [CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L Total Cost: $%2", _formattedTotalLiters, _formattedTotalCost]], _player] call CFUNC(targetEvent);
+ if (isNull _player || { _uid isEqualTo "" }) exitWith {
+ ["WARNING", format ["Unable to resolve refueling player for vehicle %1.", _index], nil, nil] call EFUNC(common,log);
+ _self call ["rollbackFuel", [_target, _initialFuel]];
+ _fuelRegistry deleteAt _index;
+ };
+
+ if (_totalCost <= 0) exitWith {
+ [CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L", _formattedTotalLiters]], _player] call CFUNC(targetEvent);
+ _fuelRegistry deleteAt _index;
+ };
+
+ if (isNil QGVAR(SEconomyStore)) exitWith {
+ ["ERROR", "Service economy store unavailable for refueling charge.", nil, nil] call EFUNC(common,log);
+ [CRPC(notifications,recieveNotification), ["danger", "Refueling", "Organization billing is unavailable. Refueling was not completed."], _player] call CFUNC(targetEvent);
+ _self call ["rollbackFuel", [_target, _initialFuel]];
+ _fuelRegistry deleteAt _index;
+ };
+
+ private _chargeResult = GVAR(SEconomyStore) call ["chargeOrg", [_player, _totalCost, "Refueling"]];
+ if !(_chargeResult getOrDefault ["success", false]) exitWith {
+ [CRPC(notifications,recieveNotification), ["danger", "Refueling", _chargeResult getOrDefault ["message", "Organization funds cannot cover this refuel. Refueling was not completed."]], _player] call CFUNC(targetEvent);
+ _self call ["rollbackFuel", [_target, _initialFuel]];
+ _fuelRegistry deleteAt _index;
+ };
+
+ [CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L Organization charged $%2.", _formattedTotalLiters, _formattedTotalCost]], _player] call CFUNC(targetEvent);
_fuelRegistry deleteAt _index;
}]
]];
diff --git a/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf
index d6edd84..217fe97 100644
--- a/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf
+++ b/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf
@@ -4,20 +4,23 @@
* File: fnc_initMEconomyStore.sqf
* Author: IDSolutions
* Date: 2025-12-20
- * Last Update: 2026-02-13
+ * Last Update: 2026-04-18
* Public: No
*
* Description:
- * No description added yet.
+ * Initializes the medical economy store. Respawn, body-bag, and spawn
+ * occupancy behavior remains server-local, while money mutations are
+ * routed through player bank hot state first, then organization hot state
+ * with a repayable member debt when personal funds cannot cover the bill.
*
* Parameter(s):
* N/A
*
* Returns:
- * Something [BOOL]
+ * Medical economy store object [HASHMAP OBJECT]
*
* Example(s):
- * [parameter] call forge_x_component_fnc_myFunction
+ * call forge_server_economy_fnc_initMEconomyStore
*/
#pragma hemtt ignore_variables ["_self"]
@@ -63,37 +66,105 @@ GVAR(MEconomyStore) = createHashMapObject [[
} forEach _mSpawns;
};
}],
+ ["chargePlayer", {
+ params [["_uid", "", [""]], ["_amount", 0, [0]]];
+
+ private _result = createHashMapFromArray [
+ ["success", false],
+ ["fallbackEligible", false],
+ ["source", ""],
+ ["message", "Unable to charge personal funds."]
+ ];
+
+ if (_uid isEqualTo "") exitWith {
+ _result set ["message", "A valid player UID is required for medical billing."];
+ _result
+ };
+ if (_amount <= 0) exitWith {
+ _result set ["success", true];
+ _result
+ };
+ if (isNil QEGVAR(bank,BankStore)) exitWith {
+ _result set ["message", "Personal billing is unavailable. Medical service cannot complete."];
+ _result
+ };
+
+ private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]];
+ if (_account isEqualTo createHashMap) exitWith {
+ _result set ["message", "Personal account could not be loaded for medical billing."];
+ _result
+ };
+
+ private _source = "";
+ if ((_account getOrDefault ["bank", 0]) >= _amount) then {
+ _source = "bank";
+ } else {
+ if ((_account getOrDefault ["cash", 0]) >= _amount) then {
+ _source = "cash";
+ };
+ };
+
+ if (_source isEqualTo "") exitWith {
+ _result set ["fallbackEligible", true];
+ _result set ["message", "Personal bank and cash balances cannot cover this medical service."];
+ _result
+ };
+
+ private _charge = EGVAR(bank,BankStore) call ["chargeCheckout", [_uid, _source, _amount, true]];
+ if !(_charge getOrDefault ["success", false]) exitWith {
+ _result set ["message", _charge getOrDefault ["message", "Personal funds could not be charged for medical service."]];
+ _result
+ };
+
+ private _patch = _charge getOrDefault ["patch", createHashMap];
+ if (_patch isNotEqualTo createHashMap && { !(isNil QEGVAR(bank,BankMessenger)) }) then {
+ EGVAR(bank,BankMessenger) call ["sendAccountSync", [_uid, _patch]];
+ };
+
+ private _savedAccount = EGVAR(bank,BankStore) call ["save", [_uid]];
+ if (_savedAccount isEqualTo createHashMap) then {
+ ["ERROR", format ["Medical charge for %1 succeeded in hot bank state, but durable bank save failed.", _uid]] call EFUNC(common,log);
+ };
+
+ _result set ["success", true];
+ _result set ["source", _source];
+ _result set ["message", ""];
+ _result
+ }],
["onHealed", {
params [["_unit", objNull, [objNull]]];
if (isNull _unit) exitWith { ["WARNING", format ["Invalid unit provided: %1", (name _unit)], nil, nil] call EFUNC(common,log); };
private _uid = getPlayerUID _unit;
- private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]];
- if (_account isEqualTo createHashMap) then {
- _account = EGVAR(bank,BankStore) call ["init", [_uid]];
- };
-
- if (_account isEqualTo createHashMap) exitWith { ["ERROR", format ["No account found for %1. UID: %2", (name _unit), _uid], nil, nil] call EFUNC(common,log); };
-
- private _bank = _account get "bank";
- private _cash = _account get "cash";
+ if (_uid isEqualTo "") exitWith { ["WARNING", "Unable to charge medical service for unit without UID.", nil, nil] call EFUNC(common,log); };
private _healCost = 100;
- private _newBalance = 0;
- if (_bank < _healCost && _cash < _healCost) exitWith {
- [CRPC(notifications,recieveNotification), ["danger", "Insufficient Funds", format ["Insufficient funds for %1. Bank: $%2, Cash: $%3, Required: $%4", (name _unit), [_bank] call EFUNC(common,formatNumber), [_cash] call EFUNC(common,formatNumber), [_healCost] call EFUNC(common,formatNumber)]], _unit] call CFUNC(targetEvent);
+ private _personalCharge = _self call ["chargePlayer", [_uid, _healCost]];
+ if (_personalCharge getOrDefault ["success", false]) exitWith {
+ private _sourceLabel = ["cash", "bank"] select ((_personalCharge getOrDefault ["source", "bank"]) isEqualTo "bank");
+ [CRPC(notifications,recieveNotification), ["info", "Medical Billing", format ["Medical service charged $%1 from your %2.", [_healCost] call EFUNC(common,formatNumber), _sourceLabel]], _unit] call CFUNC(targetEvent);
+ [CRPC(actor,onActorHealed), [], _unit] call CFUNC(targetEvent);
};
- if (_bank >= _healCost) then {
- _newBalance = _bank - _healCost;
- _account set ["bank", _newBalance];
- } else {
- _newBalance = _cash - _healCost;
- _account set ["cash", _newBalance];
+ if !(_personalCharge getOrDefault ["fallbackEligible", false]) exitWith {
+ private _message = _personalCharge getOrDefault ["message", "Personal funds could not be charged for medical service."];
+ [CRPC(notifications,recieveNotification), ["danger", "Medical Billing", _message], _unit] call CFUNC(targetEvent);
};
+ if (isNil QGVAR(SEconomyStore)) exitWith {
+ ["ERROR", "Service economy store unavailable for medical organization fallback charge.", nil, nil] call EFUNC(common,log);
+ [CRPC(notifications,recieveNotification), ["danger", "Medical Billing", "Organization billing is unavailable. Medical service cannot complete."], _unit] call CFUNC(targetEvent);
+ };
+
+ private _chargeResult = GVAR(SEconomyStore) call ["chargeOrg", [_unit, _healCost, "Medical", true]];
+ if !(_chargeResult getOrDefault ["success", false]) exitWith {
+ private _message = _chargeResult getOrDefault ["message", "Organization funds cannot cover this medical service."];
+ [CRPC(notifications,recieveNotification), ["danger", "Medical Billing", _message], _unit] call CFUNC(targetEvent);
+ };
+
+ [CRPC(notifications,recieveNotification), ["info", "Medical Billing", format ["Personal funds could not cover medical service. Organization charged $%1; repay it through your organization credit line.", [_healCost] call EFUNC(common,formatNumber)]], _unit] call CFUNC(targetEvent);
[CRPC(actor,onActorHealed), [], _unit] call CFUNC(targetEvent);
}],
["onRespawn", {
diff --git a/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf
index 07a94dc..52c6454 100644
--- a/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf
+++ b/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf
@@ -1,31 +1,136 @@
#include "..\script_component.hpp"
/*
- * File: initSEconomyStore.sqf
+ * File: fnc_initSEconomyStore.sqf
* Author: IDSolutions
* Date: 2025-12-20
- * Last Update: 2026-01-03
+ * Last Update: 2026-04-18
* Public: No
*
* Description:
- * No description added yet.
+ * Initializes the service economy store for organization-funded world
+ * services such as repairs, with optional member debt recording for
+ * organization-covered medical fallback charges.
*
* Parameter(s):
* N/A
*
* Returns:
- * Something [BOOL]
+ * Service economy store object [HASHMAP OBJECT]
*
* Example(s):
- * [parameter] call forge_x_component_fnc_myFunction
+ * call forge_server_economy_fnc_initSEconomyStore
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(SEconomyStore) = createHashMapObject [[
["#type", "IServiceEconomy"],
["#create", {
+ GVAR(ServiceRepairCost) = 500;
["INFO", "Service Store Initialized!", nil, nil] call EFUNC(common,log);
}],
+ ["notify", {
+ params [["_unit", objNull, [objNull]], ["_type", "info", [""]], ["_title", "Service", [""]], ["_message", "", [""]]];
+
+ if (isNull _unit || { _message isEqualTo "" }) exitWith { false };
+
+ [CRPC(notifications,recieveNotification), [_type, _title, _message], _unit] call CFUNC(targetEvent);
+ true
+ }],
+ ["syncOrgPatch", {
+ params [["_result", createHashMap, [createHashMap]]];
+
+ private _patch = _result getOrDefault ["patch", createHashMap];
+ if ((keys _patch) isEqualTo []) exitWith { false };
+
+ {
+ private _memberPlayer = [_x] call EFUNC(common,getPlayer);
+ if (_memberPlayer isNotEqualTo objNull) then {
+ [CRPC(org,responseSyncOrg), [_patch], _memberPlayer] call CFUNC(targetEvent);
+ };
+ } forEach (_result getOrDefault ["memberUids", []]);
+
+ true
+ }],
+ ["chargeOrg", {
+ params [
+ ["_unit", objNull, [objNull]],
+ ["_amount", 0, [0]],
+ ["_label", "Service", [""]],
+ ["_recordDebt", false, [false]]
+ ];
+
+ private _result = createHashMapFromArray [
+ ["success", false],
+ ["message", "Unable to charge organization funds."],
+ ["patch", createHashMap],
+ ["memberUids", []],
+ ["persisted", false],
+ ["persistenceMessage", ""]
+ ];
+
+ if (isNull _unit) exitWith {
+ _result set ["message", "A valid player is required for organization billing."];
+ _result
+ };
+
+ private _uid = getPlayerUID _unit;
+ if (_uid isEqualTo "") exitWith {
+ _result set ["message", "A valid player UID is required for organization billing."];
+ _result
+ };
+
+ if (_amount <= 0) exitWith {
+ _result set ["success", true];
+ _result set ["message", ""];
+ _result
+ };
+
+ if (isNil QEGVAR(org,OrgStore)) exitWith {
+ _result set ["message", "Organization service is unavailable."];
+ ["ERROR", format ["Org store unavailable for %1 charge.", _label], nil, nil] call EFUNC(common,log);
+ _result
+ };
+
+ private _orgID = EGVAR(org,OrgStore) call ["resolveOrgIdForUid", [_uid]];
+ if (_orgID isEqualTo "") then { _orgID = "default"; };
+
+ private _actor = createHashMap;
+ if !(isNil QEGVAR(actor,ActorStore)) then {
+ _actor = EGVAR(actor,ActorStore) call ["load", [_uid]];
+ };
+ private _memberName = EGVAR(org,OrgStore) call ["resolveActorName", [_uid, _unit, _actor]];
+ private _org = EGVAR(org,OrgStore) call ["ensureMember", [_orgID, _uid, _memberName]];
+ if (_org isEqualTo createHashMap) exitWith {
+ _result set ["message", "Organization membership could not be verified."];
+ _result
+ };
+
+ private _charge = EGVAR(org,OrgStore) call ["chargeCheckout", [_uid, _unit, "org_funds", _amount, true, true, _recordDebt]];
+ if !(_charge getOrDefault ["success", false]) exitWith {
+ _result set ["message", _charge getOrDefault ["message", "Organization funds cannot cover this service."]];
+ _result
+ };
+
+ _self call ["syncOrgPatch", [_charge]];
+ _charge
+ }],
+ ["repair", {
+ params [["_target", objNull, [objNull]], ["_unit", objNull, [objNull]], ["_cost", -1, [0]]];
+
+ if (isNull _target || { isNull _unit }) exitWith { false };
+
+ private _repairCost = [_cost, GVAR(ServiceRepairCost)] select (_cost < 0);
+ private _charge = _self call ["chargeOrg", [_unit, _repairCost, "Repair"]];
+ if !(_charge getOrDefault ["success", false]) exitWith {
+ _self call ["notify", [_unit, "danger", "Repair", _charge getOrDefault ["message", "Organization funds cannot cover this repair."]]];
+ false
+ };
+
+ _target setDamage 0;
+ _self call ["notify", [_unit, "info", "Repair", format ["Repair complete. Organization charged $%1.", [_repairCost] call EFUNC(common,formatNumber)]]];
+ true
+ }],
["init", {}]
]];
diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf
index e1a8dbd..cda2669 100644
--- a/arma/server/addons/org/functions/fnc_initOrgStore.sqf
+++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf
@@ -4,12 +4,13 @@
* File: fnc_initOrgStore.sqf
* Author: IDSolutions
* Date: 2026-02-13
- * Last Update: 2026-04-04
+ * Last Update: 2026-04-18
* Public: Yes
*
* Description:
* Initializes the org store for managing player organizations.
- * Org hot state is owned by the extension; SQF acts as the bridge.
+ * Org hot state is owned by the extension; SQF acts as the bridge for
+ * treasury charges, credit lines, and service debt recording.
*
* Arguments:
* None
@@ -800,7 +801,15 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result
}],
["chargeCheckout", compileFinal {
- params [["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]], ["_source", "org_funds", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]];
+ params [
+ ["_requesterUid", "", [""]],
+ ["_requesterPlayer", objNull, [objNull]],
+ ["_source", "org_funds", [""]],
+ ["_amount", 0, [0]],
+ ["_commit", false, [false]],
+ ["_allowMemberCharge", false, [false]],
+ ["_recordMemberDebt", false, [false]]
+ ];
private _result = createHashMapFromArray [
["success", false],
@@ -822,6 +831,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["requesterUid", _requesterUid],
["orgId", _orgID],
["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo],
+ ["allowMemberCharge", _allowMemberCharge],
+ ["recordMemberDebt", _recordMemberDebt],
["source", _source],
["amount", _amount],
["commit", _commit]
diff --git a/arma/server/addons/task/README.md b/arma/server/addons/task/README.md
index 038449d..785002c 100644
--- a/arma/server/addons/task/README.md
+++ b/arma/server/addons/task/README.md
@@ -63,8 +63,105 @@ Task time limits use `0` for no limit on attack, destroy, delivery, hostage,
and HVT tasks. Defuse IED timers are different: each IED must have a positive
countdown value.
+Mission designers can create tasks in four ways:
+
+- Eden modules for editor-authored tasks.
+- `fnc_startTask.sqf` for script-authored tasks.
+- `fnc_handler.sqf` for pre-registered entities with reputation gating and
+ ownership binding. This path expects the BIS task and catalog entry to
+ already exist if map-task and CAD visibility are required.
+- Direct task function calls for server-owned or mission-authored flows that
+ intentionally fall back to the `default` org. This path expects the BIS task
+ to already exist if map-task visibility is required.
+
+The dynamic mission manager can also generate attack tasks from config. That is
+system-generated content rather than a hand-authored task creation path.
+
+### CAD Compatibility
+CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
+have a catalog entry and active task status before CAD can show and assign it.
+
+CAD-compatible creation paths:
+- Eden modules: compatible because they delegate to `fnc_startTask.sqf`
+- `fnc_startTask.sqf`: compatible because it registers the catalog entry,
+ creates the BIS task, and dispatches through `fnc_handler.sqf`
+- dynamic mission manager attack tasks: compatible because the mission manager
+ uses `fnc_startTask.sqf`
+
+Limited or incompatible paths:
+- `fnc_handler.sqf`: only compatible if a catalog entry was already registered
+ elsewhere. The handler sets active status and ownership, but it does not
+ create the BIS task shown in the map task tab or upsert the catalog entry
+- direct task function calls: not CAD-compatible by default. They bypass
+ `fnc_startTask.sqf` and usually do not register the task catalog entry or
+ active status that CAD hydrates from. They also only call
+ `BIS_fnc_taskSetState` at completion/failure; they do not create the BIS task
+ first
+
+### BIS Map Task Prerequisite
+Only the Eden task modules and `fnc_startTask.sqf` create the BIS task
+automatically through `BIS_fnc_taskCreate`.
+
+If a mission uses `fnc_handler.sqf` directly or calls a task flow function such
+as `forge_server_task_fnc_attack`, the mission must create a BIS task with the
+same task ID before the Forge task completes. Otherwise the success/failure
+`BIS_fnc_taskSetState` call has no visible map task to update.
+
+That prerequisite can be satisfied with a vanilla Eden task creation module or
+a scripted `BIS_fnc_taskCreate` call. `fnc_startTask.sqf` is the preferred Forge
+path because it handles BIS task creation, Forge catalog registration, entity
+registration, and handler dispatch together.
+
+### Create With Eden Modules
+Eden task modules are the normal designer-facing path. Place the module,
+configure its attributes, and sync it to the relevant entities or grouping
+modules.
+
+Available task modules:
+- `FORGE_Module_Attack`: sync directly to target units or vehicles
+- `FORGE_Module_Destroy`: sync directly to objects, vehicles, or units
+- `FORGE_Module_Defuse`: sync to `FORGE_Module_Explosives` and optionally
+ `FORGE_Module_Protected`
+- `FORGE_Module_Delivery`: sync to `FORGE_Module_Cargo`; the cargo module syncs
+ to cargo objects
+- `FORGE_Module_Hostage`: sync to `FORGE_Module_Hostages` and
+ `FORGE_Module_Shooters`
+- `FORGE_Module_HVT`: sync directly to HVT units
+- `FORGE_Module_Defend`: configure the defense marker and wave settings
+
+These modules delegate to `fnc_startTask.sqf`.
+
+### Start Through `fnc_startTask.sqf`
+Use `fnc_startTask.sqf` for script-authored tasks. It registers task entities,
+creates the BIS task, stores the catalog entry, and dispatches through
+`fnc_handler.sqf`.
+
+```sqf
+[
+ "attack",
+ "compound_attack_01",
+ getPosATL leader1,
+ "Attack: East Compound",
+ "Eliminate all hostile forces.",
+ createHashMapFromArray [["targets", [unit1, unit2, unit3]]],
+ createHashMapFromArray [
+ ["limitFail", 0],
+ ["limitSuccess", 3],
+ ["funds", 50000],
+ ["ratingFail", -10],
+ ["ratingSuccess", 20],
+ ["timeLimit", 900]
+ ],
+ 0,
+ getPlayerUID player,
+ "script"
+] call forge_server_task_fnc_startTask;
+```
+
### Start Through The Handler
Use the handler when you want reputation gating and task ownership binding.
+Create the BIS task and catalog entry separately if this task should appear in
+the map task tab or CAD.
```sqf
["attack", ["task_attack_1", 1, 2, 1500000, -75, 375, false, false], 250, getPlayerUID player] call forge_server_task_fnc_handler;
@@ -79,6 +176,7 @@ Arguments:
### Start Task Functions Directly
Direct task calls still work, but they do not provide a requester UID. That means task ownership falls back to the `default` org.
+Create the BIS task separately if this task should appear in the map task tab.
Use direct starts only when that behavior is intended, such as:
- mission-authored tasks
diff --git a/docs/CLIENT_ACTOR_USAGE_GUIDE.md b/docs/CLIENT_ACTOR_USAGE_GUIDE.md
new file mode 100644
index 0000000..bd514dd
--- /dev/null
+++ b/docs/CLIENT_ACTOR_USAGE_GUIDE.md
@@ -0,0 +1,98 @@
+# Client Actor Usage Guide
+
+The client actor addon owns the player interaction menu and client-side actor
+repository. It is the main launcher for nearby player actions and other Forge
+client UIs.
+
+## Open the Actor Menu
+
+```sqf
+call forge_client_actor_fnc_openUI;
+```
+
+The actor menu opens `RscActorMenu`, loads `ui/_site/index.html`, and routes
+browser alerts through `forge_client_actor_fnc_handleUIEvents`.
+
+## Repository
+
+`forge_client_actor_fnc_initRepository` creates `GVAR(ActorRepository)`.
+
+The repository:
+
+- requests actor initialization from the server
+- saves actor state through the server actor addon
+- caches client-visible actor fields
+- applies position, direction, stance, rank, and loadout on JIP sync when the
+ relevant settings allow it
+- provides nearby interaction actions to the browser UI
+
+Initialize actor state through the repository:
+
+```sqf
+GVAR(ActorRepository) call ["init", []];
+```
+
+Save actor state through the server:
+
+```sqf
+GVAR(ActorRepository) call ["save", [true]];
+```
+
+## Nearby Actions
+
+The menu asks for nearby actions with:
+
+```text
+actor::get::actions
+```
+
+The repository scans objects within 5 meters and returns actions based on
+mission object variables:
+
+| Variable | Action |
+| --- | --- |
+| `storeType` | store |
+| `isAtm` | ATM |
+| `isBank` | bank |
+| `isGarage` | garage |
+| `garageType` | garage subtype |
+| `isLocker` | virtual arsenal action when VA is enabled |
+| `deviceType` | device action placeholder |
+| nearby player unit | player interaction placeholder |
+
+The response is pushed into the browser with `updateAvailableActions(...)`.
+
+## Browser Events
+
+| Event | Client behavior |
+| --- | --- |
+| `actor::get::actions` | Refresh nearby actions. |
+| `actor::close::menu` | Close actor menu. |
+| `actor::open::atm` | Open bank UI in ATM mode. |
+| `actor::open::bank` | Open bank UI in bank mode. |
+| `actor::open::cad` | Open CAD UI. |
+| `actor::open::garage` | Open garage UI. |
+| `actor::open::vgarage` | Open virtual garage. |
+| `actor::open::org` | Open organization UI. |
+| `actor::open::vlocker` | Open ACE arsenal on `FORGE_Locker_Box`. |
+| `actor::open::phone` | Open phone UI. |
+| `actor::open::store` | Open store UI. |
+
+Device and player interaction events currently display placeholder feedback.
+
+## Authoritative State
+
+Actor persistence is server-owned. The client repository requests and displays
+actor data, but actor creation, durable updates, and hot-state behavior are
+handled by the server actor addon and extension.
+
+## Related Guides
+
+- [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md)
+- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
+- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md)
+- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
+- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md)
+- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md)
+- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md)
+- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)
diff --git a/docs/CLIENT_BANK_USAGE_GUIDE.md b/docs/CLIENT_BANK_USAGE_GUIDE.md
new file mode 100644
index 0000000..4390f55
--- /dev/null
+++ b/docs/CLIENT_BANK_USAGE_GUIDE.md
@@ -0,0 +1,84 @@
+# Client Bank Usage Guide
+
+The client bank addon opens the bank and ATM browser UI, forwards banking
+requests to the server bank addon, and pushes account updates back into the
+browser.
+
+## Open Bank UI
+
+Open full bank mode:
+
+```sqf
+call forge_client_bank_fnc_openUI;
+```
+
+Open ATM mode:
+
+```sqf
+[true] call forge_client_bank_fnc_openUI;
+```
+
+The open function creates `RscBank`, sets the bridge mode to `bank` or `atm`,
+loads `ui/_site/index.html`, and routes browser events through
+`forge_client_bank_fnc_handleUIEvents`.
+
+## Bridge and Repository
+
+`forge_client_bank_fnc_initRepository` tracks account load and cached account
+state.
+
+`forge_client_bank_fnc_initUIBridge` owns:
+
+- active browser control tracking
+- bank/ATM mode
+- browser ready handling
+- account hydrate and sync responses
+- deposit, withdrawal, transfer, earnings deposit, credit repayment, and PIN
+ requests
+- browser notice delivery
+
+## Browser Events
+
+| Event | Client behavior |
+| --- | --- |
+| `bank::ready` | Mark browser ready and request hydrate from the server. |
+| `bank::refresh` | Request fresh bank hydrate data. |
+| `bank::deposit::request` | Forward deposit amount to the server. |
+| `bank::withdraw::request` | Forward withdrawal amount to the server. |
+| `bank::transfer::request` | Forward target, source field, and amount. |
+| `bank::depositEarnings::request` | Request earnings deposit. |
+| `bank::repayCreditLine::request` | Request credit-line repayment. |
+| `bank::pin::request` | Forward PIN validation request. |
+| `bank::close` | Dispose bridge screen state and close the display. |
+
+## Browser Response Events
+
+The bridge sends:
+
+| Event | Purpose |
+| --- | --- |
+| `bank::hydrate` | Full session/account payload. |
+| `bank::sync` | Account patch or sync data. |
+| `bank::notice` | UI-visible notice payload. |
+
+## Request Flow
+
+Example deposit flow:
+
+1. Browser sends `bank::deposit::request` with an `amount`.
+2. Client bridge calls the server bank request event.
+3. Server bank addon validates the request and calls bank hot-state logic.
+4. Server response is caught by the client post-init event handlers.
+5. Client bridge sends `bank::sync` or `bank::notice` back to the browser.
+
+## Authoritative State
+
+Balances, PIN authorization, transfers, checkout charges, credit lines, and
+persistence are server-owned. The client should only display account data and
+request mutations through server events.
+
+## Related Guides
+
+- [Bank Usage Guide](./BANK_USAGE_GUIDE.md)
+- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)
+- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)
diff --git a/docs/CLIENT_CAD_USAGE_GUIDE.md b/docs/CLIENT_CAD_USAGE_GUIDE.md
new file mode 100644
index 0000000..654f23b
--- /dev/null
+++ b/docs/CLIENT_CAD_USAGE_GUIDE.md
@@ -0,0 +1,100 @@
+# Client CAD Usage Guide
+
+The client CAD addon provides the map and dispatch UI for groups, active
+tasks, task assignment, dispatch orders, support requests, and task
+acknowledge/decline workflows.
+
+## Open CAD UI
+
+```sqf
+call forge_client_cad_fnc_openUI;
+```
+
+The CAD UI opens `RscMapUI` and loads separate browser controls for:
+
+- top bar
+- bottom bar
+- side panel
+- dispatcher board
+
+The native Arma map remains part of the same display.
+
+## Repository and Bridge
+
+`forge_client_cad_fnc_initRepository` caches the hydrated CAD payload,
+selected mode, dispatch view, session data, groups, tasks, requests, and
+assignments.
+
+`forge_client_cad_fnc_initUIBridge` owns:
+
+- ready state for side panel, top bar, and dispatcher board
+- operations vs dispatch mode
+- board vs map dispatch view
+- hydrate requests
+- task assignment, acknowledge, and decline requests
+- dispatch order create/close requests
+- support request submit/close requests
+- group status, role, and profile requests
+- map focus actions
+
+## Browser Events
+
+| Event | Client behavior |
+| --- | --- |
+| `cad::topbar::ready` | Mark top bar ready and push top bar state. |
+| `cad::ready` | Mark side panel ready and request hydrate. |
+| `cad::dispatcher::ready` | Mark dispatcher board ready and push hydrate data. |
+| `cad::mode::set` | Switch between operations and dispatch mode. |
+| `cad::dispatchView::set` | Switch dispatch board/map view. |
+| `cad::refresh` | Request fresh CAD hydrate data. |
+| `cad::tasks::assign` | Assign a task to a group. |
+| `cad::tasks::acknowledge` | Acknowledge assigned task. |
+| `cad::tasks::decline` | Decline assigned task. |
+| `cad::dispatchOrder::create` | Create dispatch order. |
+| `cad::dispatchOrder::close` | Close dispatch order. |
+| `cad::supportRequest::submit` | Submit support request. |
+| `cad::supportRequest::close` | Close support request. |
+| `cad::groups::status` | Update group status. |
+| `cad::groups::role` | Update group role. |
+| `cad::groups::profile` | Update status and role together. |
+| `cad::groups::focus` | Center map on a group. |
+| `cad::tasks::focus` | Center map on a task. |
+| `cad::requests::focus` | Center map on a support request. |
+| `map::zoomIn` | Zoom native map in. |
+| `map::zoomOut` | Zoom native map out. |
+| `map::search` | Placeholder status update. |
+| `map::close` | Dispose bridge state and close the display. |
+
+## Response Events
+
+The bridge pushes:
+
+| Event | Purpose |
+| --- | --- |
+| `cad::hydrate` | Full hydrated CAD payload to the side panel. |
+| `cad::assignment::response` | Task assignment/acknowledge/decline result. |
+| `cad::group::response` | Group status/role/profile result. |
+| `cad::request::response` | Support request result. |
+
+Dispatcher board controls also receive direct `ExecJS` status and hydrate
+calls.
+
+## Task Compatibility
+
+CAD task visibility depends on server-side task catalog entries. Tasks created
+through Eden Forge task modules or `forge_server_task_fnc_startTask` are the
+normal CAD-compatible task sources because they register task catalog data.
+
+Direct handler or task-function calls only work with CAD when the task catalog
+entry already exists.
+
+## Authorization Notes
+
+Only dispatcher sessions can enter dispatch mode. If the hydrated session is
+not a dispatcher, the bridge forces the UI back to operations mode.
+
+## Related Guides
+
+- [CAD Usage Guide](./CAD_USAGE_GUIDE.md)
+- [Task Usage Guide](./TASK_USAGE_GUIDE.md)
+- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)
diff --git a/docs/CLIENT_COMMON_USAGE_GUIDE.md b/docs/CLIENT_COMMON_USAGE_GUIDE.md
new file mode 100644
index 0000000..754d6c8
--- /dev/null
+++ b/docs/CLIENT_COMMON_USAGE_GUIDE.md
@@ -0,0 +1,92 @@
+# Client Common Usage Guide
+
+The client `common` addon contains shared browser UI bridge declarations and
+common client-side browser integration patterns.
+
+## Purpose
+
+Use `forge_client_common` when a browser-backed feature UI needs reusable
+screen lifecycle behavior:
+
+- active browser control tracking
+- browser ready state
+- pending event queues
+- `ExecJS` payload delivery
+- shared bridge object inheritance through `createHashMapObject`
+
+Feature addons still own their app-specific events and server RPC mapping.
+
+## Shared Bridge
+
+Initialize the bridge declarations with:
+
+```sqf
+private _webUIDeclarations = call forge_client_common_fnc_initWebUIBridge;
+private _bridgeDeclaration = _webUIDeclarations get "bridgeDeclaration";
+```
+
+Feature bridges can inherit from the shared declaration:
+
+```sqf
+GVAR(MyUIBridgeBaseClass) = compileFinal createHashMapFromArray [
+ ["#base", _bridgeDeclaration],
+ ["#type", "MyUIBridgeBaseClass"],
+ ["handleReady", compileFinal {
+ params [["_control", controlNull, [controlNull]]];
+
+ _self call ["setActiveBrowserControl", [_control]];
+ _self call ["sendEvent", ["myAddon::hydrate", createHashMap, _control]];
+ }]
+];
+```
+
+## Event Delivery
+
+`sendEvent` builds this payload:
+
+```json
+{
+ "event": "myAddon::event",
+ "data": {}
+}
+```
+
+If the browser control is missing or not ready, the payload is queued on the
+screen object. When the screen marks ready, `flushPendingEvents` delivers the
+queue.
+
+## Screen Lifecycle
+
+The shared screen object tracks:
+
+| Field | Purpose |
+| --- | --- |
+| `control` | Active browser control. |
+| `readyState` | Whether the browser app has sent its ready event. |
+| `pendingEvents` | Outbound events waiting for a ready browser. |
+
+Call `handleClose` or `dispose` when a display closes so stale controls and
+queued events are cleared.
+
+## Current Consumers
+
+The common bridge pattern is used by the newer bank, CAD, garage, and
+organization client bridges. Store currently keeps its own bridge object and
+browser bridge function names.
+
+## Usage Rules
+
+- Keep bridge inheritance in feature addons thin and explicit.
+- Keep shared code generic; do not add bank, CAD, org, or store-specific logic
+ to `common`.
+- Prefer namespaced events such as `garage::sync`.
+- Send hash maps or arrays that can be safely serialized with `toJSON`.
+- Avoid direct extension calls from the client bridge; send CBA server events.
+
+## Related Guides
+
+- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md)
+- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
+- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md)
+- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
+- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md)
diff --git a/docs/CLIENT_GARAGE_USAGE_GUIDE.md b/docs/CLIENT_GARAGE_USAGE_GUIDE.md
new file mode 100644
index 0000000..c670ff0
--- /dev/null
+++ b/docs/CLIENT_GARAGE_USAGE_GUIDE.md
@@ -0,0 +1,95 @@
+# Client Garage Usage Guide
+
+The client garage addon provides player vehicle storage UI, vehicle
+store/retrieve actions, selected nearby vehicle service requests, vehicle
+context building, and the virtual garage view.
+
+## Open Garage UI
+
+```sqf
+call forge_client_garage_fnc_openUI;
+```
+
+The garage UI opens `RscGarage`, loads `ui/_site/index.html`, and routes
+browser events through `forge_client_garage_fnc_handleUIEvents`.
+
+## Open Virtual Garage
+
+```sqf
+call forge_client_garage_fnc_openVG;
+```
+
+The virtual garage uses mission-configured `FORGE_CfgGarages` locations to set
+the spawn/preview position, opens the BIS garage interface, and restricts the
+available vehicle lists from the virtual garage repository.
+
+## Client Services
+
+| Service | Purpose |
+| --- | --- |
+| `GarageRepository` | Player garage view state. |
+| `VGRepository` | Virtual garage unlock view state. |
+| `GarageHelperService` | Vehicle names, hit points, and payload helpers. |
+| `GarageContextService` | Nearby/current vehicle context. |
+| `GaragePayloadService` | Browser hydrate payload construction. |
+| `GarageActionService` | Store/retrieve request handling and selected nearby vehicle refuel/repair request forwarding. |
+| `GarageUIBridge` | Browser ready, hydrate, and sync delivery. |
+
+## Browser Events
+
+| Event | Client behavior |
+| --- | --- |
+| `garage::ready` | Mark browser ready and send `garage::hydrate`. |
+| `garage::refresh` | Send current garage payload as `garage::sync`. |
+| `garage::vehicle::retrieve::request` | Forward retrieve request through the action service. |
+| `garage::vehicle::store::request` | Forward store request through the action service. |
+| `garage::vehicle::refuel::request` | Forward selected nearby vehicle refuel request to the server economy service. |
+| `garage::vehicle::repair::request` | Forward selected nearby vehicle repair request to the server economy service. |
+| `garage::close` | Dispose bridge screen state and close the display. |
+
+## Browser Response Events
+
+| Event | Purpose |
+| --- | --- |
+| `garage::hydrate` | Initial vehicle and session payload. |
+| `garage::sync` | Refreshed vehicle payload. |
+| `garage::service::success` | Browser notice for accepted refuel/repair requests. |
+| `garage::service::failure` | Browser notice for rejected refuel/repair requests. |
+
+Server action responses are handled by the action service and notification
+flow.
+
+## Vehicle Service
+
+The selected vehicle detail panel includes refuel and repair actions for nearby
+world vehicles. Stored records must be retrieved first because server economy
+services operate on live vehicle objects, not stored garage records.
+
+Refuel requests use the server economy `RefuelService` event. Repair requests
+use the server economy `RepairService` event. Both services are billed by the
+server economy addon through organization funds.
+
+## Mission Setup
+
+Garage interactions are normally surfaced through the actor menu when nearby
+objects have garage variables such as:
+
+```sqf
+_object setVariable ["isGarage", true, true];
+_object setVariable ["garageType", "cars", true];
+```
+
+Virtual garage access also requires configured garage locations in mission
+config so the preview/spawn position can be resolved.
+
+## Authoritative State
+
+The client gathers vehicle context and sends store/retrieve requests. Stored
+vehicle state, validation, spawning, removal, and persistence are owned by the
+server garage addon and extension.
+
+## Related Guides
+
+- [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md)
+- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md)
+- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md)
diff --git a/docs/CLIENT_LOCKER_USAGE_GUIDE.md b/docs/CLIENT_LOCKER_USAGE_GUIDE.md
new file mode 100644
index 0000000..dc8d724
--- /dev/null
+++ b/docs/CLIENT_LOCKER_USAGE_GUIDE.md
@@ -0,0 +1,87 @@
+# Client Locker Usage Guide
+
+The client locker addon manages personal locker display state, local locker
+container behavior, and virtual arsenal unlock state.
+
+## Repositories
+
+`forge_client_locker_fnc_initRepository` creates `GVAR(LockerRepository)`.
+
+`forge_client_locker_fnc_initVARepository` creates `GVAR(VARepository)`.
+
+Initialize locker state:
+
+```sqf
+GVAR(LockerRepository) call ["init", []];
+GVAR(VARepository) call ["init", []];
+```
+
+## Locker Container Flow
+
+The repository searches mission namespace variables whose names contain
+`locker` and refer to objects. For each server/mission locker object, it creates
+a local `Box_NATO_Equip_F` at the same position and attaches container event
+handlers.
+
+On container open:
+
+- the local container is cleared
+- cached locker items are inserted into the container
+- over-capacity warnings are emitted when the item count is above 25
+
+On container close:
+
+- cargo, nested container items, and weapon attachments are read back
+- the new locker map is sent to the server with the override request
+- the local repository cache is updated
+
+## Virtual Arsenal Flow
+
+The virtual arsenal repository creates a local `FORGE_Locker_Box` and requests
+virtual arsenal unlocks from the server.
+
+As sync data arrives, it applies unlocks through ACE Arsenal:
+
+| Data key | Client behavior |
+| --- | --- |
+| `items` | Add virtual items. |
+| `weapons` | Add virtual weapons. |
+| `magazines` | Add virtual magazines. |
+| `backpacks` | Add virtual backpacks. |
+
+The actor menu opens the virtual locker with:
+
+```sqf
+[FORGE_Locker_Box, player, false] spawn ace_arsenal_fnc_openBox;
+```
+
+## Server Events
+
+The client repository sends requests for:
+
+- locker initialization
+- locker save
+- locker override after container close
+- virtual arsenal initialization
+- virtual arsenal save
+
+The server locker addon and extension own the saved locker and virtual arsenal
+state.
+
+## Mission Setup
+
+Mission locker objects must be placed into `missionNamespace` with a variable
+name containing `locker`. The client creates local interactive containers from
+those authoritative mission objects.
+
+Example:
+
+```sqf
+missionNamespace setVariable ["forge_locker_alpha", _lockerObject, true];
+```
+
+## Related Guides
+
+- [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md)
+- [Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md)
+- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md)
diff --git a/docs/CLIENT_MAIN_USAGE_GUIDE.md b/docs/CLIENT_MAIN_USAGE_GUIDE.md
new file mode 100644
index 0000000..0ac8fbf
--- /dev/null
+++ b/docs/CLIENT_MAIN_USAGE_GUIDE.md
@@ -0,0 +1,48 @@
+# Client Main Usage Guide
+
+The client `main` addon provides the shared mod identity, version metadata,
+CBA settings, and macro foundation used by the Forge client addons.
+
+## Purpose
+
+Use `forge_client_main` as the foundation dependency for client addons that
+need Forge macros, function naming, settings, or mod-level configuration.
+
+Feature logic should stay in the owning addon. `main` should remain limited to
+shared client configuration and compile infrastructure.
+
+## Key Files
+
+| File | Purpose |
+| --- | --- |
+| `script_mod.hpp` | Client mod identity. |
+| `script_version.hpp` | Client mod version values. |
+| `script_macros.hpp` | Shared client macros. |
+| `CfgSettings.hpp` | Client CBA settings. |
+| `config.cpp` | Addon config and mod wiring. |
+
+## Dependency Pattern
+
+Feature addons normally depend on `forge_client_main` in their `config.cpp`.
+
+```cpp
+class forge_client_example {
+ requiredAddons[] = {
+ "forge_client_main"
+ };
+};
+```
+
+## Usage Notes
+
+- Put domain UI, repositories, and event handling in feature addons.
+- Put reusable browser bridge behavior in `forge_client_common`.
+- Put server-only behavior in `arma/server/addons`.
+- Keep settings in `CfgSettings.hpp` when they apply to the client mod as a
+ whole or to a client feature toggle.
+
+## Related Guides
+
+- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md)
+- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)
+- [Development Guide](./DEVELOPMENT_GUIDE.md)
diff --git a/docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md b/docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md
new file mode 100644
index 0000000..e102583
--- /dev/null
+++ b/docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md
@@ -0,0 +1,74 @@
+# Client Notifications Usage Guide
+
+The client notifications addon owns the notification HUD, notification sound,
+and local notification service used by Forge client and server modules.
+
+## Runtime Behavior
+
+The notification display is created during client initialization. The browser
+HUD sends:
+
+```text
+notifications::ready
+```
+
+When that event is received, `NotificationService` initializes and sends a
+startup notification.
+
+## Create a Notification
+
+Use the notification service when available:
+
+```sqf
+GVAR(NotificationService) call ["create", [
+ "success",
+ "Title",
+ "Notification text.",
+ 4000
+]];
+```
+
+Arguments:
+
+| Argument | Purpose |
+| --- | --- |
+| `_type` | Notification type, such as `success`, `info`, `warning`, or `error`. |
+| `_title` | Notification title. |
+| `_content` | Notification body text. |
+| `_duration` | Display duration in milliseconds. |
+
+The service dispatches a browser `forge:notify` custom event.
+
+## CBA Event Surface
+
+Other addons can use the client notification event:
+
+```sqf
+["forge_client_notifications_recieveNotification", [
+ "warning",
+ "Garage",
+ "Vehicle spawn position is blocked.",
+ 3000
+]] call CBA_fnc_localEvent;
+```
+
+The event payload is:
+
+```sqf
+[_type, _title, _content, _duration]
+```
+
+## Usage Rules
+
+- Use the shared notification service instead of opening separate transient
+ browser UIs.
+- Keep server-driven player feedback short and actionable.
+- Treat notification state as transient client UI state.
+- Do not use notifications as the only record of durable domain changes.
+
+## Related Guides
+
+- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md)
+- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
+- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
+- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)
diff --git a/docs/CLIENT_ORG_USAGE_GUIDE.md b/docs/CLIENT_ORG_USAGE_GUIDE.md
new file mode 100644
index 0000000..5a5723b
--- /dev/null
+++ b/docs/CLIENT_ORG_USAGE_GUIDE.md
@@ -0,0 +1,106 @@
+# Client Organization Usage Guide
+
+The client organization addon provides the organization portal UI and browser
+bridge for login, registration, membership, invites, credit lines, leave and
+disband flows, assets, fleet, and treasury display.
+
+## Open Organization UI
+
+```sqf
+call forge_client_org_fnc_openUI;
+```
+
+The UI opens `RscOrg`, loads `ui/_site/index.html`, and routes browser alerts
+through `forge_client_org_fnc_handleUIEvents`.
+
+## Repository and Bridge
+
+`forge_client_org_fnc_initRepository` caches organization portal state.
+
+`forge_client_org_fnc_initUIBridge` owns:
+
+- active browser control tracking
+- portal hydrate requests
+- create/login response routing
+- leave and disband requests
+- credit-line assignment requests
+- invite, accept invite, and decline invite requests
+- targeted browser response events
+
+## Browser Events
+
+| Event | Client behavior |
+| --- | --- |
+| `org::ready` | Mark browser ready and request `org::sync`. |
+| `org::login::request` | Request portal hydrate as `org::login::success`. |
+| `org::create::request` | Validate org name and request creation on server. |
+| `org::disband::request` | Request disband on server. |
+| `org::leave::request` | Request leave on server. |
+| `org::credit::request` | Request credit-line assignment. |
+| `org::invite::request` | Request member invite. |
+| `org::invite::accept` | Accept invite by org ID. |
+| `org::invite::decline` | Decline invite by org ID. |
+| `org::close` | Close the display. |
+
+## Browser Response Events
+
+| Event | Purpose |
+| --- | --- |
+| `org::sync` | Full portal sync payload. |
+| `org::login::success` | Login hydrate payload. |
+| `org::create::success` | Creation hydrate payload. |
+| `org::create::failure` | Creation validation or server failure. |
+| `org::disband::success` | Requester disband success. |
+| `org::disband::failure` | Disband failure. |
+| `org::portal::revoked` | Portal state revoked by someone else's disband action. |
+| `org::leave::success` | Leave success. |
+| `org::leave::failure` | Leave failure. |
+| `org::credit::success` | Credit-line request success. |
+| `org::credit::failure` | Credit-line request failure. |
+| `org::member::creditUpdated` | Targeted member credit-line patch. |
+| `org::invite::success` | Invite success. |
+| `org::invite::failure` | Invite failure. |
+| `org::invite::decision::success` | Invite accept/decline success. |
+| `org::invite::decision::failure` | Invite accept/decline failure. |
+
+## Request Examples
+
+Create organization request payload:
+
+```json
+{
+ "orgName": "Example Logistics"
+}
+```
+
+Credit-line request payload:
+
+```json
+{
+ "memberUid": "76561198000000000",
+ "memberName": "Player Name",
+ "amount": 2500
+}
+```
+
+Invite request payload:
+
+```json
+{
+ "targetUid": "76561198000000000",
+ "targetName": "Player Name"
+}
+```
+
+## Authoritative State
+
+Organization funds, reputation, membership, invites, credit lines, assets,
+fleet, and persistence are server-owned. The client portal only displays and
+requests changes.
+
+## Related Guides
+
+- [Organization Usage Guide](./ORG_USAGE_GUIDE.md)
+- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)
+- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
+- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)
diff --git a/docs/CLIENT_PHONE_USAGE_GUIDE.md b/docs/CLIENT_PHONE_USAGE_GUIDE.md
new file mode 100644
index 0000000..28832a1
--- /dev/null
+++ b/docs/CLIENT_PHONE_USAGE_GUIDE.md
@@ -0,0 +1,107 @@
+# Client Phone Usage Guide
+
+The client phone addon provides the in-game phone UI for contacts, SMS
+messages, email, and local utility apps such as notes, calendar events, world
+clocks, and alarms.
+
+## Open Phone UI
+
+```sqf
+call forge_client_phone_fnc_openUI;
+```
+
+The phone UI creates `RscPhone`, loads `ui/_site/index.html`, and routes
+browser alerts through `forge_client_phone_fnc_handleUIEvents`.
+
+## State Ownership
+
+Contacts, messages, and emails are server-owned and requested through the
+server phone addon.
+
+Local utility app state is stored in `profileNamespace`:
+
+- notes
+- calendar events
+- world clocks
+- alarms
+- theme/preferences
+
+## Phone Class
+
+`forge_client_phone_fnc_initClass` creates `GVAR(PhoneClass)`.
+
+The phone class currently owns local notes, events, and settings helpers.
+Contacts, messages, and emails continue to use server-backed request/response
+events.
+
+## Browser Events
+
+### Session and Preferences
+
+| Event | Client behavior |
+| --- | --- |
+| `phone::get::player` | Send player UID to browser with `setPlayerUid`. |
+| `phone::get::theme` | Send saved light/dark theme to browser. |
+| `phone::set::theme` | Save theme preference to `profileNamespace`. |
+
+### Contacts
+
+| Event | Client behavior |
+| --- | --- |
+| `phone::get::contacts` | Load cached contacts and request server refresh. |
+| `phone::refresh::contacts` | Request contacts from server. |
+| `phone::add::contact` | Add contact by phone number. |
+| `phone::add::contact::by::phone` | Add contact by phone number. |
+| `phone::add::contact::by::email` | Add contact by email. |
+| `phone::remove::contact` | Remove contact by UID. |
+
+### Messages
+
+| Event | Client behavior |
+| --- | --- |
+| `phone::get::messages` | Request messages from server. |
+| `phone::get::message::thread` | Request thread with another UID. |
+| `phone::send::message` | Send SMS through server. |
+| `phone::mark::message::read` | Mark message read on server. |
+| `phone::delete::message` | Delete message on server. |
+
+### Email
+
+| Event | Client behavior |
+| --- | --- |
+| `phone::get::emails` | Request emails from server. |
+| `phone::send::email` | Send email through server. |
+| `phone::mark::email::read` | Mark email read on server. |
+| `phone::delete::email` | Delete email on server. |
+
+### Local Utility Apps
+
+| Event | Client behavior |
+| --- | --- |
+| `phone::get::notes` | Load local notes. |
+| `phone::save::note` | Save local note. |
+| `phone::delete::note` | Delete local note. |
+| `phone::get::events` | Load local calendar events. |
+| `phone::save::event` | Save local calendar event. |
+| `phone::delete::event` | Delete local calendar event. |
+| `phone::get::clocks` | Load local world clocks. |
+| `phone::save::clock` | Save local world clock. |
+| `phone::delete::clock` | Delete local world clock. |
+| `phone::get::alarms` | Load local alarms. |
+| `phone::save::alarm` | Save local alarm. |
+| `phone::delete::alarm` | Delete local alarm. |
+| `phone::toggle::alarm` | Toggle local alarm enabled state. |
+
+## Usage Rules
+
+- Send contact, message, and email mutations to the server phone addon.
+- Keep local-only utility apps in `profileNamespace` until they are migrated to
+ server-backed storage.
+- Do not treat local phone utility state as shared multiplayer state.
+- Validate required UID, phone, email, subject, and message fields before
+ sending server requests.
+
+## Related Guides
+
+- [Phone Usage Guide](./PHONE_USAGE_GUIDE.md)
+- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md)
diff --git a/docs/CLIENT_STORE_USAGE_GUIDE.md b/docs/CLIENT_STORE_USAGE_GUIDE.md
new file mode 100644
index 0000000..3078d3c
--- /dev/null
+++ b/docs/CLIENT_STORE_USAGE_GUIDE.md
@@ -0,0 +1,92 @@
+# Client Store Usage Guide
+
+The client store addon provides the storefront browser UI for catalog browsing,
+category hydration, payment source display, cart handling, and checkout
+requests.
+
+## Open Store UI
+
+```sqf
+call forge_client_store_fnc_openUI;
+```
+
+The UI opens `RscStore`, loads `ui/_site/index.html`, and routes browser alerts
+through `forge_client_store_fnc_handleUIEvents`.
+
+## Bridge
+
+`forge_client_store_fnc_initUIBridge` owns:
+
+- browser control lookup
+- store hydrate requests
+- category requests
+- checkout requests
+- category hydrate/failure responses
+- checkout success/failure responses
+- store config refresh after successful checkout
+
+Store currently uses its own `StoreUIBridge.receive(...)` browser bridge rather
+than the shared `ForgeBridge.receive(...)` delivery used by newer bridges.
+
+## Browser Events
+
+| Event | Client behavior |
+| --- | --- |
+| `store::ready` | Request store hydrate from the server. |
+| `store::category::request` | Request catalog items for a category. |
+| `store::checkout::request` | Forward checkout JSON to the server. |
+| `store::close` | Close the display. |
+
+## Browser Response Events
+
+| Event | Purpose |
+| --- | --- |
+| `store::hydrate` | Initial storefront/session/config payload. |
+| `store::config::hydrate` | Refreshed payment/source config. |
+| `store::category::hydrate` | Category catalog payload. |
+| `store::category::failure` | Category request failure. |
+| `store::checkout::success` | Checkout success payload. |
+| `store::checkout::failure` | Checkout failure payload. |
+
+## Category Requests
+
+Category requests require a non-empty category value.
+
+```json
+{
+ "category": "weapons"
+}
+```
+
+The client lowercases the category before forwarding it to the server store
+addon.
+
+## Checkout Requests
+
+Checkout requests send a serialized checkout payload:
+
+```json
+{
+ "checkoutJson": "{\"items\":[],\"paymentSource\":\"cash\"}"
+}
+```
+
+The client only forwards the checkout data. The server store addon and
+extension validate prices, inventory grants, payment source authorization, and
+integration with bank, organization, locker, and garage state.
+
+After a successful checkout, the client asks the server for a fresh store config
+payload so payment-source balances and permissions stay current.
+
+## Authoritative State
+
+Catalog data, prices, checkout validation, money movement, organization funds,
+credit lines, locker grants, garage grants, and persistence are server-owned.
+
+## Related Guides
+
+- [Store Usage Guide](./STORE_USAGE_GUIDE.md)
+- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
+- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md)
+- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md)
+- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
diff --git a/docs/CLIENT_USAGE_GUIDE.md b/docs/CLIENT_USAGE_GUIDE.md
new file mode 100644
index 0000000..23f74f1
--- /dev/null
+++ b/docs/CLIENT_USAGE_GUIDE.md
@@ -0,0 +1,125 @@
+# Client Usage Guide
+
+Forge Client contains the Arma client-side addons that open player interfaces,
+handle browser events, cache client-visible state, and forward authoritative
+requests to the server addons.
+
+Use this guide as the entry point for client-side integration. Domain data,
+validation, persistence, rewards, ownership, and checkout behavior remain
+server-side responsibilities.
+
+## Client Responsibilities
+
+- Open Arma displays and `CT_WEBBROWSER` controls.
+- Load browser UI assets from each addon's `ui/_site` folder.
+- Receive browser alerts through `JSDialog` handlers.
+- Translate browser events into local actions or CBA server events.
+- Cache display state in client repositories.
+- Push server responses back into browser UIs with `ExecJS`.
+- Provide local-only utility state where the feature is intentionally local.
+
+## Authoritative Boundaries
+
+Client repositories are view state. They are useful for rendering, local UI
+decisions, and short-lived session behavior, but they should not be treated as
+durable state.
+
+Authoritative state lives in:
+
+- server SQF addons for mission and player workflow ownership
+- the `forge_server` extension for durable and hot-state domain logic
+- SurrealDB where the extension persists durable domain records
+
+## Common Runtime Flow
+
+Most browser-backed client addons follow this shape:
+
+1. The addon creates a display, finds a browser control, and registers a
+ `JSDialog` event handler.
+2. The browser loads an HTML entrypoint from `ui/_site`.
+3. The browser sends JSON alerts with an `event` name and `data` payload.
+4. `fnc_handleUIEvents.sqf` parses the alert and routes the event.
+5. A bridge object or repository sends a CBA server event when server data is
+ needed.
+6. Server responses are caught in `XEH_postInitClient.sqf`.
+7. The bridge sends browser update events back through `ExecJS`.
+
+Browser alert payload:
+
+```json
+{
+ "event": "module::action",
+ "data": {}
+}
+```
+
+## Open UI Entry Points
+
+| UI | Entry point |
+| --- | --- |
+| Actor menu | `call forge_client_actor_fnc_openUI;` |
+| Bank | `call forge_client_bank_fnc_openUI;` |
+| ATM | `[true] call forge_client_bank_fnc_openUI;` |
+| CAD | `call forge_client_cad_fnc_openUI;` |
+| Garage | `call forge_client_garage_fnc_openUI;` |
+| Virtual garage | `call forge_client_garage_fnc_openVG;` |
+| Organization portal | `call forge_client_org_fnc_openUI;` |
+| Phone | `call forge_client_phone_fnc_openUI;` |
+| Store | `call forge_client_store_fnc_openUI;` |
+
+Notifications are normally opened during client initialization and then updated
+through the notification event/service.
+
+## Addon Guides
+
+- [Client Main Usage Guide](./CLIENT_MAIN_USAGE_GUIDE.md)
+- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)
+- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md)
+- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
+- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md)
+- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
+- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md)
+- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md)
+- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md)
+- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md)
+- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)
+
+## Extension Calls
+
+Client addons should usually call server SQF events, not the `forge_server`
+extension directly. The server addon owns validation context and converts the
+request into extension commands.
+
+Example:
+
+```sqf
+[SRPC(bank,requestDeposit), [getPlayerUID player, 100]] call CFUNC(serverEvent);
+```
+
+Direct extension calls from client code bypass server authorization boundaries
+and should be avoided.
+
+## Browser Bridge Notes
+
+`forge_client_common_fnc_initWebUIBridge` provides reusable bridge and screen
+objects for newer browser UIs. It queues outbound events until a browser screen
+is ready, then delivers payloads through:
+
+```sqf
+_control ctrlWebBrowserAction ["ExecJS", format ["ForgeBridge.receive(%1)", _json]];
+```
+
+Feature addons still own their event names, request payloads, and response
+mapping.
+
+## Development Checklist
+
+- Keep feature-specific behavior in the owning addon.
+- Send authoritative changes to the server addon.
+- Use namespaced browser events such as `bank::deposit::request`.
+- Treat `profileNamespace` as local player preference or utility state only.
+- Make browser-ready events request the current server state before rendering
+ stale data.
+- Queue or ignore bridge responses when the display is closed.
+- Keep mission object setup on the mission/server side and client display logic
+ on the client side.
diff --git a/docs/ECONOMY_USAGE_GUIDE.md b/docs/ECONOMY_USAGE_GUIDE.md
new file mode 100644
index 0000000..88b6181
--- /dev/null
+++ b/docs/ECONOMY_USAGE_GUIDE.md
@@ -0,0 +1,77 @@
+# Economy Usage Guide
+
+The economy server addon owns Arma-world service behavior for fuel, medical,
+and repair interactions. It does not own money state. Money mutations go
+through extension-backed bank and organization hot state before the world
+effect is applied.
+
+## Dependencies
+
+- `forge_server_common` for logging, formatting, and player lookup.
+- `forge_server_bank` for personal medical billing.
+- `forge_server_org` for organization-funded services and medical fallback
+ debt.
+- `forge_client_actor` and `forge_client_notifications` for targeted client
+ responses.
+
+## Fuel
+
+Fuel is organization-funded.
+
+When refueling stops, `fnc_initFEconomyStore.sqf` calculates the fuel delta and
+cost, charges the player's organization through `OrgStore chargeCheckout`, and
+syncs the organization patch to online members. If organization funds cannot
+cover the refuel, the vehicle is rolled back to the fuel level it had when the
+session started.
+
+Garage UI refuel requests use the server `RefuelService` event. The fuel store
+calculates missing fuel from the vehicle config `fuelCapacity`, charges the
+player's organization, and fills the vehicle only after the organization charge
+succeeds.
+
+## Repair
+
+Repair is organization-funded.
+
+Use the repair service event:
+
+```sqf
+[QEGVAR(economy,RepairService), [_target, _unit, _cost]] call CBA_fnc_serverEvent;
+```
+
+`_cost` is optional. Passing `-1` uses the configured service repair cost.
+The target is only repaired after the organization charge succeeds.
+
+The client garage UI forwards selected nearby vehicle repair requests through
+the same event.
+
+## Medical
+
+Medical is player-funded first.
+
+When a heal is requested, `fnc_initMEconomyStore.sqf` uses this billing order:
+
+1. Charge the player's bank balance when it can cover the medical fee.
+2. Otherwise charge the player's cash when it can cover the fee.
+3. If neither personal balance can cover the fee, charge organization funds.
+4. When organization funds cover the fallback charge, record the same amount as
+ debt on the player's organization credit line.
+
+The heal only completes after one of those charges succeeds. If personal
+billing is unavailable, the heal does not fall back to organization funds
+because the server cannot verify that the player is unable to cover the fee.
+
+## Medical Debt Repayment
+
+Medical fallback debt uses the existing organization credit-line repayment
+flow. The organization treasury is reduced when the service is rendered, and
+the player's credit-line `amount_due` increases by the medical fee. When the
+player repays through the bank credit-line repayment action, player bank funds
+are moved back into the organization treasury.
+
+## Hot-Cache Boundary
+
+The economy addon should stay server-authoritative for world effects such as
+vehicle fuel, vehicle repair, healing, respawn placement, and death inventory
+movement. Bank and organization balances should continue to mutate through the
+extension-backed hot-cache services.
diff --git a/docs/MODULE_REFERENCE.md b/docs/MODULE_REFERENCE.md
index 1b9ec1e..9b16dc9 100644
--- a/docs/MODULE_REFERENCE.md
+++ b/docs/MODULE_REFERENCE.md
@@ -33,10 +33,11 @@ docs/ Framework-level documentation
| Owned Garage | Organization or owner-scoped vehicle unlock storage. | via garage/org UI | server extension only | `lib/models/src/v_garage.rs`, `lib/services/src/v_garage.rs` | `owned:garage:*` |
| Owned Locker | Organization or owner-scoped arsenal unlock storage. | via locker/org UI | server extension only | `lib/models/src/v_locker.rs`, `lib/services/src/v_locker.rs` | `owned:locker:*` |
-Guides:
+Server and extension guides:
[Actor](./ACTOR_USAGE_GUIDE.md),
[Bank](./BANK_USAGE_GUIDE.md),
[CAD](./CAD_USAGE_GUIDE.md),
+[Economy](./ECONOMY_USAGE_GUIDE.md),
[Garage](./GARAGE_USAGE_GUIDE.md),
[Locker](./LOCKER_USAGE_GUIDE.md),
[Organization](./ORG_USAGE_GUIDE.md),
@@ -45,6 +46,20 @@ Guides:
[Store](./STORE_USAGE_GUIDE.md),
[Task](./TASK_USAGE_GUIDE.md).
+Client guides:
+[Client Overview](./CLIENT_USAGE_GUIDE.md),
+[Main](./CLIENT_MAIN_USAGE_GUIDE.md),
+[Common](./CLIENT_COMMON_USAGE_GUIDE.md),
+[Actor](./CLIENT_ACTOR_USAGE_GUIDE.md),
+[Bank](./CLIENT_BANK_USAGE_GUIDE.md),
+[CAD](./CLIENT_CAD_USAGE_GUIDE.md),
+[Garage](./CLIENT_GARAGE_USAGE_GUIDE.md),
+[Locker](./CLIENT_LOCKER_USAGE_GUIDE.md),
+[Notifications](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md),
+[Organization](./CLIENT_ORG_USAGE_GUIDE.md),
+[Phone](./CLIENT_PHONE_USAGE_GUIDE.md),
+[Store](./CLIENT_STORE_USAGE_GUIDE.md).
+
## Infrastructure Modules
| Module | Purpose | Location |
@@ -52,7 +67,7 @@ Guides:
| `common` | Shared SQF helpers, base stores, utility functions, and shared UI bridge pieces. | `arma/client/addons/common`, `arma/server/addons/common` |
| `extension` | Server SQF bridge around `forge_server` extension calls and chunked transport. | `arma/server/addons/extension` |
| `main` | Mod-level configuration, pre-init wiring, and server/client startup glue. | `arma/client/addons/main`, `arma/server/addons/main` |
-| `economy` | Server-side economy store initialization and economy-specific state helpers. | `arma/server/addons/economy` |
+| `economy` | Server-side fuel, medical, and service economy helpers. Fuel and repair charge organization hot state; medical charges player bank/cash first, then organization funds with repayable member debt when personal funds cannot cover the bill. | `arma/server/addons/economy` |
| `notifications` | Client notification UI, sounds, and UI event handling. | `arma/client/addons/notifications` |
| `icom` | Rust helper for interprocess communication and event broadcasting. | `bin/icom`, `arma/server/extension/src/icom.rs` |
| `terrain` | Extension-side terrain export helper. | `arma/server/extension/src/terrain.rs` |
@@ -89,6 +104,8 @@ Nested groups use additional `:` separators, for example
| `actor:delete` | Delete actor data. |
| `actor:hot:init`, `actor:hot:get`, `actor:hot:keys`, `actor:hot:override`, `actor:hot:save`, `actor:hot:remove` | Manage actor hot state. |
+See [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md) for examples.
+
### Bank
| Command | Purpose |
@@ -99,6 +116,8 @@ Nested groups use additional `:` separators, for example
| `bank:hot:charge_checkout` | Charge a checkout against hot bank state. |
| `bank:hot:validate_pin` | Validate a PIN for bank operations. |
+See [Bank Usage Guide](./BANK_USAGE_GUIDE.md) for examples.
+
### Garage
| Command | Purpose |
@@ -127,6 +146,8 @@ See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples.
| `org:members:get`, `org:members:add`, `org:members:remove` | Manage organization membership. |
| `org:hot:*` | Runtime organization workflows including registration, invites, credit lines, checkout charging, assets, fleet, leave, disband, save, and remove. |
+See [Org Usage Guide](./ORG_USAGE_GUIDE.md) for examples.
+
### Phone
| Command | Purpose |
@@ -137,6 +158,8 @@ See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples.
| `phone:emails:list`, `phone:emails:send`, `phone:emails:mark_read`, `phone:emails:delete` | Manage emails. |
| `phone:remove` | Remove phone state for a UID. |
+See [Phone Usage Guide](./PHONE_USAGE_GUIDE.md) for examples.
+
### CAD
| Command Group | Purpose |
@@ -149,6 +172,8 @@ See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples.
| `cad:groups:build` | Build grouped CAD state. |
| `cad:view:hydrate` | Build the dispatcher view model. |
+See [CAD Usage Guide](./CAD_USAGE_GUIDE.md) for examples.
+
### Task
| Command Group | Purpose |
@@ -160,6 +185,8 @@ See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples.
| `task:defuse:increment`, `task:defuse:get` | Manage defuse counters. |
| `task:clear` | Clear task state. |
+See [Task Usage Guide](./TASK_USAGE_GUIDE.md) for examples.
+
### Owned Storage
| Command Group | Purpose |
@@ -169,6 +196,8 @@ See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples.
| `owned:locker:create`, `owned:locker:fetch`, `owned:locker:get`, `owned:locker:add`, `owned:locker:remove`, `owned:locker:delete`, `owned:locker:exists` | Owner-scoped item storage. |
| `owned:locker:hot:*` | Owner-scoped item hot state. |
+See [Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md) for examples.
+
### Other Extension Groups
| Command Group | Purpose |
diff --git a/docs/README.md b/docs/README.md
index 641e9ba..09210e6 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -14,11 +14,12 @@ collects framework-level documentation for those pieces.
- [Development Guide](./DEVELOPMENT_GUIDE.md): how to add or change a module
without breaking the framework boundaries.
-## Existing Usage Guides
+## Server and Extension Usage Guides
- [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md)
- [Bank Usage Guide](./BANK_USAGE_GUIDE.md)
- [CAD Usage Guide](./CAD_USAGE_GUIDE.md)
+- [Economy Usage Guide](./ECONOMY_USAGE_GUIDE.md)
- [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md)
- [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md)
- [Organization Usage Guide](./ORG_USAGE_GUIDE.md)
@@ -27,6 +28,21 @@ collects framework-level documentation for those pieces.
- [Store Usage Guide](./STORE_USAGE_GUIDE.md)
- [Task Usage Guide](./TASK_USAGE_GUIDE.md)
+## Client Usage Guides
+
+- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md)
+- [Client Main Usage Guide](./CLIENT_MAIN_USAGE_GUIDE.md)
+- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md)
+- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md)
+- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
+- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md)
+- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
+- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md)
+- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md)
+- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md)
+- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md)
+- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md)
+
## Related Documentation
- [Server Extension Docs](../arma/server/docs/README.md)
diff --git a/docs/TASK_USAGE_GUIDE.md b/docs/TASK_USAGE_GUIDE.md
index 6003e55..0639dbb 100644
--- a/docs/TASK_USAGE_GUIDE.md
+++ b/docs/TASK_USAGE_GUIDE.md
@@ -128,6 +128,83 @@ The task addon provides these server-owned task flows:
- `hostage`
- `hvt`
+Mission designers can create tasks in four ways:
+
+- Eden modules for editor-authored tasks.
+- `forge_server_task_fnc_startTask` for script-authored tasks.
+- `forge_server_task_fnc_handler` for pre-registered entities with reputation
+ gating and ownership binding. This path expects the BIS task and catalog
+ entry to already exist if map-task and CAD visibility are required.
+- Direct task function calls for server-owned or mission-authored flows that
+ intentionally fall back to the `default` org. This path expects the BIS task
+ to already exist if map-task visibility is required.
+
+The dynamic mission manager can also generate attack tasks from config. That is
+system-generated content rather than a hand-authored task creation path.
+
+## CAD Compatibility
+
+CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
+have a catalog entry and active task status before CAD can show and assign it.
+
+CAD-compatible creation paths:
+
+- Eden modules: compatible because they delegate to
+ `forge_server_task_fnc_startTask`.
+- `forge_server_task_fnc_startTask`: compatible because it registers the
+ catalog entry, creates the BIS task, and dispatches through the handler.
+- Dynamic mission manager attack tasks: compatible because the mission manager
+ uses `forge_server_task_fnc_startTask`.
+
+Limited or incompatible paths:
+
+- `forge_server_task_fnc_handler`: only compatible if a catalog entry was
+ already registered elsewhere. The handler sets active status and ownership,
+ but it does not create the BIS task shown in the map task tab or upsert the
+ catalog entry.
+- Direct task function calls: not CAD-compatible by default. They bypass
+ `startTask` and usually do not register the task catalog entry or active
+ status that CAD hydrates from. They also only call `BIS_fnc_taskSetState` at
+ completion/failure; they do not create the BIS task first.
+
+## BIS Map Task Prerequisite
+
+Only the Eden task modules and `forge_server_task_fnc_startTask` create the BIS
+task automatically through `BIS_fnc_taskCreate`.
+
+If a mission uses `forge_server_task_fnc_handler` directly or calls a task flow
+function such as `forge_server_task_fnc_attack`, the mission must create a BIS
+task with the same task ID before the Forge task completes. Otherwise the
+success/failure `BIS_fnc_taskSetState` call has no visible map task to update.
+
+That prerequisite can be satisfied with a vanilla Eden task creation module or
+a scripted `BIS_fnc_taskCreate` call. `forge_server_task_fnc_startTask` is the
+preferred Forge path because it handles BIS task creation, Forge catalog
+registration, entity registration, and handler dispatch together.
+
+## Eden Modules
+
+Eden task modules are the normal designer-facing path. Place the module,
+configure its attributes, and sync it to the relevant entities or grouping
+modules.
+
+Available task modules:
+
+- `FORGE_Module_Attack`: sync directly to target units or vehicles.
+- `FORGE_Module_Destroy`: sync directly to objects, vehicles, or units.
+- `FORGE_Module_Defuse`: sync to `FORGE_Module_Explosives` and optionally
+ `FORGE_Module_Protected`.
+- `FORGE_Module_Delivery`: sync to `FORGE_Module_Cargo`; the cargo module syncs
+ to cargo objects.
+- `FORGE_Module_Hostage`: sync to `FORGE_Module_Hostages` and
+ `FORGE_Module_Shooters`.
+- `FORGE_Module_HVT`: sync directly to HVT units.
+- `FORGE_Module_Defend`: configure the defense marker and wave settings.
+
+These modules delegate to `forge_server_task_fnc_startTask`.
+
+## Scripted Start Task
+
Use `forge_server_task_fnc_startTask` when creating tasks from modules,
mission scripts, or generated mission-manager content. It registers task
entities, creates the BIS task, stores the catalog entry, then dispatches
@@ -155,8 +232,12 @@ through `forge_server_task_fnc_handler`.
] call forge_server_task_fnc_startTask;
```
+## Handler Calls
+
Use `forge_server_task_fnc_handler` directly when the task entities are already
-registered and you want reputation gating plus ownership binding:
+registered and you want reputation gating plus ownership binding. Create the
+BIS task and catalog entry separately if this task should appear in the map
+task tab or CAD:
```sqf
[
@@ -167,9 +248,12 @@ registered and you want reputation gating plus ownership binding:
] call forge_server_task_fnc_handler;
```
+## Direct Task Calls
+
Direct task function calls still work for mission-authored or server-owned
tasks, but they do not provide a requester UID. Ownership falls back to the
-`default` org.
+`default` org. Create the BIS task separately if this task should appear in the
+map task tab.
## Timer Semantics
diff --git a/lib/models/src/org.rs b/lib/models/src/org.rs
index 5d05592..266fec3 100644
--- a/lib/models/src/org.rs
+++ b/lib/models/src/org.rs
@@ -180,6 +180,10 @@ pub struct OrgCheckoutContext {
pub requester_uid: String,
pub org_id: String,
pub requester_is_default_org_ceo: bool,
+ #[serde(default)]
+ pub allow_member_charge: bool,
+ #[serde(default)]
+ pub record_member_debt: bool,
pub source: String,
pub amount: f64,
pub commit: bool,
diff --git a/lib/services/src/org.rs b/lib/services/src/org.rs
index 066d72b..f3e5ae0 100644
--- a/lib/services/src/org.rs
+++ b/lib/services/src/org.rs
@@ -793,24 +793,69 @@ impl OrgHotStateService {
match context.source.trim().to_ascii_lowercase().as_str() {
"org_funds" => {
- if !can_manage_treasury(
+ let charged_amount = round_currency(context.amount);
+ let can_charge_org_funds = can_manage_treasury(
&org,
&context.requester_uid,
context.requester_is_default_org_ceo,
- ) {
+ ) || (context.allow_member_charge
+ && org.members.contains_key(&context.requester_uid));
+
+ if !can_charge_org_funds {
return Err(
"Only the organization leader or CEO can charge org funds.".to_string()
);
}
- if org.funds < context.amount {
+ if org.funds < charged_amount {
return Err("Organization funds cannot cover this checkout.".to_string());
}
- org.funds -= context.amount;
+ org.funds = round_currency(org.funds - charged_amount);
+ if context.record_member_debt {
+ let member_name = org
+ .members
+ .get(&context.requester_uid)
+ .map(|member| member.name.clone())
+ .filter(|name| !name.trim().is_empty())
+ .unwrap_or_else(|| "Unknown".to_string());
+ let mut credit_line = org
+ .credit_lines
+ .get(&context.requester_uid)
+ .cloned()
+ .unwrap_or_else(|| CreditLineSummary {
+ uid: context.requester_uid.clone(),
+ name: member_name.clone(),
+ approved_amount: 0.0,
+ available_amount: 0.0,
+ outstanding_principal: 0.0,
+ interest_rate: DEFAULT_CREDIT_LINE_INTEREST_RATE,
+ amount_due: 0.0,
+ amount: 0.0,
+ });
+ credit_line.normalize();
+ credit_line.uid = context.requester_uid.clone();
+ credit_line.name = member_name;
+ if credit_line.interest_rate <= 0.0 {
+ credit_line.interest_rate = DEFAULT_CREDIT_LINE_INTEREST_RATE;
+ }
+ credit_line.outstanding_principal =
+ round_currency(credit_line.outstanding_principal + charged_amount);
+ credit_line.amount_due =
+ round_currency(credit_line.amount_due + charged_amount);
+ credit_line.amount = credit_line.available_amount;
+ org.credit_lines
+ .insert(context.requester_uid.clone(), credit_line);
+ }
self.repository.save(&org)?;
+ let patch_fields = if context.record_member_debt {
+ vec!["funds", "credit_lines"]
+ } else {
+ vec!["funds"]
+ };
+
Ok(OrgMutationResult {
- patch: build_org_patch(&org, &["funds"])?,
+ patch: build_org_patch(&org, &patch_fields)?,
member_uids,
message: String::new(),
org,
@@ -1220,3 +1265,158 @@ fn format_currency(amount: f64) -> String {
fn round_currency(amount: f64) -> f64 {
(amount.max(0.0) * 100.0).round() / 100.0
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use forge_repositories::InMemoryOrgHotRepository;
+
+ #[derive(Clone, Default)]
+ struct TestOrgRepository;
+
+ impl OrgRepository for TestOrgRepository {
+ fn create(&self, _org: &Org) -> Result<(), String> {
+ Ok(())
+ }
+
+ fn get_by_id(&self, _id: &str) -> Result