diff --git a/arma/server/addons/economy/README.md b/arma/server/addons/economy/README.md
index 51adab8..f2e567c 100644
--- a/arma/server/addons/economy/README.md
+++ b/arma/server/addons/economy/README.md
@@ -2,30 +2,80 @@
## 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, 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.
+
+## 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.
+
+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..7b5bd48 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,11 @@ 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(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..eb820f9 100644
--- a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf
+++ b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf
@@ -8,16 +8,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.
*
* 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 +38,66 @@ 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
+ }],
["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/docs/ECONOMY_USAGE_GUIDE.md b/docs/ECONOMY_USAGE_GUIDE.md
new file mode 100644
index 0000000..3552e15
--- /dev/null
+++ b/docs/ECONOMY_USAGE_GUIDE.md
@@ -0,0 +1,69 @@
+# 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.
+
+## 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.
+
+## 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 eb50fc8..9b16dc9 100644
--- a/docs/MODULE_REFERENCE.md
+++ b/docs/MODULE_REFERENCE.md
@@ -37,6 +37,7 @@ 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),
@@ -66,7 +67,7 @@ Client 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` |
diff --git a/docs/README.md b/docs/README.md
index 81c25a7..09210e6 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -19,6 +19,7 @@ collects framework-level documentation for those pieces.
- [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)
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